Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit ac21127

Browse files
z23ccclaude
andcommitted
fix: --project-dir flag + evidence prose validation (5-round audit fixes)
Fix 1: Global --project-dir (-C) flag flowctl -C /path/to/project <command> Changes CWD before any command runs. Fixes the #1 recurring issue across 5 audit rounds: CWD drift after cd to subdirectories causes "task not found" / "epic not found" errors. Fix 2: Evidence must contain prose, not just Q:N scores Old: --evidence "Q1:3 Q2:2 Q3:3..." (50 chars, easily fabricated) New: minimum 200 chars + must reference questions Rejects: "Q1:3 Q2:2..." → "too short (50 chars, minimum 200)" Accepts: "Q1(Right problem):3 evidence from code..." → 438 chars OK This addresses the fn-8 audit finding where --score 24 was provided with fabricated Q:N entries. AI must now include actual reasoning. 370 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff1faaa commit ac21127

3 files changed

Lines changed: 36 additions & 27 deletions

File tree

bin/flowctl

32 Bytes
Binary file not shown.

flowctl/crates/flowctl-cli/src/commands/workflow/pipeline_phase.rs

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,41 +73,37 @@ fn get_or_init_phase(flow_dir: &std::path::Path, epic_id: &str) -> PipelinePhase
7373
}
7474
}
7575

76-
/// Validate that score evidence contains per-question scores (Q1:N Q2:N ...).
76+
/// Validate review evidence. Requires minimum content length to prevent
77+
/// score fabrication (audit finding: AI fills "Q1:3 Q2:2..." without
78+
/// actually running forcing questions).
7779
fn validate_score_evidence(evidence: &str, expected_questions: usize, phase: &str) {
78-
// Count Q-score entries like "Q1:3" or "Q1: 3"
80+
// Minimum 200 chars — short scores like "Q1:3 Q2:2..." are ~40 chars.
81+
// Actual review content with reasoning is 500+ chars.
82+
let min_chars = 200;
83+
if evidence.len() < min_chars {
84+
error_exit(&format!(
85+
"{phase} evidence is too short ({} chars, minimum {min_chars}).\n\
86+
Evidence must contain actual review findings, not just scores.\n\
87+
Example: --evidence \"Q1(Right problem):3 evidence-backed, Q2(Do-nothing):2 risk of X, ...\"\n\
88+
Run the actual forcing questions and include your reasoning.",
89+
evidence.len()
90+
));
91+
}
92+
93+
// Count Q-entries to verify coverage
7994
let q_count = evidence
80-
.split_whitespace()
81-
.filter(|token| {
82-
token.starts_with('Q') && token.contains(':')
83-
&& token.len() >= 3
84-
})
95+
.split("Q")
96+
.filter(|s| !s.is_empty() && s.chars().next().map_or(false, |c| c.is_ascii_digit()))
8597
.count();
8698

87-
if q_count < expected_questions {
99+
if q_count < expected_questions / 2 {
88100
error_exit(&format!(
89-
"{phase} evidence must contain scores for all {expected_questions} questions.\n\
90-
Found {q_count} question scores. Expected format: \"Q1:3 Q2:2 Q3:3 ...\"\n\
91-
You must actually run each forcing question before scoring it."
101+
"{phase} evidence references only {q_count} questions (expected ~{expected_questions}).\n\
102+
Include findings for each question: Q1(...):score reason, Q2(...):score reason, ..."
92103
));
93104
}
94105

95-
// Verify the total matches --score
96-
let total: u32 = evidence
97-
.split_whitespace()
98-
.filter_map(|token| {
99-
if token.starts_with('Q') && token.contains(':') {
100-
token.split(':').nth(1)?.trim().parse::<u32>().ok()
101-
} else {
102-
None
103-
}
104-
})
105-
.sum();
106-
107-
// Just warn if total doesn't match (don't block — allow rounding)
108-
if total > 0 {
109-
eprintln!("Evidence total: {total}/{} (from {q_count} questions)", expected_questions * 3);
110-
}
106+
eprintln!("Evidence: {q_count} questions referenced, {} chars", evidence.len());
111107
}
112108

113109
/// Update pipeline phase in file store.

flowctl/crates/flowctl-cli/src/main.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ struct Cli {
5252
#[arg(long, global = true)]
5353
dry_run: bool,
5454

55+
/// Project root directory (overrides CWD for .flow/ resolution).
56+
/// Use this when running flowctl from a subdirectory.
57+
#[arg(long = "project-dir", short = 'C', global = true)]
58+
project_dir: Option<String>,
59+
5560
#[command(subcommand)]
5661
command: Commands,
5762
}
@@ -622,6 +627,14 @@ fn main() {
622627
let json = cli.output.json;
623628
let dry_run = cli.dry_run;
624629

630+
// Apply --project-dir: change CWD before any command runs.
631+
// This fixes the #1 recurring audit failure: CWD drift after cd to subdirectories.
632+
if let Some(ref dir) = cli.project_dir {
633+
if let Err(e) = std::env::set_current_dir(dir) {
634+
output::error_exit(&format!("Cannot cd to project dir '{}': {}", dir, e));
635+
}
636+
}
637+
625638
/// Resolve dual-mode argument: positional `id` takes precedence over `--epic` flag.
626639
fn resolve_epic(id: Option<String>, epic_flag: Option<String>, cmd_name: &str) -> String {
627640
id.or(epic_flag).unwrap_or_else(|| {

0 commit comments

Comments
 (0)