@kontourai/flow-agents 3.3.0 → 3.4.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 (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +48 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +56 -3
  34. package/build/src/lib/flow-resolver.js +151 -11
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +101 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +153 -10
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -0,0 +1,575 @@
1
+ #!/usr/bin/env bash
2
+ # test_liveness_worktree_root.sh -- integration eval for #357: the shared, git-common-dir-aware
3
+ # artifact-root resolver (flowAgentsArtifactRoot / defaultArtifactRootForRead, under
4
+ # src/lib/ in the main tree, and the CJS hook twin under scripts/hooks/lib/ -- see
5
+ # local-artifact-root.ts and local-artifact-paths.js).
6
+ #
7
+ # Uses a REAL `git worktree add` linked worktree (not a simulated divergent path -- the plan's
8
+ # own stop-short-risk note explicitly requires this, since `git rev-parse --git-common-dir`
9
+ # behaves identically to `--absolute-git-dir` outside a real linked worktree, so a simulated
10
+ # two-unrelated-repos setup would never actually exercise the git-common-dir resolution code
11
+ # path at all).
12
+ #
13
+ # Covers:
14
+ # AC3 liveness-claim-from-worktree-shared-store: `liveness claim` invoked with cwd inside the
15
+ # linked worktree (no --artifact-root) writes into the SAME liveness stream a
16
+ # `liveness status` invocation from the PRIMARY checkout sees.
17
+ # AC4 ensure-session-ownership-guard-shared-store: a claim held (via ensure-session) in the
18
+ # PRIMARY checkout is recognized by `ensure-session`'s ownership guard when invoked for the
19
+ # SAME subject from the linked worktree's cwd -- the guard must refuse (see the existing hold),
20
+ # never silently proceed as if the subject were free.
21
+ # AC5 current-pointer-shared-store-from-worktree: `ensure-session`'s current.json write (from
22
+ # the worktree cwd, no --artifact-root) is visible to `workflow-sidecar current` invoked from
23
+ # the PRIMARY checkout cwd (both resolve the SAME shared .kontourai/flow-agents root).
24
+ # AC6 single-checkout-backward-compat (this eval's own contribution): byte-identical to
25
+ # before this change ONLY for the two cases where the pre-#357 code and the new
26
+ # git-common-dir resolver necessarily agree: (a) cwd is the repo ROOT of a plain
27
+ # (non-worktree) checkout, and (b) cwd is not inside a git repo at all (fail-open). Both are
28
+ # asserted below with a companion case proving byte-identical path.resolve(cwd,
29
+ # ".kontourai/flow-agents") output.
30
+ # AC6b subdirectory-resolves-to-shared-repo-root (#413 iteration-2 Fix 2, INTENDED CHANGE, not
31
+ # byte-identical): when cwd is a SUBDIRECTORY of a plain (non-worktree) checkout, the OLD
32
+ # code anchored the store to that subdir (path.resolve(cwd, ...)), but `git rev-parse
33
+ # --git-common-dir` from a subdirectory returns a RELATIVE path this resolver walks up to the
34
+ # repo root, so flowAgentsArtifactRoot now correctly resolves to
35
+ # <repo-root>/.kontourai/flow-agents regardless of which subdirectory cwd is -- ONE shared
36
+ # store per repo, matching every other cwd (primary checkout, linked worktree) this resolver
37
+ # already unifies. This is intentional, correct shared-store behavior, NOT a regression to
38
+ # revert -- asserted explicitly below so it stays a documented, tested behavior change
39
+ # rather than an unverified side effect of the git-common-dir walk.
40
+ #
41
+ # Deterministic, no model spend, self-cleaning (mktemp -d + trap EXIT). The worktree fixture is
42
+ # entirely disposable, created and torn down inside this eval's own scratch directory -- it never
43
+ # touches this session's real worktree (see the plan's Sandbox mode note).
44
+ #
45
+ # Usage: bash evals/integration/test_liveness_worktree_root.sh
46
+
47
+ set -uo pipefail
48
+
49
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
50
+ source "$ROOT/evals/lib/node.sh"
51
+
52
+ WRITER="workflow-sidecar"
53
+ TMP="$(mktemp -d)"
54
+ errors=0
55
+
56
+ _pass() { echo " PASS: $1"; }
57
+ _fail() { echo " FAIL: $1"; errors=$((errors + 1)); }
58
+
59
+ cleanup() { rm -rf "$TMP"; }
60
+ trap cleanup EXIT
61
+
62
+ if ! command -v git >/dev/null 2>&1; then
63
+ echo "git is not installed -- cannot exercise a real 'git worktree add' fixture." >&2
64
+ echo "ACCEPTED GAP: this eval requires git; recording as an explicit, non-silent skip (not a pass)." >&2
65
+ exit 1
66
+ fi
67
+
68
+ flow_agents_build_ts || { echo "build failed" >&2; exit 1; }
69
+
70
+ # ==== SETUP: a real primary checkout + a real linked worktree ======================
71
+ echo ""
72
+ echo "=== SETUP: primary checkout + real 'git worktree add' linked worktree ==="
73
+
74
+ PRIMARY="$TMP/primary"
75
+ mkdir -p "$PRIMARY/kits"
76
+ (
77
+ cd "$PRIMARY" \
78
+ && git init -q \
79
+ && git config user.email "test@test.local" \
80
+ && git config user.name "test" \
81
+ && printf '# marker\n' > kits/.keep \
82
+ && git add -A \
83
+ && git commit -q -m "init"
84
+ )
85
+ if [[ -d "$PRIMARY/.git" ]]; then
86
+ _pass "SETUP: primary checkout is a real git repo with a kits/ marker"
87
+ else
88
+ _fail "SETUP: primary checkout git init failed"
89
+ fi
90
+
91
+ WORKTREE="$TMP/linked-worktree"
92
+ if (cd "$PRIMARY" && git worktree add -q -b worktree-branch "$WORKTREE" >/dev/null 2>"$TMP/worktree-add.err"); then
93
+ _pass "SETUP: 'git worktree add' created a genuine linked worktree"
94
+ else
95
+ _fail "SETUP: 'git worktree add' failed: $(cat "$TMP/worktree-add.err")"
96
+ fi
97
+
98
+ # A linked worktree's OWN .git is a FILE (a gitdir: pointer), never a directory -- this is the
99
+ # exact condition the pre-fix cwd/`.git`-ancestor-walk resolvers could not see past.
100
+ if [[ -f "$WORKTREE/.git" && ! -d "$WORKTREE/.git" ]]; then
101
+ _pass "SETUP: linked worktree's own .git is a FILE, not a directory (the condition this fix targets)"
102
+ else
103
+ _fail "SETUP: linked worktree's .git was not the expected gitdir-pointer FILE"
104
+ fi
105
+
106
+ # Confirm the git primitive itself resolves the worktree to the PRIMARY checkout's real .git dir
107
+ # -- this is the exact resolution flowAgentsArtifactRoot()/defaultArtifactRootForRead() now use.
108
+ COMMON_DIR="$(cd "$WORKTREE" && git rev-parse --git-common-dir 2>/dev/null)"
109
+ if [[ -n "$COMMON_DIR" ]]; then
110
+ ABS_COMMON_DIR="$(cd "$WORKTREE" && node -e 'console.log(require("path").resolve(process.argv[1], process.argv[2]))' "$WORKTREE" "$COMMON_DIR")"
111
+ RESOLVED_SHARED_ROOT="$(dirname "$ABS_COMMON_DIR")"
112
+ REAL_PRIMARY="$(cd "$PRIMARY" && pwd -P)"
113
+ REAL_RESOLVED="$(cd "$RESOLVED_SHARED_ROOT" && pwd -P)"
114
+ if [[ "$REAL_RESOLVED" == "$REAL_PRIMARY" ]]; then
115
+ _pass "SETUP: 'git rev-parse --git-common-dir' from the worktree resolves to the PRIMARY checkout's repo root"
116
+ else
117
+ _fail "SETUP: git-common-dir resolution did not land on the primary checkout: got $REAL_RESOLVED, expected $REAL_PRIMARY"
118
+ fi
119
+ else
120
+ _fail "SETUP: 'git rev-parse --git-common-dir' failed from the linked worktree"
121
+ fi
122
+
123
+ # ==== AC3: liveness claim from worktree cwd is visible from the primary checkout ====
124
+ echo ""
125
+ echo "=== AC3: liveness claim (worktree cwd, no --artifact-root) visible from primary checkout ==="
126
+
127
+ SUBJECT="worktree-liveness-subject"
128
+ if (cd "$WORKTREE" && flow_agents_node "$WRITER" liveness claim "$SUBJECT" --actor worktree-actor \
129
+ >"$TMP/claim.out" 2>"$TMP/claim.err"); then
130
+ _pass "AC3: liveness claim from the worktree cwd (no --artifact-root) exits 0"
131
+ else
132
+ _fail "AC3: liveness claim from the worktree cwd failed: $(cat "$TMP/claim.out" "$TMP/claim.err")"
133
+ fi
134
+
135
+ # The claim must land in the PRIMARY checkout's .kontourai/flow-agents/liveness store, NOT an
136
+ # isolated .kontourai/flow-agents under the worktree's own directory.
137
+ if [[ -f "$PRIMARY/.kontourai/flow-agents/liveness/events.jsonl" ]]; then
138
+ _pass "AC3: the claim landed in the PRIMARY checkout's shared .kontourai/flow-agents/liveness store"
139
+ else
140
+ _fail "AC3: no liveness/events.jsonl found under the primary checkout's .kontourai/flow-agents"
141
+ fi
142
+ if [[ ! -e "$WORKTREE/.kontourai/flow-agents/liveness/events.jsonl" ]]; then
143
+ _pass "AC3: no ISOLATED per-worktree .kontourai/flow-agents/liveness store was created under the worktree"
144
+ else
145
+ _fail "AC3: an isolated per-worktree liveness store was created -- the shared-root fix did not take effect"
146
+ fi
147
+
148
+ if (cd "$PRIMARY" && flow_agents_node "$WRITER" liveness status --subject "$SUBJECT" --json \
149
+ >"$TMP/status.out" 2>"$TMP/status.err"); then
150
+ _pass "AC3: liveness status from the PRIMARY checkout cwd (no --artifact-root) exits 0"
151
+ else
152
+ _fail "AC3: liveness status from the primary checkout failed: $(cat "$TMP/status.out" "$TMP/status.err")"
153
+ fi
154
+ if grep -qF "worktree-actor" "$TMP/status.out"; then
155
+ _pass "AC3: a reader in the PRIMARY checkout sees the claim made from the linked worktree's cwd"
156
+ else
157
+ _fail "AC3: the primary checkout's liveness status did not see the worktree-made claim: $(cat "$TMP/status.out")"
158
+ fi
159
+
160
+ # ==== AC4: ensure-session's ownership guard recognizes a primary-checkout hold from worktree cwd
161
+ echo ""
162
+ echo "=== AC4: ensure-session ownership guard sees a primary-checkout hold from worktree cwd ==="
163
+
164
+ GUARD_WORK_ITEM="kontourai/flow-agents#8801"
165
+ GUARD_SLUG="kontourai-flow-agents-8801"
166
+
167
+ if (cd "$PRIMARY" && flow_agents_node "$WRITER" ensure-session \
168
+ --work-item "$GUARD_WORK_ITEM" \
169
+ --actor primary-holder \
170
+ --source-request "Primary-checkout actor claims a free subject." \
171
+ --summary "Establish the hold in the primary checkout." \
172
+ >"$TMP/guard-primary.out" 2>"$TMP/guard-primary.err"); then
173
+ _pass "AC4 setup: ensure-session from the primary checkout establishes a durable claim"
174
+ else
175
+ _fail "AC4 setup: ensure-session from the primary checkout failed: $(cat "$TMP/guard-primary.out" "$TMP/guard-primary.err")"
176
+ fi
177
+
178
+ # A fresh liveness claim event for the SAME actor/subject, so the join classifies this `held`
179
+ # (self_is_holder / fresh_liveness_heartbeat) rather than merely `reclaimable` -- matches
180
+ # test_ensure_session_ownership_guard.sh's own convention of backing the assignment record with
181
+ # a real liveness event before asserting a second actor's refusal.
182
+ (cd "$PRIMARY" && flow_agents_node "$WRITER" liveness claim "$GUARD_SLUG" --actor primary-holder \
183
+ >"$TMP/guard-liveness-claim.out" 2>"$TMP/guard-liveness-claim.err") || true
184
+
185
+ if (cd "$WORKTREE" && flow_agents_node "$WRITER" ensure-session \
186
+ --work-item "$GUARD_WORK_ITEM" \
187
+ --actor worktree-other-actor \
188
+ --source-request "A different actor attempts entry from the linked worktree's cwd." \
189
+ --summary "Should be refused -- the primary checkout's hold must be visible here too." \
190
+ >"$TMP/guard-worktree.out" 2>"$TMP/guard-worktree.err"); then
191
+ _fail "AC4: ensure-session from the worktree cwd should have been refused (primary checkout's hold is fresh) but exited 0"
192
+ else
193
+ _pass "AC4: ensure-session from the worktree cwd refuses -- it sees the primary checkout's fresh hold (shared store, not isolated)"
194
+ fi
195
+ if grep -qF "primary-holder" "$TMP/guard-worktree.err"; then
196
+ _pass "AC4: the refusal from the worktree cwd names the primary checkout's holder identity"
197
+ else
198
+ _fail "AC4: refusal message did not name the primary checkout's holder identity: $(cat "$TMP/guard-worktree.err")"
199
+ fi
200
+
201
+ # ==== AC5: current.json (per-actor + legacy) written from worktree cwd is visible from primary =
202
+ echo ""
203
+ echo "=== AC5: current.json written from worktree cwd is visible from the primary checkout ==="
204
+
205
+ CURRENT_WORK_ITEM="kontourai/flow-agents#8802"
206
+ CURRENT_SLUG="kontourai-flow-agents-8802"
207
+
208
+ if (cd "$WORKTREE" && flow_agents_node "$WRITER" ensure-session \
209
+ --work-item "$CURRENT_WORK_ITEM" \
210
+ --actor worktree-current-actor \
211
+ --source-request "ensure-session from the worktree cwd writes current.json." \
212
+ --summary "Prove current.json lands in the shared store." \
213
+ >"$TMP/current-worktree.out" 2>"$TMP/current-worktree.err"); then
214
+ _pass "AC5 setup: ensure-session from the worktree cwd (no --artifact-root) exits 0"
215
+ else
216
+ _fail "AC5 setup: ensure-session from the worktree cwd failed: $(cat "$TMP/current-worktree.out" "$TMP/current-worktree.err")"
217
+ fi
218
+
219
+ if [[ -f "$PRIMARY/.kontourai/flow-agents/current.json" ]]; then
220
+ _pass "AC5: current.json landed under the PRIMARY checkout's shared .kontourai/flow-agents, not an isolated worktree copy"
221
+ else
222
+ _fail "AC5: no current.json found under the primary checkout's .kontourai/flow-agents"
223
+ fi
224
+ if [[ ! -e "$WORKTREE/.kontourai/flow-agents/current.json" ]]; then
225
+ _pass "AC5: no isolated per-worktree current.json was created under the worktree"
226
+ else
227
+ _fail "AC5: an isolated per-worktree current.json was created -- the shared-root fix did not take effect"
228
+ fi
229
+
230
+ if (cd "$PRIMARY" && flow_agents_node "$WRITER" current --format slug \
231
+ >"$TMP/current-primary.out" 2>"$TMP/current-primary.err"); then
232
+ if [[ "$(cat "$TMP/current-primary.out")" == "$CURRENT_SLUG" ]]; then
233
+ _pass "AC5: 'current' invoked from the PRIMARY checkout cwd (no --artifact-root) resolves the session ensure-session wrote from the worktree cwd"
234
+ else
235
+ _fail "AC5: primary-checkout 'current' resolved the wrong slug: $(cat "$TMP/current-primary.out"), expected $CURRENT_SLUG"
236
+ fi
237
+ else
238
+ _fail "AC5: 'current' from the primary checkout failed: $(cat "$TMP/current-primary.out" "$TMP/current-primary.err")"
239
+ fi
240
+
241
+ # stop-goal-fit.js / workflow-steering.js's own findRepoRoot (updated to prefer
242
+ # git-common-dir resolution) must ALSO resolve the primary checkout when invoked with cwd set
243
+ # to the LINKED WORKTREE -- proving the hook-side mirrors got the same fix, not just the CLI.
244
+ HOOK_ROOT_CHECK="$(node -e '
245
+ const { findRepoRoot } = require(process.argv[1]);
246
+ console.log(findRepoRoot(process.argv[2]));
247
+ ' "$ROOT/scripts/hooks/stop-goal-fit.js" "$WORKTREE" 2>/dev/null)"
248
+ REAL_PRIMARY2="$(cd "$PRIMARY" && pwd -P)"
249
+ if [[ -n "$HOOK_ROOT_CHECK" ]]; then
250
+ REAL_HOOK_ROOT="$(cd "$HOOK_ROOT_CHECK" && pwd -P)"
251
+ if [[ "$REAL_HOOK_ROOT" == "$REAL_PRIMARY2" ]]; then
252
+ _pass "AC5: stop-goal-fit.js's findRepoRoot(worktreeCwd) resolves the shared PRIMARY checkout root, not the worktree's own directory"
253
+ else
254
+ _fail "AC5: stop-goal-fit.js's findRepoRoot(worktreeCwd) did not resolve the primary checkout: got $REAL_HOOK_ROOT, expected $REAL_PRIMARY2"
255
+ fi
256
+ else
257
+ _fail "AC5: stop-goal-fit.js's findRepoRoot(worktreeCwd) returned nothing"
258
+ fi
259
+
260
+ HOOK_ROOT_CHECK_STEERING="$(node -e '
261
+ const { findRepoRoot } = require(process.argv[1]);
262
+ console.log(findRepoRoot(process.argv[2]));
263
+ ' "$ROOT/scripts/hooks/workflow-steering.js" "$WORKTREE" 2>/dev/null)"
264
+ if [[ -n "$HOOK_ROOT_CHECK_STEERING" ]]; then
265
+ REAL_HOOK_ROOT_STEERING="$(cd "$HOOK_ROOT_CHECK_STEERING" && pwd -P)"
266
+ if [[ "$REAL_HOOK_ROOT_STEERING" == "$REAL_PRIMARY2" ]]; then
267
+ _pass "AC5: workflow-steering.js's findRepoRoot(worktreeCwd) resolves the shared PRIMARY checkout root, not the worktree's own directory"
268
+ else
269
+ _fail "AC5: workflow-steering.js's findRepoRoot(worktreeCwd) did not resolve the primary checkout: got $REAL_HOOK_ROOT_STEERING, expected $REAL_PRIMARY2"
270
+ fi
271
+ else
272
+ _fail "AC5: workflow-steering.js's findRepoRoot(worktreeCwd) returned nothing"
273
+ fi
274
+
275
+ # ==== FIX 1 (#413 iteration-2, HIGH security): loud, not silent, fail-open inside a broken git
276
+ # working tree ==============================================================
277
+ # A SECOND, disposable linked worktree, deliberately corrupted (its own .git gitlink pointer
278
+ # rewritten to a nonexistent path) AFTER creation, so `git rev-parse --git-common-dir` from
279
+ # inside it genuinely FAILS (git resolution attempted, not merely absent) while the directory is
280
+ # still, by every other measure, inside a git working tree (a `.git` FILE exists right there).
281
+ # This is #357's exact silent-fail-open bug: prior to Fix 1, flowAgentsArtifactRoot/findRepoRoot
282
+ # would fall back to a cwd-local store with NO diagnostic at all. Uses its own worktree fixture
283
+ # (never $WORKTREE, which later AC sections below still depend on being intact).
284
+ echo ""
285
+ echo "=== FIX 1: loud WARNING when inside a git working tree but resolution fails (corrupted gitlink) ==="
286
+
287
+ BROKEN_WORKTREE="$TMP/broken-worktree"
288
+ if (cd "$PRIMARY" && git worktree add -q -b broken-worktree-branch "$BROKEN_WORKTREE" >/dev/null 2>"$TMP/broken-worktree-add.err"); then
289
+ _pass "FIX 1 setup: a second, disposable linked worktree was created for the corruption fixture"
290
+ else
291
+ _fail "FIX 1 setup: 'git worktree add' for the broken-worktree fixture failed: $(cat "$TMP/broken-worktree-add.err")"
292
+ fi
293
+
294
+ # Corrupt the gitlink: a linked worktree's own .git is normally a FILE containing
295
+ # `gitdir: <path-to-real-.git-entry>`. Point it at a nonexistent path so git-common-dir
296
+ # resolution genuinely fails from inside this worktree, while a `.git` FILE still exists here
297
+ # (the exact condition isInsideGitWorkingTree's discriminator detects).
298
+ printf 'gitdir: /nonexistent-gitdir-for-fix1-eval
299
+ ' > "$BROKEN_WORKTREE/.git"
300
+
301
+ BROKEN_COMMON_DIR_CHECK="$(cd "$BROKEN_WORKTREE" && git rev-parse --git-common-dir 2>/dev/null)"
302
+ if [[ -z "$BROKEN_COMMON_DIR_CHECK" ]]; then
303
+ _pass "FIX 1 setup: 'git rev-parse --git-common-dir' genuinely fails from inside the corrupted worktree"
304
+ else
305
+ _fail "FIX 1 setup: corruption did not take effect -- git-common-dir still resolved: $BROKEN_COMMON_DIR_CHECK"
306
+ fi
307
+
308
+ if (cd "$BROKEN_WORKTREE" && flow_agents_node "$WRITER" liveness claim "fix1-corrupted-gitlink-subject" --actor fix1-actor \
309
+ >"$TMP/fix1-claim.out" 2>"$TMP/fix1-claim.err"); then
310
+ _pass "FIX 1: liveness claim from the corrupted worktree still exits 0 (loud warning, not a hard refuse -- this helper sits on read paths too)"
311
+ else
312
+ _fail "FIX 1: liveness claim from the corrupted worktree unexpectedly failed (should warn and fall back, not refuse): $(cat "$TMP/fix1-claim.out" "$TMP/fix1-claim.err")"
313
+ fi
314
+
315
+ if grep -q "\[artifact-root\] WARNING: inside a git working tree but could not resolve the shared repo root" "$TMP/fix1-claim.err"; then
316
+ _pass "FIX 1: a LOUD [artifact-root] WARNING was emitted to stderr for the corrupted-gitlink fail-open"
317
+ else
318
+ _fail "FIX 1: expected a loud [artifact-root] WARNING on stderr, got: $(cat "$TMP/fix1-claim.err")"
319
+ fi
320
+
321
+ # The fallback must still be a cwd-local store under the (corrupted) worktree itself -- the
322
+ # warning is loud, but the actual fail-open target is unchanged from #357's baseline behavior.
323
+ if [[ -f "$BROKEN_WORKTREE/.kontourai/flow-agents/liveness/events.jsonl" ]]; then
324
+ _pass "FIX 1: the fallback store landed at the cwd-local (corrupted worktree) path, as documented"
325
+ else
326
+ _fail "FIX 1: no cwd-local fallback store found under the corrupted worktree after the loud warning"
327
+ fi
328
+
329
+ # Mutation-test note: deleting the isInsideGitWorkingTree() check (i.e. warning unconditionally,
330
+ # even for a cwd with no git repo at all) would make the AC6 non-git companion case below FAIL,
331
+ # since that case asserts pure, silent fail-open with no stderr expectations. Deleting the
332
+ # warnIfFailingOpenInsideGitTree() call entirely (i.e. reverting to pure silent fail-open) would
333
+ # make the grep assertion immediately above this note FAIL.
334
+
335
+ # ==== FIX 1b: a bad ambient GIT_DIR must resolve SILENTLY (proves Fix 3 runs BEFORE Fix 1's
336
+ # warning check) =============================================================================
337
+ # A nonexistent-path GIT_DIR, set as an AMBIENT env var, from a cwd that IS genuinely inside a
338
+ # real, uncorrupted git working tree (the PRIMARY checkout). Fix 3 (env-stripping) means this
339
+ # ambient GIT_DIR is deleted from the env BEFORE the internal `git rev-parse --git-common-dir`
340
+ # shell-out runs, so resolution succeeds normally here -- NO warning, NO fail-open at all. This
341
+ # is an important companion to FIX 1's own gitlink-corruption repro above: it proves Fix 1's loud
342
+ # warning is reserved for GENUINE resolution failures (corrupted .git, git unusable, etc.), not
343
+ # merely "some ambient git env var looked suspicious" -- an ambient GIT_DIR alone must never, by
344
+ # itself, cause a loud warning once Fix 3 has stripped it.
345
+ echo ""
346
+ echo "=== FIX 1b: an ambient (nonexistent) GIT_DIR resolves SILENTLY -- Fix 3's stripping runs first, no false-positive warning ==="
347
+
348
+ if (cd "$PRIMARY" && GIT_DIR=/nonexistent-git-dir-for-fix1b-eval flow_agents_node "$WRITER" liveness claim "fix1b-baddir-subject" --actor fix1b-actor \
349
+ >"$TMP/fix1b-claim.out" 2>"$TMP/fix1b-claim.err"); then
350
+ _pass "FIX 1b: liveness claim with an ambient (nonexistent) GIT_DIR exits 0"
351
+ else
352
+ _fail "FIX 1b: liveness claim with an ambient GIT_DIR unexpectedly failed: $(cat "$TMP/fix1b-claim.out" "$TMP/fix1b-claim.err")"
353
+ fi
354
+
355
+ if grep -q "\[artifact-root\] WARNING" "$TMP/fix1b-claim.err"; then
356
+ _fail "FIX 1b: unexpectedly emitted an [artifact-root] WARNING for a merely-ambient (stripped) GIT_DIR -- Fix 3's env-stripping regressed, or Fix 1 is firing as a false positive: $(cat "$TMP/fix1b-claim.err")"
357
+ else
358
+ _pass "FIX 1b: no [artifact-root] WARNING was emitted -- the ambient GIT_DIR was correctly stripped before resolution (Fix 3), so Fix 1's warning never had a genuine failure to react to"
359
+ fi
360
+
361
+ if [[ -f "$PRIMARY/.kontourai/flow-agents/liveness/events.jsonl" ]] && grep -qF "fix1b-actor" "$PRIMARY/.kontourai/flow-agents/liveness/events.jsonl"; then
362
+ _pass "FIX 1b: the claim landed in PRIMARY's own shared store, resolution was not disrupted by the ambient GIT_DIR"
363
+ else
364
+ _fail "FIX 1b: the claim did not land in PRIMARY's own shared store as expected"
365
+ fi
366
+
367
+ # ==== FIX 1c: loud WARNING via a SECOND, distinct real corruption vector (unreadable .git/HEAD)
368
+ # =============================================================================================
369
+ # A DIFFERENT genuine git-resolution-failure vector from FIX 1's gitlink corruption above:
370
+ # an unreadable .git/HEAD inside yet another disposable worktree. Confirms the loud warning is
371
+ # not narrowly coupled to the specific "gitdir: pointer" corruption shape -- ANY genuine
372
+ # `git rev-parse --git-common-dir` failure from inside a real working tree triggers it. Skips
373
+ # gracefully (not a silent pass) when running as root, where chmod 000 does not restrict access.
374
+ echo ""
375
+ echo "=== FIX 1c: loud WARNING via a second, distinct corruption vector (unreadable .git/HEAD) ==="
376
+
377
+ if [[ "$(id -u)" == "0" ]]; then
378
+ echo " SKIP: running as root -- chmod 000 does not restrict access, cannot exercise this vector here."
379
+ else
380
+ PERM_WORKTREE="$TMP/perm-broken-worktree"
381
+ if (cd "$PRIMARY" && git worktree add -q -b perm-broken-worktree-branch "$PERM_WORKTREE" >/dev/null 2>"$TMP/perm-broken-worktree-add.err"); then
382
+ _pass "FIX 1c setup: a third, disposable linked worktree was created for the permission-corruption fixture"
383
+ else
384
+ _fail "FIX 1c setup: 'git worktree add' for the perm-broken-worktree fixture failed: $(cat "$TMP/perm-broken-worktree-add.err")"
385
+ fi
386
+
387
+ # The linked worktree's real git-dir entry lives under the PRIMARY checkout's
388
+ # .git/worktrees/<name>/ -- corrupt ITS HEAD file (unreadable), which is what
389
+ # `git rev-parse --git-common-dir` from inside PERM_WORKTREE actually needs to read.
390
+ REAL_GITDIR_ENTRY="$(cd "$PERM_WORKTREE" && git rev-parse --git-dir 2>/dev/null)"
391
+ if [[ -n "$REAL_GITDIR_ENTRY" && -f "$REAL_GITDIR_ENTRY/HEAD" ]]; then
392
+ chmod 000 "$REAL_GITDIR_ENTRY/HEAD"
393
+ _pass "FIX 1c setup: made the linked worktree's own HEAD file unreadable"
394
+ else
395
+ _fail "FIX 1c setup: could not locate the linked worktree's real git-dir HEAD file to corrupt"
396
+ fi
397
+
398
+ PERM_COMMON_DIR_CHECK="$(cd "$PERM_WORKTREE" && git rev-parse --git-common-dir 2>/dev/null)"
399
+ if [[ -z "$PERM_COMMON_DIR_CHECK" ]]; then
400
+ _pass "FIX 1c setup: 'git rev-parse --git-common-dir' genuinely fails from inside the permission-corrupted worktree"
401
+ else
402
+ _fail "FIX 1c setup: corruption did not take effect -- git-common-dir still resolved: $PERM_COMMON_DIR_CHECK"
403
+ fi
404
+
405
+ if (cd "$PERM_WORKTREE" && flow_agents_node "$WRITER" liveness claim "fix1c-perm-corrupted-subject" --actor fix1c-actor \
406
+ >"$TMP/fix1c-claim.out" 2>"$TMP/fix1c-claim.err"); then
407
+ _pass "FIX 1c: liveness claim from the permission-corrupted worktree still exits 0 (loud warning, not a hard refuse)"
408
+ else
409
+ _fail "FIX 1c: liveness claim from the permission-corrupted worktree unexpectedly failed: $(cat "$TMP/fix1c-claim.out" "$TMP/fix1c-claim.err")"
410
+ fi
411
+
412
+ if grep -q "\[artifact-root\] WARNING: inside a git working tree but could not resolve the shared repo root" "$TMP/fix1c-claim.err"; then
413
+ _pass "FIX 1c: a LOUD [artifact-root] WARNING was emitted to stderr for this second, distinct corruption vector"
414
+ else
415
+ _fail "FIX 1c: expected a loud [artifact-root] WARNING on stderr, got: $(cat "$TMP/fix1c-claim.err")"
416
+ fi
417
+
418
+ # Restore permissions so this eval's own cleanup (rm -rf "$TMP") can succeed.
419
+ chmod 644 "$REAL_GITDIR_ENTRY/HEAD" 2>/dev/null || true
420
+ fi
421
+
422
+ # ==== AC6: single-checkout backward compat -- no worktree at all, path is byte-identical =======
423
+ echo ""
424
+ echo "=== AC6: single-checkout backward compat (no worktree) -- path unchanged from today ==="
425
+
426
+ PLAIN="$TMP/plain-repo"
427
+ mkdir -p "$PLAIN/kits"
428
+ (cd "$PLAIN" && git init -q && git config user.email "test@test.local" && git config user.name "test" \
429
+ && printf '# marker\n' > kits/.keep && git add -A && git commit -q -m "init")
430
+
431
+ if (cd "$PLAIN" && flow_agents_node "$WRITER" liveness claim "plain-subject" --actor plain-actor \
432
+ >"$TMP/plain-claim.out" 2>"$TMP/plain-claim.err"); then
433
+ _pass "AC6: liveness claim from a plain (non-worktree) git repo cwd exits 0"
434
+ else
435
+ _fail "AC6: liveness claim from a plain git repo cwd failed: $(cat "$TMP/plain-claim.out" "$TMP/plain-claim.err")"
436
+ fi
437
+ # In the single-checkout case, --git-common-dir resolves to .git under cwd itself, so
438
+ # path.dirname() of its absolute form is cwd -- the resolved root MUST be exactly
439
+ # <PLAIN>/.kontourai/flow-agents, byte-for-byte the same as plain path.resolve(cwd, ...).
440
+ if [[ -f "$PLAIN/.kontourai/flow-agents/liveness/events.jsonl" ]]; then
441
+ _pass "AC6: the claim landed at exactly <repo>/.kontourai/flow-agents/liveness (unchanged, single-checkout path)"
442
+ else
443
+ _fail "AC6: the claim did not land at the expected unchanged single-checkout path"
444
+ fi
445
+
446
+ LOCAL_ARTIFACT_ROOT_SUBDIR="lib"
447
+ LOCAL_ARTIFACT_ROOT_MODULE="$ROOT/build/src/$LOCAL_ARTIFACT_ROOT_SUBDIR/local-artifact-root.js"
448
+ RESOLVED_PLAIN_ROOT="$(node -e '
449
+ const { flowAgentsArtifactRoot } = require(process.argv[1]);
450
+ console.log(flowAgentsArtifactRoot(process.argv[2]));
451
+ ' "$LOCAL_ARTIFACT_ROOT_MODULE" "$PLAIN")"
452
+ EXPECTED_PLAIN_ROOT="$(node -e 'console.log(require("path").resolve(process.argv[1], ".kontourai/flow-agents"))' "$PLAIN")"
453
+ if [[ "$RESOLVED_PLAIN_ROOT" == "$EXPECTED_PLAIN_ROOT" ]]; then
454
+ _pass "AC6: flowAgentsArtifactRoot(cwd) for a plain single-checkout repo is byte-identical to plain path.resolve(cwd, '.kontourai/flow-agents')"
455
+ else
456
+ _fail "AC6: flowAgentsArtifactRoot(cwd) diverged from the plain cwd-based path in the single-checkout case: got $RESOLVED_PLAIN_ROOT, expected $EXPECTED_PLAIN_ROOT"
457
+ fi
458
+
459
+ # Companion: a cwd with NO git repo at all (git resolution fails) must ALSO fall back to the
460
+ # exact plain cwd-based path -- the fail-open guarantee.
461
+ NONGIT="$TMP/non-git-dir"
462
+ mkdir -p "$NONGIT"
463
+ RESOLVED_NONGIT_ROOT="$(node -e '
464
+ const { flowAgentsArtifactRoot } = require(process.argv[1]);
465
+ console.log(flowAgentsArtifactRoot(process.argv[2]));
466
+ ' "$LOCAL_ARTIFACT_ROOT_MODULE" "$NONGIT")"
467
+ EXPECTED_NONGIT_ROOT="$(node -e 'console.log(require("path").resolve(process.argv[1], ".kontourai/flow-agents"))' "$NONGIT")"
468
+ if [[ "$RESOLVED_NONGIT_ROOT" == "$EXPECTED_NONGIT_ROOT" ]]; then
469
+ _pass "AC6: flowAgentsArtifactRoot(cwd) for a non-git directory fails open to the plain cwd-based path, byte-identical to today"
470
+ else
471
+ _fail "AC6: flowAgentsArtifactRoot(cwd) for a non-git directory did not fail open correctly: got $RESOLVED_NONGIT_ROOT, expected $EXPECTED_NONGIT_ROOT"
472
+ fi
473
+
474
+ # ==== AC6b: subdirectory cwd resolves to the SHARED REPO-ROOT store (#413 iteration-2 Fix 2) ====
475
+ # Intentional, NOT byte-identical: `git rev-parse --git-common-dir` from a subdirectory returns a
476
+ # RELATIVE path this resolver walks up to the repo root, so a subdirectory invocation now
477
+ # correctly resolves to <repo-root>/.kontourai/flow-agents -- the SAME shared store every other
478
+ # cwd in this repo resolves to -- rather than the OLD cwd-anchored path.resolve(cwd, ...) a
479
+ # subdirectory invocation used to produce. See this eval's header docstring (AC6b) for the full
480
+ # rationale and the Fix 2a grep confirming no real call site depends on the old subdir-anchored
481
+ # behavior.
482
+ echo ""
483
+ echo "=== AC6b: subdirectory cwd resolves to the shared repo-root store (intended, not byte-identical) ==="
484
+
485
+ SUBDIR="$PLAIN/nested/deeper"
486
+ mkdir -p "$SUBDIR"
487
+ RESOLVED_SUBDIR_ROOT="$(node -e '
488
+ const { flowAgentsArtifactRoot } = require(process.argv[1]);
489
+ console.log(flowAgentsArtifactRoot(process.argv[2]));
490
+ ' "$LOCAL_ARTIFACT_ROOT_MODULE" "$SUBDIR")"
491
+ EXPECTED_SUBDIR_ROOT="$(node -e 'console.log(require("path").resolve(process.argv[1], ".kontourai/flow-agents"))' "$PLAIN")"
492
+ OLD_CWD_ANCHORED_ROOT="$(node -e 'console.log(require("path").resolve(process.argv[1], ".kontourai/flow-agents"))' "$SUBDIR")"
493
+
494
+ if [[ "$RESOLVED_SUBDIR_ROOT" == "$EXPECTED_SUBDIR_ROOT" ]]; then
495
+ _pass "AC6b: flowAgentsArtifactRoot(subdirCwd) resolves to the REPO-ROOT shared store <repo>/.kontourai/flow-agents, not the subdir"
496
+ else
497
+ _fail "AC6b: flowAgentsArtifactRoot(subdirCwd) did not resolve to the repo-root store: got $RESOLVED_SUBDIR_ROOT, expected $EXPECTED_SUBDIR_ROOT"
498
+ fi
499
+ if [[ "$RESOLVED_SUBDIR_ROOT" != "$OLD_CWD_ANCHORED_ROOT" ]]; then
500
+ _pass "AC6b: this genuinely differs from the OLD cwd-anchored subdir path (confirms this is the intended git-common-dir walk-up, not an accidental no-op)"
501
+ else
502
+ _fail "AC6b: resolved root unexpectedly matched the old cwd-anchored subdir path -- the git-common-dir walk-up did not take effect for this cwd"
503
+ fi
504
+
505
+ # ==== FIX 3 (#413 iteration-2, MEDIUM security): ambient GIT_DIR/GIT_COMMON_DIR must NOT redirect
506
+ # resolution to a DIFFERENT real repo =========================================================
507
+ # A SECOND, wholly unrelated real git repo, plus a VALID (not merely nonexistent, unlike Fix 1b's
508
+ # corruption fixture) ambient GIT_DIR pointing at that other repo's .git directory, invoked from
509
+ # inside $PLAIN (the AC6 single-checkout fixture). Without Fix 3's env-stripping, git would honor
510
+ # the ambient GIT_DIR and --git-common-dir would resolve to the OTHER repo, silently redirecting
511
+ # the shared .kontourai/flow-agents store there. With Fix 3, resolution must depend on cwd alone.
512
+ echo ""
513
+ echo "=== FIX 3: an ambient GIT_DIR pointing at a DIFFERENT real repo does not redirect the resolved store ==="
514
+
515
+ OTHER_REPO="$TMP/fix3-other-repo"
516
+ mkdir -p "$OTHER_REPO/kits"
517
+ (cd "$OTHER_REPO" && git init -q && git config user.email "test@test.local" && git config user.name "test" \
518
+ && printf '# marker\n' > kits/.keep && git add -A && git commit -q -m "init other repo")
519
+ if [[ -d "$OTHER_REPO/.git" ]]; then
520
+ _pass "FIX 3 setup: a second, unrelated real git repo was created for the GIT_DIR redirection fixture"
521
+ else
522
+ _fail "FIX 3 setup: 'git init' for the other-repo fixture failed"
523
+ fi
524
+
525
+ FIX3_SUBJECT="fix3-git-dir-redirect-subject"
526
+ if (cd "$PLAIN" && GIT_DIR="$OTHER_REPO/.git" flow_agents_node "$WRITER" liveness claim "$FIX3_SUBJECT" --actor fix3-actor \
527
+ >"$TMP/fix3-claim.out" 2>"$TMP/fix3-claim.err"); then
528
+ _pass "FIX 3: liveness claim with an ambient GIT_DIR pointing at another repo exits 0"
529
+ else
530
+ _fail "FIX 3: liveness claim with an ambient GIT_DIR pointing at another repo unexpectedly failed: $(cat "$TMP/fix3-claim.out" "$TMP/fix3-claim.err")"
531
+ fi
532
+
533
+ if [[ -f "$PLAIN/.kontourai/flow-agents/liveness/events.jsonl" ]] && grep -qF "fix3-actor" "$PLAIN/.kontourai/flow-agents/liveness/events.jsonl"; then
534
+ _pass "FIX 3: the claim landed in PLAIN's OWN shared store (cwd-anchored), not redirected by the ambient GIT_DIR"
535
+ else
536
+ _fail "FIX 3: the claim did not land in PLAIN's own store -- ambient GIT_DIR may have redirected resolution"
537
+ fi
538
+ if [[ ! -d "$OTHER_REPO/.kontourai" ]]; then
539
+ _pass "FIX 3: the OTHER repo (named by the ambient GIT_DIR) received NO .kontourai artifact at all -- resolution was not redirected there"
540
+ else
541
+ _fail "FIX 3: the OTHER repo unexpectedly received a .kontourai artifact -- ambient GIT_DIR redirected resolution (Fix 3 regressed)"
542
+ fi
543
+
544
+ # Direct resolver-level assertion (no subprocess env-inheritance ambiguity): resolveSharedRepoRoot
545
+ # invoked with an explicit ambient GIT_DIR pointing at OTHER_REPO must still resolve PLAIN's own
546
+ # root, never OTHER_REPO's.
547
+ RESOLVED_WITH_BAD_GIT_DIR="$(cd "$PLAIN" && GIT_DIR="$OTHER_REPO/.git" node -e '
548
+ const { resolveSharedRepoRoot } = require(process.argv[1]);
549
+ console.log(resolveSharedRepoRoot(process.argv[2]));
550
+ ' "$LOCAL_ARTIFACT_ROOT_MODULE" "$PLAIN")"
551
+ REAL_PLAIN="$(cd "$PLAIN" && pwd -P)"
552
+ REAL_OTHER_REPO="$(cd "$OTHER_REPO" && pwd -P)"
553
+ if [[ -n "$RESOLVED_WITH_BAD_GIT_DIR" ]]; then
554
+ REAL_RESOLVED_WITH_BAD_GIT_DIR="$(cd "$RESOLVED_WITH_BAD_GIT_DIR" && pwd -P)"
555
+ if [[ "$REAL_RESOLVED_WITH_BAD_GIT_DIR" == "$REAL_PLAIN" ]]; then
556
+ _pass "FIX 3: resolveSharedRepoRoot(PLAIN cwd) with ambient GIT_DIR=<other repo> still resolves PLAIN's own root"
557
+ elif [[ "$REAL_RESOLVED_WITH_BAD_GIT_DIR" == "$REAL_OTHER_REPO" ]]; then
558
+ _fail "FIX 3: resolveSharedRepoRoot was REDIRECTED to the other repo named by ambient GIT_DIR -- Fix 3 regressed"
559
+ else
560
+ _fail "FIX 3: resolveSharedRepoRoot resolved to neither expected root: got $REAL_RESOLVED_WITH_BAD_GIT_DIR"
561
+ fi
562
+ else
563
+ _fail "FIX 3: resolveSharedRepoRoot(PLAIN cwd) with ambient GIT_DIR returned nothing"
564
+ fi
565
+
566
+ # ---- Summary ----
567
+ echo ""
568
+ echo "----------------------------------------------"
569
+ if [[ $errors -eq 0 ]]; then
570
+ echo "test_liveness_worktree_root: all checks passed."
571
+ exit 0
572
+ else
573
+ echo "test_liveness_worktree_root: $errors check(s) failed."
574
+ exit 1
575
+ fi
@@ -201,13 +201,18 @@ flow_agents_node "workflow-sidecar" ensure-session \
201
201
  --title "Composed Gate Claim Test" \
202
202
  --summary "Test composed Builder publish/learn flow producer." \
203
203
  --flow-id builder.build \
204
- --step-id pr-open \
205
204
  --timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
206
205
 
207
206
  flow_agents_node "workflow-sidecar" init-plan "$COMPOSED_ROOT/composed-gate-claim/composed-gate-claim--deliver.md" \
208
207
  --source-request "Test" --summary "Testing" \
209
208
  --timestamp "2026-06-26T00:00:00Z" >/dev/null 2>&1
210
209
 
210
+ flow_agents_node "workflow-sidecar" advance-state "$COMPOSED_ROOT/composed-gate-claim" \
211
+ --status in_progress --phase release \
212
+ --summary "Testing composed pr-open producer." --next-action "Record claim." \
213
+ --flow-definition builder.build \
214
+ --timestamp "2026-06-26T00:00:30Z" >/dev/null 2>&1
215
+
211
216
  if flow_agents_node "workflow-sidecar" record-gate-claim "$COMPOSED_ROOT/composed-gate-claim" \
212
217
  --status pass \
213
218
  --summary "PR opened with verification evidence." \