Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Synthetic secret fixtures retained in Git history; tracked in pickforge/picklab#60.
01c7e60369b9e3a415218a6b23a397c2eeb9abd4:crates/pickforge-cli/tests/evidence.rs:generic-api-key:184
de467fbd14d00d4afa4946f610d05d724a6fa777:packages/browser/test/evidence-integration.test.ts:jwt:27
d3e4353d8fbd470deaaab30c61c7fdb0c5b14727:packages/browser/test/evidence-integration.test.ts:jwt:27
596c7c21cd5472c99db0f516988b5483354e2fe7:packages/core/test/redact.test.ts:jwt:258
Expand Down
83 changes: 83 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/pickforge-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
serde_yaml_ng = "0.10"
sha2 = "0.10"
regex = "1"
thiserror = "2"
time = { version = "0.3", features = ["formatting", "macros"] }
tempfile = "3"
toml = { version = "1.1", default-features = false, features = ["parse", "serde", "std"] }
which = "8"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"] }

[dev-dependencies]
assert_cmd = "2"
14 changes: 12 additions & 2 deletions crates/pickforge-cli/assets/skills/pickforge-flutter/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ description: >-
`flutter_driver`, or enable the driver extension. Ask first if any of those
are necessary.
4. Run scoped analysis and tests for the changed source, then use hot reload.
5. Repeat the same runtime scenario and capture before/after evidence. Hand off
the source mapping, change, checks, and observed result.
5. Repeat the same runtime scenario and capture before/after evidence. Review
every screenshot first: Pickforge cannot redact secret or private pixels.
6. Supply the complete bounded envelope and absolute image paths in one call:

```sh
pickforge evidence record --project-dir "$PWD" --input - <<'JSON'
{"schemaVersion":1,"scenario":"Counter increments after hot reload","outcome":"passed","before":{"summary":"Counter stayed at zero.","observations":[],"artifacts":[]},"after":{"summary":"Counter changed to one.","observations":[{"label":"Counter","value":"1"}],"artifacts":[]},"sourceChanges":["lib/main.dart"],"checks":[{"name":"flutter test","status":"passed","summary":"Focused tests passed."}],"limitations":[]}
JSON
```

Pickforge only validates and records this envelope; it never invokes MCP,
Flutter, Dart, Git, network tools, or screenshot capture.

If an MCP tool, resource, runtime, or hot-reload capability is unavailable, name
that exact capability and run `pickforge doctor`; never fabricate evidence.
Expand Down
2 changes: 1 addition & 1 deletion crates/pickforge-cli/src/adapters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ impl IntegrationPack {
pub fn flutter() -> Self {
Self {
name: "pickforge-flutter".into(),
version: 1,
version: 2,
mcp_servers: vec![McpServerSpec {
name: "pickforge-dart".into(),
command: "dart".into(),
Expand Down
76 changes: 68 additions & 8 deletions crates/pickforge-cli/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,28 @@ fn normalize_key(key: String) -> String {
}
}

/// The ambient inputs `doctor` is allowed to read: environment variables and
/// the user's home directory.
fn collect_vars(
vars: impl IntoIterator<Item = (OsString, OsString)>,
) -> BTreeMap<String, OsString> {
vars.into_iter()
.filter_map(|(key, value)| Some((normalize_key(key.into_string().ok()?), value)))
.collect()
}

fn home_from_vars(vars: &BTreeMap<String, OsString>) -> Option<PathBuf> {
#[cfg(windows)]
const HOME_KEYS: [&str; 2] = ["USERPROFILE", "HOME"];
#[cfg(not(windows))]
const HOME_KEYS: [&str; 1] = ["HOME"];

HOME_KEYS.iter().find_map(|key| {
let path = PathBuf::from(vars.get(*key)?);
path.is_absolute().then_some(path)
})
}

/// The ambient inputs CLI commands are allowed to read: environment variables
/// and the user's home directory.
#[derive(Debug, Clone, Default)]
pub struct Environment {
vars: BTreeMap<String, OsString>,
Expand All @@ -30,12 +50,10 @@ pub struct Environment {
impl Environment {
/// The real process environment.
pub fn from_process() -> Self {
Self {
vars: std::env::vars_os()
.filter_map(|(key, value)| Some((normalize_key(key.into_string().ok()?), value)))
.collect(),
home_dir: directories::BaseDirs::new().map(|dirs| dirs.home_dir().to_path_buf()),
}
let vars = collect_vars(std::env::vars_os());
let home_dir = home_from_vars(&vars)
.or_else(|| directories::BaseDirs::new().map(|dirs| dirs.home_dir().to_path_buf()));
Self { vars, home_dir }
}

/// An environment with no variables and no home directory.
Expand Down Expand Up @@ -68,3 +86,45 @@ impl Environment {
self.home_dir.as_deref()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn process_home_requires_an_absolute_path() {
let mut vars = BTreeMap::new();
vars.insert(normalize_key("HOME".into()), OsString::from("relative"));
assert_eq!(home_from_vars(&vars), None);

#[cfg(windows)]
let absolute = PathBuf::from(r"C:\isolated-home");
#[cfg(not(windows))]
let absolute = PathBuf::from("/isolated-home");
vars.insert(normalize_key("HOME".into()), absolute.clone().into());
assert_eq!(home_from_vars(&vars), Some(absolute));
}

#[cfg(windows)]
#[test]
fn userprofile_precedes_home_on_windows() {
let profile = PathBuf::from(r"C:\profile");
let home = PathBuf::from(r"D:\home");
let vars = collect_vars([
(OsString::from("UserProfile"), profile.clone().into()),
(OsString::from("Home"), home.into()),
]);
assert_eq!(home_from_vars(&vars), Some(profile));
}

#[cfg(windows)]
#[test]
fn relative_userprofile_falls_through_to_home() {
let home = PathBuf::from(r"D:\home");
let vars = collect_vars([
(OsString::from("UserProfile"), OsString::from("relative")),
(OsString::from("Home"), home.clone().into()),
]);
assert_eq!(home_from_vars(&vars), Some(home));
}
}
Loading
Loading