@kontourai/flow-agents 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/build/src/builder-flow-runtime.js +10 -5
  3. package/context/contracts/verification-contract.md +6 -0
  4. package/dist/base/build/package.json +1 -1
  5. package/dist/base/build/src/builder-flow-runtime.js +10 -5
  6. package/dist/base/context/contracts/verification-contract.md +6 -0
  7. package/dist/base/evals/acceptance/prove-capture-teeth.sh +24 -0
  8. package/dist/base/evals/integration/test_effective_backlog_settings.sh +118 -15
  9. package/dist/base/install.sh +1 -1
  10. package/dist/base/scripts/hooks/lib/codex-exit-code.js +75 -23
  11. package/dist/claude-code/build/package.json +1 -1
  12. package/dist/claude-code/build/src/builder-flow-runtime.js +10 -5
  13. package/dist/claude-code/context/contracts/verification-contract.md +6 -0
  14. package/dist/claude-code/evals/acceptance/prove-capture-teeth.sh +24 -0
  15. package/dist/claude-code/evals/integration/test_effective_backlog_settings.sh +118 -15
  16. package/dist/claude-code/install.sh +1 -1
  17. package/dist/claude-code/scripts/hooks/lib/codex-exit-code.js +75 -23
  18. package/dist/codex/build/package.json +1 -1
  19. package/dist/codex/build/src/builder-flow-runtime.js +10 -5
  20. package/dist/codex/context/contracts/verification-contract.md +6 -0
  21. package/dist/codex/evals/acceptance/prove-capture-teeth.sh +24 -0
  22. package/dist/codex/evals/integration/test_effective_backlog_settings.sh +118 -15
  23. package/dist/codex/install.sh +1 -1
  24. package/dist/codex/scripts/hooks/lib/codex-exit-code.js +75 -23
  25. package/dist/kiro/build/package.json +1 -1
  26. package/dist/kiro/build/src/builder-flow-runtime.js +10 -5
  27. package/dist/kiro/context/contracts/verification-contract.md +6 -0
  28. package/dist/kiro/evals/acceptance/prove-capture-teeth.sh +24 -0
  29. package/dist/kiro/evals/integration/test_effective_backlog_settings.sh +118 -15
  30. package/dist/kiro/install.sh +1 -1
  31. package/dist/kiro/scripts/hooks/lib/codex-exit-code.js +75 -23
  32. package/dist/opencode/build/package.json +1 -1
  33. package/dist/opencode/build/src/builder-flow-runtime.js +10 -5
  34. package/dist/opencode/context/contracts/verification-contract.md +6 -0
  35. package/dist/opencode/evals/acceptance/prove-capture-teeth.sh +24 -0
  36. package/dist/opencode/evals/integration/test_effective_backlog_settings.sh +118 -15
  37. package/dist/opencode/install.sh +1 -1
  38. package/dist/opencode/scripts/hooks/lib/codex-exit-code.js +75 -23
  39. package/dist/pi/build/package.json +1 -1
  40. package/dist/pi/build/src/builder-flow-runtime.js +10 -5
  41. package/dist/pi/context/contracts/verification-contract.md +6 -0
  42. package/dist/pi/evals/acceptance/prove-capture-teeth.sh +24 -0
  43. package/dist/pi/evals/integration/test_effective_backlog_settings.sh +118 -15
  44. package/dist/pi/install.sh +1 -1
  45. package/dist/pi/scripts/hooks/lib/codex-exit-code.js +75 -23
  46. package/evals/acceptance/prove-capture-teeth.sh +24 -0
  47. package/evals/integration/test_effective_backlog_settings.sh +118 -15
  48. package/package.json +2 -1
  49. package/scripts/hooks/lib/codex-exit-code.js +75 -23
  50. package/src/builder-flow-runtime.ts +10 -5
  51. package/src/cli/builder-flow-runtime.test.mjs +55 -2
  52. package/src/cli/codex-exit-code.test.mjs +217 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.2.1](https://github.com/kontourai/flow-agents/compare/v4.2.0...v4.2.1) (2026-07-19)
4
+
5
+
6
+ ### Fixes
7
+
8
+ * correlate Codex command results exactly ([#710](https://github.com/kontourai/flow-agents/issues/710)) ([089aca3](https://github.com/kontourai/flow-agents/commit/089aca32ecc41d637d770ae4b1b447956de0dc3f))
9
+ * restore clean npm installs ([#707](https://github.com/kontourai/flow-agents/issues/707)) ([2096acd](https://github.com/kontourai/flow-agents/commit/2096acddb6bf48db5dcad2a24f744b5b72022e73))
10
+ * scope critique freshness to gate visits ([#724](https://github.com/kontourai/flow-agents/issues/724)) ([d2dc723](https://github.com/kontourai/flow-agents/commit/d2dc723b73f27fa24e94604492dbb1fffc82af1d))
11
+
3
12
  ## [4.2.0](https://github.com/kontourai/flow-agents/compare/v4.1.0...v4.2.0) (2026-07-18)
4
13
 
5
14
 
@@ -572,7 +572,7 @@ async function bundleGateEvidence(bundle, gate, state, subject, projectRoot, man
572
572
  throw new BuilderBuildRunInputError("evidence.claims.metadata.gate_claim.route_reason", `is not declared by gate ${String(gate.id ?? "<unknown>")}`);
573
573
  }
574
574
  if (String(gate.id) === "verify-gate" && relevant.some((claim) => claim.claimType === "builder.verify.tests" && claim.value === "pass")) {
575
- await assertVerifiedTestsTrust(bundle.claims, projectRoot);
575
+ await assertVerifiedTestsTrust(relevant, projectRoot);
576
576
  }
577
577
  return { failed, routeReason, expectationIds, visitEnteredAt: enteredAt };
578
578
  }
@@ -602,8 +602,8 @@ function timestampAtOrAfter(value, boundary) {
602
602
  const parsed = parseTimestamp(value);
603
603
  return parsed !== null && parsed >= boundary;
604
604
  }
605
- async function assertVerifiedTestsTrust(claims, projectRoot) {
606
- const testClaims = claims.filter((claim) => isRecord(claim)
605
+ async function assertVerifiedTestsTrust(currentGateClaims, projectRoot) {
606
+ const testClaims = currentGateClaims.filter((claim) => isRecord(claim)
607
607
  && claim.claimType === "builder.verify.tests"
608
608
  && claim.value === "pass"
609
609
  && isRecord(claim.metadata)
@@ -611,7 +611,12 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
611
611
  && claim.metadata.gate_claim.expectation_id === "tests-evidence");
612
612
  if (testClaims.length === 0)
613
613
  throw new BuilderBuildRunInputError("evidence.tests", "is missing a passing tests-evidence claim");
614
- const liveCritiques = claims.filter((claim) => isRecord(claim)
614
+ // A route-back starts a new gate visit and therefore a new critique generation. Historical
615
+ // reviewer slices remain in the bundle and manifest for audit, but only critiques acquired
616
+ // during this visit describe the implementation snapshot currently being verified. Within a
617
+ // visit every live reviewer slice still participates, so changing reviewers cannot bury a
618
+ // disputed finding.
619
+ const liveCritiques = currentGateClaims.filter((claim) => isRecord(claim)
615
620
  && isRecord(claim.metadata)
616
621
  && claim.metadata.origin === "critique"
617
622
  && typeof claim.metadata.superseded_by !== "string");
@@ -627,7 +632,7 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
627
632
  await Promise.all(artifacts.map((artifact) => assertReviewedArtifactDigest(artifact, projectRoot)));
628
633
  assertReviewedWorkspaceSnapshot(claim, artifacts, projectRoot);
629
634
  }));
630
- const criteria = claims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
635
+ const criteria = currentGateClaims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
631
636
  if (criteria.length === 0 || criteria.some((claim) => {
632
637
  const criterion = isRecord(claim.metadata.criterion) ? claim.metadata.criterion : null;
633
638
  return claim.value !== "pass" || !criterion || !Array.isArray(criterion.evidence_refs) || criterion.evidence_refs.length === 0;
@@ -20,6 +20,12 @@ Verifiers and reviewers do not modify source code. They may run commands, inspec
20
20
 
21
21
  Verifiers write evidence and acceptance status **through** `record-evidence` (directly, or via the orchestrating skill), never by hand-authoring `evidence.json`, `acceptance.json`, or `trust.bundle`. Those bespoke sidecars were retired as the source of truth by ADR 0010 Phase 4c; the `trust.bundle` is the sole verification artifact, and only the sidecar writer performs the evidence classification (`evidenceType`/`method` derivation and manifest reconciliation) the CI trust anchor depends on (ADR 0020). `config-protection.js` blocks direct tool writes to these gate files by design. If the sidecar writer is unavailable, record the exact gap instead of writing the files by hand.
22
22
 
23
+ ### Critique Generations Across Route-Backs
24
+
25
+ Each visit to a verification gate establishes a critique generation. Critiques acquired during that visit must target the current implementation snapshot, and every live reviewer slice in the generation participates in the gate decision. A disputed finding therefore cannot be hidden by adding a different reviewer.
26
+
27
+ When Flow routes back and later re-enters verification, a fresh critique generation is required. Older reviewer slices remain immutable audit history in the trust bundle and evidence manifest, but their prior snapshot hashes are not reinterpreted as reviews of the new implementation. A reviewer handoff must use the public critique writer; it must never require actor impersonation or direct trust-bundle edits.
28
+
23
29
  ## Mutation Testing Runs In A Scratch Copy
24
30
 
25
31
  Mutation-testing tools (Stryker or equivalent) **must** run against a scratch/throwaway copy of the working tree, never the live working tree. They deliberately introduce defects to measure test-suite sensitivity; running them in place risks leaving mutated source, corrupting the checkout, or tripping the gate/anchor on injected failures. Copy the tree to a temporary directory (or a git worktree/clone) and run the mutation tool there; discard it afterward.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "type": "module"
5
5
  }
@@ -572,7 +572,7 @@ async function bundleGateEvidence(bundle, gate, state, subject, projectRoot, man
572
572
  throw new BuilderBuildRunInputError("evidence.claims.metadata.gate_claim.route_reason", `is not declared by gate ${String(gate.id ?? "<unknown>")}`);
573
573
  }
574
574
  if (String(gate.id) === "verify-gate" && relevant.some((claim) => claim.claimType === "builder.verify.tests" && claim.value === "pass")) {
575
- await assertVerifiedTestsTrust(bundle.claims, projectRoot);
575
+ await assertVerifiedTestsTrust(relevant, projectRoot);
576
576
  }
577
577
  return { failed, routeReason, expectationIds, visitEnteredAt: enteredAt };
578
578
  }
@@ -602,8 +602,8 @@ function timestampAtOrAfter(value, boundary) {
602
602
  const parsed = parseTimestamp(value);
603
603
  return parsed !== null && parsed >= boundary;
604
604
  }
605
- async function assertVerifiedTestsTrust(claims, projectRoot) {
606
- const testClaims = claims.filter((claim) => isRecord(claim)
605
+ async function assertVerifiedTestsTrust(currentGateClaims, projectRoot) {
606
+ const testClaims = currentGateClaims.filter((claim) => isRecord(claim)
607
607
  && claim.claimType === "builder.verify.tests"
608
608
  && claim.value === "pass"
609
609
  && isRecord(claim.metadata)
@@ -611,7 +611,12 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
611
611
  && claim.metadata.gate_claim.expectation_id === "tests-evidence");
612
612
  if (testClaims.length === 0)
613
613
  throw new BuilderBuildRunInputError("evidence.tests", "is missing a passing tests-evidence claim");
614
- const liveCritiques = claims.filter((claim) => isRecord(claim)
614
+ // A route-back starts a new gate visit and therefore a new critique generation. Historical
615
+ // reviewer slices remain in the bundle and manifest for audit, but only critiques acquired
616
+ // during this visit describe the implementation snapshot currently being verified. Within a
617
+ // visit every live reviewer slice still participates, so changing reviewers cannot bury a
618
+ // disputed finding.
619
+ const liveCritiques = currentGateClaims.filter((claim) => isRecord(claim)
615
620
  && isRecord(claim.metadata)
616
621
  && claim.metadata.origin === "critique"
617
622
  && typeof claim.metadata.superseded_by !== "string");
@@ -627,7 +632,7 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
627
632
  await Promise.all(artifacts.map((artifact) => assertReviewedArtifactDigest(artifact, projectRoot)));
628
633
  assertReviewedWorkspaceSnapshot(claim, artifacts, projectRoot);
629
634
  }));
630
- const criteria = claims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
635
+ const criteria = currentGateClaims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
631
636
  if (criteria.length === 0 || criteria.some((claim) => {
632
637
  const criterion = isRecord(claim.metadata.criterion) ? claim.metadata.criterion : null;
633
638
  return claim.value !== "pass" || !criterion || !Array.isArray(criterion.evidence_refs) || criterion.evidence_refs.length === 0;
@@ -20,6 +20,12 @@ Verifiers and reviewers do not modify source code. They may run commands, inspec
20
20
 
21
21
  Verifiers write evidence and acceptance status **through** `record-evidence` (directly, or via the orchestrating skill), never by hand-authoring `evidence.json`, `acceptance.json`, or `trust.bundle`. Those bespoke sidecars were retired as the source of truth by ADR 0010 Phase 4c; the `trust.bundle` is the sole verification artifact, and only the sidecar writer performs the evidence classification (`evidenceType`/`method` derivation and manifest reconciliation) the CI trust anchor depends on (ADR 0020). `config-protection.js` blocks direct tool writes to these gate files by design. If the sidecar writer is unavailable, record the exact gap instead of writing the files by hand.
22
22
 
23
+ ### Critique Generations Across Route-Backs
24
+
25
+ Each visit to a verification gate establishes a critique generation. Critiques acquired during that visit must target the current implementation snapshot, and every live reviewer slice in the generation participates in the gate decision. A disputed finding therefore cannot be hidden by adding a different reviewer.
26
+
27
+ When Flow routes back and later re-enters verification, a fresh critique generation is required. Older reviewer slices remain immutable audit history in the trust bundle and evidence manifest, but their prior snapshot hashes are not reinterpreted as reviews of the new implementation. A reviewer handoff must use the public critique writer; it must never require actor impersonation or direct trust-bundle edits.
28
+
23
29
  ## Mutation Testing Runs In A Scratch Copy
24
30
 
25
31
  Mutation-testing tools (Stryker or equivalent) **must** run against a scratch/throwaway copy of the working tree, never the live working tree. They deliberately introduce defects to measure test-suite sensitivity; running them in place risks leaving mutated source, corrupting the checkout, or tripping the gate/anchor on injected failures. Copy the tree to a temporary directory (or a git worktree/clone) and run the mutation tool there; discard it afterward.
@@ -246,6 +246,30 @@ codex_banner_teeth(){
246
246
  else
247
247
  _f "Codex banner CALL_ID: did not record pass/exitCode:0 for the call_id-matched entry: $(cat "$log_callid" 2>/dev/null)"
248
248
  fi
249
+
250
+ # --- PARALLEL command correlation (#644): current Codex exec_command calls
251
+ # store arguments under `cmd`. With no hook call_id, each PostToolUse event
252
+ # must resolve its exact function_call/output pair by normalized command.
253
+ local proj_parallel; proj_parallel="$(mktemp -d)"
254
+ mkdir -p "$proj_parallel/.kontourai/flow-agents/cap-codex-parallel"
255
+ printf '# Repo\n' > "$proj_parallel/AGENTS.md"
256
+ printf '%s' '{"schema_version":"1.0","task_slug":"cap-codex-parallel","status":"in_progress","phase":"verification","updated_at":"2026-06-23T00:00:00Z"}' > "$proj_parallel/.kontourai/flow-agents/cap-codex-parallel/state.json"
257
+ local rollout_parallel="$proj_parallel/rollout-parallel.jsonl"
258
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:00Z","type":"response_item","payload":{"type":"function_call","call_id":"call_pass","name":"exec_command","arguments":"{\"cmd\":\"find seed -type f\"}"}}' > "$rollout_parallel"
259
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:01Z","type":"response_item","payload":{"type":"function_call","call_id":"call_fail","name":"exec_command","arguments":"{\"cmd\":\"cat seed/src/window.js\"}"}}' >> "$rollout_parallel"
260
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:02Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_pass","output":"Process exited with code 0\nOutput:\n..."}}' >> "$rollout_parallel"
261
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:03Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_fail","output":"Process exited with code 1\nOutput:\n..."}}' >> "$rollout_parallel"
262
+ printf '{"hook_event_name":"PostToolUse","tool_name":"Bash","cwd":"%s","tool_input":{"command":"find seed -type f"},"transcript_path":"%s"}' "$proj_parallel" "$rollout_parallel" \
263
+ | env CODEX_HOME="$home" CLAUDE_PROJECT_DIR="$home" bash -c "$capcmd" >/dev/null 2>&1 || true
264
+ printf '{"hook_event_name":"PostToolUse","tool_name":"Bash","cwd":"%s","tool_input":{"command":"cat seed/src/window.js"},"transcript_path":"%s"}' "$proj_parallel" "$rollout_parallel" \
265
+ | env CODEX_HOME="$home" CLAUDE_PROJECT_DIR="$home" bash -c "$capcmd" >/dev/null 2>&1 || true
266
+ local log_parallel="$proj_parallel/.kontourai/flow-agents/cap-codex-parallel/command-log.jsonl"
267
+ if rg -q '"command":"find seed -type f","observedResult":"pass","exitCode":0' "$log_parallel" 2>/dev/null \
268
+ && rg -q '"command":"cat seed/src/window.js","observedResult":"fail","exitCode":1' "$log_parallel" 2>/dev/null; then
269
+ _p "Codex banner PARALLEL: cmd correlation keeps each command attached to its own exit code"
270
+ else
271
+ _f "Codex banner PARALLEL: command/result attribution crossed or stayed unresolved: $(cat "$log_parallel" 2>/dev/null)"
272
+ fi
249
273
  }
250
274
 
251
275
  run_bundle "Claude Code" "$ROOT/dist/claude-code/install.sh" ".claude/settings.json" "CLAUDE_PROJECT_DIR"
@@ -2,13 +2,18 @@
2
2
  set -uo pipefail
3
3
 
4
4
  ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5
- SCRIPT="$ROOT/scripts/effective-backlog-settings.js"
6
- PROJECT="$ROOT/context/settings/backlog-provider-settings.json"
7
- GLOBAL="$ROOT/evals/fixtures/backlog-provider-settings/global-default.json"
8
- OVERRIDE="$ROOT/evals/fixtures/backlog-provider-settings/project-override.json"
9
- EMPTY="$(mktemp)"
10
- trap 'rm -f "$EMPTY"' EXIT
11
- printf '{"schema_version":"1.0","projects":[]}\n' > "$EMPTY"
5
+ CLI="$ROOT/build/src/cli.js"
6
+ BACKLOG_SETTINGS="$ROOT/context/settings/backlog-provider-settings.json"
7
+ ASSIGNMENT_SETTINGS="$ROOT/context/settings/assignment-provider-settings.json"
8
+ BACKLOG_GLOBAL="$ROOT/evals/fixtures/backlog-provider-settings/global-default.json"
9
+ BACKLOG_OVERRIDE="$ROOT/evals/fixtures/backlog-provider-settings/project-override.json"
10
+ TMPDIR_EVAL="$(mktemp -d)"
11
+ EMPTY_BACKLOG="$TMPDIR_EVAL/empty-backlog-settings.json"
12
+ EMPTY_ASSIGNMENT="$TMPDIR_EVAL/empty-assignment-settings.json"
13
+ trap 'rm -rf "$TMPDIR_EVAL"' EXIT
14
+
15
+ printf '{"schema_version":"1.0","projects":[]}\n' > "$EMPTY_BACKLOG"
16
+ printf '{"schema_version":"1.0","projects":[]}\n' > "$EMPTY_ASSIGNMENT"
12
17
 
13
18
  errors=0
14
19
  pass() { echo " ✓ $1"; }
@@ -18,17 +23,32 @@ json_value() {
18
23
  NO_COLOR=1 FORCE_COLOR=0 node -e 'const fs=require("fs"); let cur=JSON.parse(fs.readFileSync(0,"utf8")); for (const part of process.argv[1].split(".")) cur=Array.isArray(cur) ? cur[Number(part)] : cur[part]; console.log(cur);' "$1"
19
24
  }
20
25
 
21
- echo "=== Effective Backlog Settings ==="
26
+ echo "=== Effective provider settings ==="
27
+
28
+ # Build exactly once before calling the actual compiled CLI commands below.
29
+ if (cd "$ROOT" && npm run build --silent); then
30
+ pass "compiled CLI build succeeds"
31
+ else
32
+ fail "compiled CLI build succeeds"
33
+ fi
22
34
 
23
- default_result="$(cd "$ROOT" && npm run --silent effective-backlog-settings -- --repo-path . --json)"
24
- [[ "$(printf '%s' "$default_result" | json_value status)" == "configured" ]] && pass "default project settings resolve through npm command" || fail "default project settings resolve through npm command"
35
+ if [[ ! -f "$CLI" ]]; then
36
+ fail "compiled CLI is available"
37
+ else
38
+ pass "compiled CLI is available"
39
+ fi
40
+
41
+ echo "--- existing backlog precedence and ask-user behavior ---"
42
+
43
+ default_result="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --json 2>/dev/null)"
44
+ [[ "$(printf '%s' "$default_result" | json_value status)" == "configured" ]] && pass "default project settings resolve through compiled CLI" || fail "default project settings resolve through compiled CLI"
25
45
  [[ "$(printf '%s' "$default_result" | json_value settings.work_item_provider.kind)" == "github" ]] && pass "default provider kind is github" || fail "default provider kind is github"
26
46
  [[ "$(printf '%s' "$default_result" | json_value settings.work_item_provider.repo.owner)" == "kontourai" ]] && pass "default repo owner is kontourai" || fail "default repo owner is kontourai"
27
47
  [[ "$(printf '%s' "$default_result" | json_value settings.work_item_provider.repo.name)" == "flow-agents" ]] && pass "default repo name is flow-agents" || fail "default repo name is flow-agents"
28
48
  [[ "$(printf '%s' "$default_result" | json_value settings.board_provider.board.number)" == "1" ]] && pass "default project number is 1" || fail "default project number is 1"
29
49
  ! printf '%s' "$default_result" | rg -q '/build/context/settings/backlog-provider-settings.json' && pass "default settings path is not build-relative" || fail "default settings path is not build-relative"
30
50
 
31
- configured="$(node "$SCRIPT" --repo-path "$ROOT" --project-settings "$PROJECT" --global-settings "$EMPTY" --json)"
51
+ configured="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --project-settings "$BACKLOG_SETTINGS" --global-settings "$EMPTY_BACKLOG" --json)"
32
52
  [[ "$(printf '%s' "$configured" | json_value status)" == "configured" ]] && pass "configured repo resolves" || fail "configured repo resolves"
33
53
  [[ "$(printf '%s' "$configured" | json_value settings.work_item_provider.kind)" == "github" ]] && pass "provider kind is github" || fail "provider kind is github"
34
54
  [[ "$(printf '%s' "$configured" | json_value settings.work_item_provider.repo.owner)" == "kontourai" ]] && pass "repo owner is kontourai" || fail "repo owner is kontourai"
@@ -38,21 +58,104 @@ printf '%s' "$configured" | rg -F -q '"ready_statuses": [' && pass "filters incl
38
58
  printf '%s' "$configured" | rg -F -q '"include_labels": []' && pass "filters do not require labels by default" || fail "filters do not require labels by default"
39
59
  printf '%s' "$configured" | rg -q '"prefer_finishing_active_work": true' && pass "WIP policy prefers finishing active work" || fail "WIP policy prefers finishing active work"
40
60
 
41
- override_result="$(node "$SCRIPT" --repo-path "$ROOT" --project-settings "$OVERRIDE" --global-settings "$GLOBAL" --json)"
61
+ override_result="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --project-settings "$BACKLOG_OVERRIDE" --global-settings "$BACKLOG_GLOBAL" --json)"
42
62
  [[ "$(printf '%s' "$override_result" | json_value source)" == "project" ]] && pass "project settings override global defaults" || fail "project settings override global defaults"
43
63
  [[ "$(printf '%s' "$override_result" | json_value settings.board_provider.board.number)" == "1" ]] && pass "project override keeps Project 1" || fail "project override keeps Project 1"
44
64
  ! printf '%s' "$override_result" | rg -q 'global-ready' && pass "project override replaces global filters" || fail "project override replaces global filters"
45
65
 
46
- missing_output="$(node "$SCRIPT" --repo-path "$ROOT" --project-settings "$EMPTY" --global-settings "$EMPTY" --json 2>/dev/null)"
66
+ missing_output="$(node "$CLI" effective-backlog-settings --repo-path "$ROOT" --project-settings "$EMPTY_BACKLOG" --global-settings "$EMPTY_BACKLOG" --json 2>/dev/null)"
47
67
  missing_status=$?
48
68
  [[ "$missing_status" -eq 2 ]] && pass "missing settings exits with ask-user status" || fail "missing settings exits with ask-user status"
49
69
  [[ "$(printf '%s' "$missing_output" | json_value status)" == "ask_user" ]] && pass "missing settings reports ask_user" || fail "missing settings reports ask_user"
50
70
  printf '%s' "$missing_output" | rg -q 'WorkItemProvider and BoardProvider' && pass "missing settings names provider roles" || fail "missing settings names provider roles"
51
71
 
72
+ echo "--- public self-configuration and external multi-repository settings ---"
73
+
74
+ # Product-specific fleet routing belongs in consumer-owned settings. The public package proves
75
+ # that an external settings file can configure multiple repositories without embedding those
76
+ # consumer mappings in Flow Agents source.
77
+ if node --input-type=module - "$ROOT" "$CLI" "$TMPDIR_EVAL/repos" "$BACKLOG_SETTINGS" "$ASSIGNMENT_SETTINGS" "$EMPTY_BACKLOG" "$EMPTY_ASSIGNMENT" <<'NODE'
78
+ import { execFileSync } from "node:child_process";
79
+ import fs from "node:fs";
80
+ import path from "node:path";
81
+ import Ajv2020 from "ajv/dist/2020.js";
82
+
83
+ const [root, cli, reposRoot, publicBacklogPath, publicAssignmentPath, emptyBacklog, emptyAssignment] = process.argv.slice(2);
84
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
85
+ const assert = (condition, message) => {
86
+ if (!condition) throw new Error(message);
87
+ };
88
+
89
+ const backlogSchema = readJson(path.join(root, "schemas/backlog-provider-settings.schema.json"));
90
+ const assignmentSchema = readJson(path.join(root, "schemas/assignment-provider-settings.schema.json"));
91
+ const ajv = new Ajv2020({ allErrors: true });
92
+ const validateBacklog = ajv.compile(backlogSchema);
93
+ const validateAssignment = ajv.compile(assignmentSchema);
94
+
95
+ const publicBacklog = readJson(publicBacklogPath);
96
+ const publicAssignment = readJson(publicAssignmentPath);
97
+ assert(validateBacklog(publicBacklog), `public backlog settings schema validation failed: ${ajv.errorsText(validateBacklog.errors)}`);
98
+ assert(validateAssignment(publicAssignment), `public assignment settings schema validation failed: ${ajv.errorsText(validateAssignment.errors)}`);
99
+ assert(publicBacklog.projects.length === 1 && publicBacklog.projects[0]?.project?.repo?.name === "flow-agents", "public backlog settings must remain self-only");
100
+ assert(publicAssignment.projects.length === 1 && publicAssignment.projects[0]?.project?.repo?.name === "flow-agents", "public assignment settings must remain self-only");
101
+
102
+ const owner = "example-org";
103
+ const targets = ["alpha", "beta"];
104
+ const projectNumber = 42;
105
+ const repoRef = (name) => ({ owner, name, url: `https://github.com/${owner}/${name}` });
106
+ const externalBacklog = {
107
+ schema_version: "1.0",
108
+ projects: targets.map((name) => ({
109
+ project: { repo: repoRef(name) },
110
+ work_item_provider: { role: "WorkItemProvider", kind: "github", repo: repoRef(name), capabilities: ["issues", "labels", "assignees", "pr_links", "comments"] },
111
+ board_provider: { role: "BoardProvider", kind: "github", repo: repoRef(name), board: { type: "github_project", owner, number: projectNumber, url: `https://github.com/orgs/${owner}/projects/${projectNumber}` }, capabilities: ["projects_boards", "status_fields", "custom_fields"] },
112
+ selection: { filters: { issue_state: "open", include_labels: [], ready_statuses: ["ready"], exclude_statuses: ["triage", "in_progress", "blocked", "review", "verification", "done"] }, wip_policy: { prefer_finishing_active_work: true, active_statuses: ["in_progress", "review", "verification"], block_new_work_when_active_count_exceeds: 0 } },
113
+ })),
114
+ };
115
+ const externalAssignment = {
116
+ schema_version: "1.0",
117
+ projects: targets.map((name) => ({
118
+ project: { repo: repoRef(name) },
119
+ provider: { kind: "github", repo: repoRef(name), capabilities: ["assignees", "labels", "comments"] },
120
+ policy: { label_name: "agent:claimed", claim_comment_marker: "<!-- flow-agents:assignment-claim -->", human_assignee_policy: { behavior: "ask_first", idle_threshold_days: 3 }, comment_refresh_on_phase_transition: false },
121
+ })),
122
+ };
123
+ assert(validateBacklog(externalBacklog), `external backlog settings schema validation failed: ${ajv.errorsText(validateBacklog.errors)}`);
124
+ assert(validateAssignment(externalAssignment), `external assignment settings schema validation failed: ${ajv.errorsText(validateAssignment.errors)}`);
125
+
126
+ fs.mkdirSync(reposRoot, { recursive: true });
127
+ const externalBacklogPath = path.join(reposRoot, "backlog-provider-settings.json");
128
+ const externalAssignmentPath = path.join(reposRoot, "assignment-provider-settings.json");
129
+ fs.writeFileSync(externalBacklogPath, JSON.stringify(externalBacklog, null, 2));
130
+ fs.writeFileSync(externalAssignmentPath, JSON.stringify(externalAssignment, null, 2));
131
+
132
+ for (const name of targets) {
133
+ const repoPath = path.join(reposRoot, name);
134
+ fs.mkdirSync(repoPath, { recursive: true });
135
+ fs.writeFileSync(path.join(repoPath, "package.json"), JSON.stringify({ name: `fixture-${name}`, repository: { type: "git", url: `https://github.com/${owner}/${name}.git` } }, null, 2));
136
+ const resolve = (command, globalPath, emptyProject) => JSON.parse(execFileSync(process.execPath, [cli, command, "--repo-path", repoPath, "--project-settings", emptyProject, "--global-settings", globalPath, "--json"], { encoding: "utf8" }));
137
+ const backlogResult = resolve("effective-backlog-settings", externalBacklogPath, emptyBacklog);
138
+ const assignmentResult = resolve("effective-assignment-provider-settings", externalAssignmentPath, emptyAssignment);
139
+
140
+ assert(backlogResult.status === "configured" && backlogResult.source === "global", `${name} backlog settings must resolve from the external file`);
141
+ assert(backlogResult.settings.work_item_provider?.repo?.name === name, `${name} must select its own work item repository`);
142
+ assert(backlogResult.settings.board_provider?.board?.number === projectNumber, `${name} must select the external board`);
143
+ assert(assignmentResult.status === "configured" && assignmentResult.source === "global", `${name} assignment settings must resolve from the external file`);
144
+ assert(assignmentResult.settings.provider?.repo?.name === name, `${name} must select its own assignment repository`);
145
+ }
146
+
147
+ console.log("Public settings remain self-only and external multi-repository settings resolve through the supported global precedence layer.");
148
+ NODE
149
+ then
150
+ pass "public settings stay self-only and external multi-repository settings resolve"
151
+ else
152
+ fail "public settings stay self-only and external multi-repository settings resolve"
153
+ fi
154
+
52
155
  if [[ "$errors" -eq 0 ]]; then
53
- echo "Effective backlog settings checks passed"
156
+ echo "Effective provider settings checks passed"
54
157
  else
55
- echo "Effective backlog settings checks failed: $errors"
158
+ echo "Effective provider settings checks failed: $errors"
56
159
  fi
57
160
 
58
161
  exit "$errors"
@@ -72,7 +72,7 @@ if [[ ! -e "$DEST/AGENTS.md" && ! -L "$DEST/AGENTS.md" ]]; then
72
72
  rm -f "$instruction_tmp"
73
73
  fi
74
74
  if command -v node >/dev/null 2>&1; then
75
- node "$DEST/scripts/install-merge.js" --stamp-only --version "4.2.0" --install-record "$DEST/.flow-agents/install.json" --runtime "base" || true
75
+ node "$DEST/scripts/install-merge.js" --stamp-only --version "4.2.1" --install-record "$DEST/.flow-agents/install.json" --runtime "base" || true
76
76
  fi
77
77
  if [[ ${#CONSOLE_CONFIG_ARGS[@]} -gt 0 || -n "${FLOW_AGENTS_TELEMETRY_SINK:-}" || -n "${FLOW_AGENTS_TELEMETRY_SINKS:-}" || -n "${FLOW_AGENTS_CONSOLE_URL:-}" || -n "${CONSOLE_TELEMETRY_URL:-}" || -n "${CONSOLE_URL:-}" || -n "${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-}" || -n "${CONSOLE_TELEMETRY_TOKEN_FILE:-}" ]]; then
78
78
  bash "$DEST/scripts/telemetry/install-console-config.sh" "$DEST/scripts/telemetry/telemetry.conf" "${CONSOLE_CONFIG_ARGS[@]}"
@@ -113,10 +113,13 @@ function normalizeCallArguments(argumentsField) {
113
113
  }
114
114
  }
115
115
  if (!parsed || typeof parsed !== 'object') return null;
116
- return normalizeCommandText(parsed.command);
116
+ // Codex records exec_command arguments under `cmd`; older shell fixtures
117
+ // and some adapters use `command`.
118
+ return normalizeCommandText(parsed.cmd) || normalizeCommandText(parsed.command);
117
119
  }
118
120
 
119
121
  const OUTPUT_FIELD_NEEDLE = '"output":"';
122
+ const COMMAND_FUNCTION_NAMES = new Set(['exec_command', 'shell']);
120
123
 
121
124
  /**
122
125
  * parseCandidateLine(line, maxLineHeadBytes) → candidate | null
@@ -149,7 +152,15 @@ function parseCandidateLine(line, maxLineHeadBytes) {
149
152
  return { type: 'function_call_output', callId, output: payload.output };
150
153
  }
151
154
  if (payload.type === 'function_call') {
152
- return { type: 'function_call', callId, command: normalizeCallArguments(payload.arguments) };
155
+ const name = typeof payload.name === 'string' && payload.name.trim() ? payload.name.trim() : null;
156
+ const command = normalizeCallArguments(payload.arguments);
157
+ const commandCapable = name === null || COMMAND_FUNCTION_NAMES.has(name) || command !== null;
158
+ return {
159
+ type: 'function_call',
160
+ callId,
161
+ commandCapable,
162
+ command,
163
+ };
153
164
  }
154
165
  return null;
155
166
  }
@@ -214,13 +225,12 @@ function resolveContainedRealPath(transcriptPath) {
214
225
  * Correlation policy (Decision B, #470 iteration 2, HIGH finding #4), in
215
226
  * priority order:
216
227
  * 1. call_id match wins — authoritative.
217
- * 2. Absent a call_id match: take the newest `function_call_output` in the
218
- * scanned window, resolve its paired `function_call` (same call_id), and
219
- * compare that call's `arguments` command (normalized) to `command`. A
220
- * resolved MATCH uses the newest output; a resolved MISMATCH DECLINES
221
- * (`null`) rather than knowingly attribute a different call's exit code.
222
- * 3. If no pairing is resolvable at all (the common single-call case): fall
223
- * back to the newest `function_call_output` banner.
228
+ * 2. Absent a call_id match: pair outputs with calls by rollout call_id and
229
+ * select the only pair whose normalized arguments match `command`. Zero
230
+ * matches or multiple matches DECLINE (`null`) rather than attribute a
231
+ * neighboring or repeated command's exit code.
232
+ * 3. If no pairing is resolvable at all and the rollout contains exactly one
233
+ * output with no function call, use that genuinely unpaired legacy output.
224
234
  *
225
235
  * Any failure (missing/unreadable/non-regular file, containment violation,
226
236
  * malformed JSON lines, no candidate found) yields `null` — never throws.
@@ -271,41 +281,83 @@ function readExitCodeFromRollout(transcriptPath, options) {
271
281
  }
272
282
  if (truncated && lines.length > 1) lines.shift();
273
283
 
274
- let matchedByCallId = null;
284
+ const outputsByRequestedCallId = [];
275
285
  let newestOutputEntry = null;
276
- let pairedCommand = null;
286
+ const outputEntries = [];
287
+ const commandByCallId = new Map();
288
+ const seenFunctionCallIds = new Set();
289
+ const ambiguousCallIds = new Set();
290
+ let functionCallCount = 0;
291
+ let sawUnclassifiableLine = false;
292
+ let sawUnresolvableFunctionCall = false;
277
293
 
278
294
  for (let i = lines.length - 1; i >= 0; i--) {
279
295
  const line = lines[i].trim();
280
296
  if (!line) continue;
281
297
  const candidate = parseCandidateLine(line, maxLineHeadBytes);
282
- if (!candidate) continue; // malformed/partial/unrecognized line — skip, keep scanning
298
+ if (!candidate) {
299
+ if (Buffer.byteLength(line, 'utf8') > maxLineHeadBytes) {
300
+ sawUnclassifiableLine = true;
301
+ } else {
302
+ try {
303
+ JSON.parse(line); // valid non-candidate events do not poison correlation
304
+ } catch {
305
+ sawUnclassifiableLine = true;
306
+ }
307
+ }
308
+ continue; // malformed/partial/unrecognized line — skip, keep scanning
309
+ }
283
310
 
284
311
  if (candidate.type === 'function_call_output') {
312
+ outputEntries.push(candidate); // scan order is newest to oldest
285
313
  if (newestOutputEntry === null) newestOutputEntry = candidate; // first seen scanning backward = newest
286
314
  if (callId && candidate.callId === callId) {
287
- matchedByCallId = candidate;
288
- break; // call_id match is authoritative — stop scanning
315
+ outputsByRequestedCallId.push(candidate);
289
316
  }
290
317
  } else if (candidate.type === 'function_call') {
291
- if (newestOutputEntry && pairedCommand === null && candidate.callId === newestOutputEntry.callId) {
292
- pairedCommand = candidate.command;
318
+ functionCallCount += 1;
319
+ if (candidate.callId) {
320
+ if (seenFunctionCallIds.has(candidate.callId)) ambiguousCallIds.add(candidate.callId);
321
+ else seenFunctionCallIds.add(candidate.callId);
322
+ }
323
+ if (candidate.commandCapable && candidate.callId && candidate.command !== null) {
324
+ if (!commandByCallId.has(candidate.callId)) commandByCallId.set(candidate.callId, candidate.command);
325
+ } else if (candidate.commandCapable) {
326
+ sawUnresolvableFunctionCall = true;
293
327
  }
294
328
  }
295
329
  }
296
330
 
297
331
  let chosenOutput = null;
298
- if (matchedByCallId) {
299
- chosenOutput = matchedByCallId.output;
332
+ if ((callId || command) && sawUnclassifiableLine) return null;
333
+ if (callId) {
334
+ if (outputsByRequestedCallId.length === 1 && !ambiguousCallIds.has(callId)) {
335
+ chosenOutput = outputsByRequestedCallId[0].output;
336
+ } else {
337
+ return null; // explicit call ID is unresolved, reused, or has duplicate outputs
338
+ }
300
339
  } else if (newestOutputEntry) {
301
- if (pairedCommand !== null && command) {
302
- if (pairedCommand === command) {
303
- chosenOutput = newestOutputEntry.output;
340
+ if (command) {
341
+ if (sawUnresolvableFunctionCall) return null;
342
+ const matchingCallIds = [...commandByCallId.entries()]
343
+ .filter(([, candidateCommand]) => candidateCommand === command)
344
+ .map(([candidateCallId]) => candidateCallId);
345
+ const matches = outputEntries.filter((entry) => commandByCallId.get(entry.callId) === command);
346
+ if (matchingCallIds.length === 1
347
+ && !ambiguousCallIds.has(matchingCallIds[0])
348
+ && matches.length === 1) {
349
+ chosenOutput = matches[0].output;
350
+ } else if (matchingCallIds.length > 0 || matches.length > 0 || commandByCallId.size > 0) {
351
+ return null; // no unique command correlation
352
+ } else if (!truncated && outputEntries.length === 1 && functionCallCount === 0) {
353
+ chosenOutput = newestOutputEntry.output; // genuinely unpaired legacy fallback
304
354
  } else {
305
- return null; // positive command mismatch decline rather than mis-attribute
355
+ return null; // multi-call or partially paired tail
306
356
  }
357
+ } else if (!truncated && outputEntries.length === 1 && functionCallCount === 0) {
358
+ chosenOutput = newestOutputEntry.output; // genuinely unpaired legacy fallback
307
359
  } else {
308
- chosenOutput = newestOutputEntry.output; // single-call fallback no pairing resolvable
360
+ return null; // no correlation signal for a paired or multi-call tail
309
361
  }
310
362
  }
311
363
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "type": "module"
5
5
  }
@@ -572,7 +572,7 @@ async function bundleGateEvidence(bundle, gate, state, subject, projectRoot, man
572
572
  throw new BuilderBuildRunInputError("evidence.claims.metadata.gate_claim.route_reason", `is not declared by gate ${String(gate.id ?? "<unknown>")}`);
573
573
  }
574
574
  if (String(gate.id) === "verify-gate" && relevant.some((claim) => claim.claimType === "builder.verify.tests" && claim.value === "pass")) {
575
- await assertVerifiedTestsTrust(bundle.claims, projectRoot);
575
+ await assertVerifiedTestsTrust(relevant, projectRoot);
576
576
  }
577
577
  return { failed, routeReason, expectationIds, visitEnteredAt: enteredAt };
578
578
  }
@@ -602,8 +602,8 @@ function timestampAtOrAfter(value, boundary) {
602
602
  const parsed = parseTimestamp(value);
603
603
  return parsed !== null && parsed >= boundary;
604
604
  }
605
- async function assertVerifiedTestsTrust(claims, projectRoot) {
606
- const testClaims = claims.filter((claim) => isRecord(claim)
605
+ async function assertVerifiedTestsTrust(currentGateClaims, projectRoot) {
606
+ const testClaims = currentGateClaims.filter((claim) => isRecord(claim)
607
607
  && claim.claimType === "builder.verify.tests"
608
608
  && claim.value === "pass"
609
609
  && isRecord(claim.metadata)
@@ -611,7 +611,12 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
611
611
  && claim.metadata.gate_claim.expectation_id === "tests-evidence");
612
612
  if (testClaims.length === 0)
613
613
  throw new BuilderBuildRunInputError("evidence.tests", "is missing a passing tests-evidence claim");
614
- const liveCritiques = claims.filter((claim) => isRecord(claim)
614
+ // A route-back starts a new gate visit and therefore a new critique generation. Historical
615
+ // reviewer slices remain in the bundle and manifest for audit, but only critiques acquired
616
+ // during this visit describe the implementation snapshot currently being verified. Within a
617
+ // visit every live reviewer slice still participates, so changing reviewers cannot bury a
618
+ // disputed finding.
619
+ const liveCritiques = currentGateClaims.filter((claim) => isRecord(claim)
615
620
  && isRecord(claim.metadata)
616
621
  && claim.metadata.origin === "critique"
617
622
  && typeof claim.metadata.superseded_by !== "string");
@@ -627,7 +632,7 @@ async function assertVerifiedTestsTrust(claims, projectRoot) {
627
632
  await Promise.all(artifacts.map((artifact) => assertReviewedArtifactDigest(artifact, projectRoot)));
628
633
  assertReviewedWorkspaceSnapshot(claim, artifacts, projectRoot);
629
634
  }));
630
- const criteria = claims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
635
+ const criteria = currentGateClaims.filter((claim) => isRecord(claim) && isRecord(claim.metadata) && claim.metadata.origin === "acceptance");
631
636
  if (criteria.length === 0 || criteria.some((claim) => {
632
637
  const criterion = isRecord(claim.metadata.criterion) ? claim.metadata.criterion : null;
633
638
  return claim.value !== "pass" || !criterion || !Array.isArray(criterion.evidence_refs) || criterion.evidence_refs.length === 0;
@@ -20,6 +20,12 @@ Verifiers and reviewers do not modify source code. They may run commands, inspec
20
20
 
21
21
  Verifiers write evidence and acceptance status **through** `record-evidence` (directly, or via the orchestrating skill), never by hand-authoring `evidence.json`, `acceptance.json`, or `trust.bundle`. Those bespoke sidecars were retired as the source of truth by ADR 0010 Phase 4c; the `trust.bundle` is the sole verification artifact, and only the sidecar writer performs the evidence classification (`evidenceType`/`method` derivation and manifest reconciliation) the CI trust anchor depends on (ADR 0020). `config-protection.js` blocks direct tool writes to these gate files by design. If the sidecar writer is unavailable, record the exact gap instead of writing the files by hand.
22
22
 
23
+ ### Critique Generations Across Route-Backs
24
+
25
+ Each visit to a verification gate establishes a critique generation. Critiques acquired during that visit must target the current implementation snapshot, and every live reviewer slice in the generation participates in the gate decision. A disputed finding therefore cannot be hidden by adding a different reviewer.
26
+
27
+ When Flow routes back and later re-enters verification, a fresh critique generation is required. Older reviewer slices remain immutable audit history in the trust bundle and evidence manifest, but their prior snapshot hashes are not reinterpreted as reviews of the new implementation. A reviewer handoff must use the public critique writer; it must never require actor impersonation or direct trust-bundle edits.
28
+
23
29
  ## Mutation Testing Runs In A Scratch Copy
24
30
 
25
31
  Mutation-testing tools (Stryker or equivalent) **must** run against a scratch/throwaway copy of the working tree, never the live working tree. They deliberately introduce defects to measure test-suite sensitivity; running them in place risks leaving mutated source, corrupting the checkout, or tripping the gate/anchor on injected failures. Copy the tree to a temporary directory (or a git worktree/clone) and run the mutation tool there; discard it afterward.
@@ -246,6 +246,30 @@ codex_banner_teeth(){
246
246
  else
247
247
  _f "Codex banner CALL_ID: did not record pass/exitCode:0 for the call_id-matched entry: $(cat "$log_callid" 2>/dev/null)"
248
248
  fi
249
+
250
+ # --- PARALLEL command correlation (#644): current Codex exec_command calls
251
+ # store arguments under `cmd`. With no hook call_id, each PostToolUse event
252
+ # must resolve its exact function_call/output pair by normalized command.
253
+ local proj_parallel; proj_parallel="$(mktemp -d)"
254
+ mkdir -p "$proj_parallel/.kontourai/flow-agents/cap-codex-parallel"
255
+ printf '# Repo\n' > "$proj_parallel/AGENTS.md"
256
+ printf '%s' '{"schema_version":"1.0","task_slug":"cap-codex-parallel","status":"in_progress","phase":"verification","updated_at":"2026-06-23T00:00:00Z"}' > "$proj_parallel/.kontourai/flow-agents/cap-codex-parallel/state.json"
257
+ local rollout_parallel="$proj_parallel/rollout-parallel.jsonl"
258
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:00Z","type":"response_item","payload":{"type":"function_call","call_id":"call_pass","name":"exec_command","arguments":"{\"cmd\":\"find seed -type f\"}"}}' > "$rollout_parallel"
259
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:01Z","type":"response_item","payload":{"type":"function_call","call_id":"call_fail","name":"exec_command","arguments":"{\"cmd\":\"cat seed/src/window.js\"}"}}' >> "$rollout_parallel"
260
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:02Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_pass","output":"Process exited with code 0\nOutput:\n..."}}' >> "$rollout_parallel"
261
+ printf '%s\n' '{"timestamp":"2026-06-23T00:00:03Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_fail","output":"Process exited with code 1\nOutput:\n..."}}' >> "$rollout_parallel"
262
+ printf '{"hook_event_name":"PostToolUse","tool_name":"Bash","cwd":"%s","tool_input":{"command":"find seed -type f"},"transcript_path":"%s"}' "$proj_parallel" "$rollout_parallel" \
263
+ | env CODEX_HOME="$home" CLAUDE_PROJECT_DIR="$home" bash -c "$capcmd" >/dev/null 2>&1 || true
264
+ printf '{"hook_event_name":"PostToolUse","tool_name":"Bash","cwd":"%s","tool_input":{"command":"cat seed/src/window.js"},"transcript_path":"%s"}' "$proj_parallel" "$rollout_parallel" \
265
+ | env CODEX_HOME="$home" CLAUDE_PROJECT_DIR="$home" bash -c "$capcmd" >/dev/null 2>&1 || true
266
+ local log_parallel="$proj_parallel/.kontourai/flow-agents/cap-codex-parallel/command-log.jsonl"
267
+ if rg -q '"command":"find seed -type f","observedResult":"pass","exitCode":0' "$log_parallel" 2>/dev/null \
268
+ && rg -q '"command":"cat seed/src/window.js","observedResult":"fail","exitCode":1' "$log_parallel" 2>/dev/null; then
269
+ _p "Codex banner PARALLEL: cmd correlation keeps each command attached to its own exit code"
270
+ else
271
+ _f "Codex banner PARALLEL: command/result attribution crossed or stayed unresolved: $(cat "$log_parallel" 2>/dev/null)"
272
+ fi
249
273
  }
250
274
 
251
275
  run_bundle "Claude Code" "$ROOT/dist/claude-code/install.sh" ".claude/settings.json" "CLAUDE_PROJECT_DIR"