@kontourai/flow-agents 2.2.0 → 2.4.0

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 (250) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/AGENTS.md +4 -4
  4. package/CHANGELOG.md +46 -0
  5. package/CONTEXT.md +14 -0
  6. package/README.md +9 -3
  7. package/agents/dev.json +1 -1
  8. package/agents/tool-code-reviewer.json +1 -1
  9. package/agents/tool-planner.json +3 -3
  10. package/agents/tool-verifier.json +3 -3
  11. package/build/src/cli/console-learning-projection.js +3 -2
  12. package/build/src/cli/init.js +104 -21
  13. package/build/src/cli/kit.js +37 -6
  14. package/build/src/cli/pull-work-provider.js +1 -1
  15. package/build/src/cli/usage-feedback.js +3 -3
  16. package/build/src/cli/validate-hook-influence.js +1 -0
  17. package/build/src/cli/validate-workflow-artifacts.js +22 -6
  18. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  19. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  20. package/build/src/cli/workflow-sidecar.js +792 -69
  21. package/build/src/flow-kit/validate.d.ts +23 -0
  22. package/build/src/flow-kit/validate.js +67 -1
  23. package/build/src/index.d.ts +1 -1
  24. package/build/src/index.js +1 -1
  25. package/build/src/lib/flow-resolver.d.ts +6 -4
  26. package/build/src/lib/flow-resolver.js +125 -58
  27. package/build/src/lib/local-artifact-root.d.ts +14 -2
  28. package/build/src/lib/local-artifact-root.js +22 -5
  29. package/build/src/lib/workflow-learning-projection.js +2 -2
  30. package/build/src/runtime-adapters.d.ts +12 -0
  31. package/build/src/runtime-adapters.js +56 -16
  32. package/build/src/tools/build-universal-bundles.js +49 -16
  33. package/build/src/tools/generate-context-map.js +10 -8
  34. package/build/src/tools/validate-source-tree.js +23 -1
  35. package/console.telemetry.json +22 -0
  36. package/context/contracts/artifact-contract.md +38 -7
  37. package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
  38. package/context/contracts/decision-registry-contract.md +263 -0
  39. package/context/contracts/delivery-contract.md +1 -1
  40. package/context/contracts/planning-contract.md +2 -1
  41. package/context/contracts/review-contract.md +5 -3
  42. package/context/contracts/verification-contract.md +9 -1
  43. package/context/contracts/work-item-contract.md +1 -1
  44. package/context/deferred/parallelization.md +1 -1
  45. package/context/gate-awareness.md +4 -4
  46. package/context/scripts/git-status.sh +2 -2
  47. package/context/scripts/hooks/config-protection.js +502 -2
  48. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  49. package/context/scripts/hooks/run-hook.js +14 -1
  50. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  51. package/context/scripts/hooks/workflow-steering.js +212 -6
  52. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  54. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  55. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  56. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  57. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
  58. package/docs/agent-system-guidebook.md +3 -3
  59. package/docs/agent-usage-feedback-loop.md +6 -6
  60. package/docs/context-map.md +9 -7
  61. package/docs/decisions/decision-records.md +57 -0
  62. package/docs/decisions/index.md +14 -0
  63. package/docs/decisions/promotion-gate.md +52 -0
  64. package/docs/developer-architecture.md +1 -1
  65. package/docs/fixture-ownership.md +3 -0
  66. package/docs/flow-kit-repository-contract.md +5 -5
  67. package/docs/getting-started.md +12 -2
  68. package/docs/integrations/harness-install.md +2 -1
  69. package/docs/kit-authoring-guide.md +64 -5
  70. package/docs/knowledge-kit.md +5 -1
  71. package/docs/learnings/2026-07-improvement-program.md +203 -0
  72. package/docs/migrations.md +6 -5
  73. package/docs/north-star.md +2 -2
  74. package/docs/operating-layers.md +4 -3
  75. package/docs/repository-structure.md +5 -5
  76. package/docs/skills-map.md +8 -8
  77. package/docs/spec/runtime-hook-surface.md +19 -15
  78. package/docs/spikes/graph-provider-2026-07.md +155 -0
  79. package/docs/standards-register.md +7 -7
  80. package/docs/trust-anchor-adoption.md +105 -4
  81. package/docs/work-item-adapters.md +1 -1
  82. package/docs/workflow-artifact-lifecycle.md +72 -9
  83. package/docs/workflow-eval-strategy.md +3 -3
  84. package/docs/workflow-shared-contracts.md +1 -1
  85. package/docs/workflow-usage-guide.md +254 -21
  86. package/evals/acceptance/DEMO-false-completion.md +20 -11
  87. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  88. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  89. package/evals/acceptance/test_kiro_harness.sh +1 -1
  90. package/evals/ci/antigaming-suite.sh +5 -0
  91. package/evals/ci/run-baseline.sh +65 -1
  92. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  93. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  94. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  95. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  96. package/evals/fixtures/hook-influence/cases.json +32 -0
  97. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  98. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  99. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  100. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  101. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  102. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  103. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  104. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  105. package/evals/integration/test_actor_identity.sh +254 -0
  106. package/evals/integration/test_builder_step_producers.sh +33 -23
  107. package/evals/integration/test_bundle_install.sh +22 -16
  108. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  109. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  110. package/evals/integration/test_checkpoint_signing.sh +43 -0
  111. package/evals/integration/test_claim_lookup.sh +4 -4
  112. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  113. package/evals/integration/test_command_log_concurrency.sh +8 -8
  114. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  115. package/evals/integration/test_command_log_integrity.sh +16 -16
  116. package/evals/integration/test_console_learning_projection.sh +1 -1
  117. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  118. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  119. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  120. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  121. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  122. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  123. package/evals/integration/test_flow_kit_repository.sh +2 -0
  124. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  125. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  126. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  127. package/evals/integration/test_gate_lockdown.sh +97 -73
  128. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  129. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  130. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  131. package/evals/integration/test_goal_fit_hook.sh +32 -32
  132. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  133. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  134. package/evals/integration/test_hook_influence_cases.sh +1 -0
  135. package/evals/integration/test_install_merge.sh +262 -1
  136. package/evals/integration/test_kit_identity_trust.sh +6 -6
  137. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  138. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  139. package/evals/integration/test_liveness_verdict.sh +394 -0
  140. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  141. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  142. package/evals/integration/test_promote_gate.sh +178 -0
  143. package/evals/integration/test_publish_delivery.sh +2 -2
  144. package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
  145. package/evals/integration/test_pull_work_provider.sh +2 -2
  146. package/evals/integration/test_reconcile_soundness.sh +4 -4
  147. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  148. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  149. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  150. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  151. package/evals/integration/test_trust_reconcile.sh +2 -2
  152. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  153. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  154. package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
  155. package/evals/integration/test_usage_feedback_global.sh +5 -5
  156. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  157. package/evals/integration/test_verify_cli.sh +2 -2
  158. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  159. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  160. package/evals/integration/test_workflow_artifacts.sh +33 -0
  161. package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
  162. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  163. package/evals/lib/codex-provider.sh +1 -1
  164. package/evals/lib/node.sh +19 -1
  165. package/evals/run.sh +22 -0
  166. package/evals/static/test_decisions.sh +150 -0
  167. package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
  168. package/evals/static/test_package.sh +19 -10
  169. package/evals/static/test_universal_bundles.sh +48 -3
  170. package/evals/static/test_workflow_skills.sh +41 -8
  171. package/kits/builder/flows/build.flow.json +3 -74
  172. package/kits/builder/flows/publish-learn.flow.json +90 -0
  173. package/kits/builder/kit.json +11 -0
  174. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  175. package/kits/builder/skills/deliver/SKILL.md +13 -11
  176. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  177. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  178. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  179. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  180. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  181. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  182. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  183. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  184. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  185. package/kits/builder/skills/pull-work/SKILL.md +80 -5
  186. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  187. package/kits/builder/skills/review-work/SKILL.md +24 -1
  188. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  189. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  190. package/kits/catalog.json +6 -0
  191. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  192. package/kits/veritas-governance/docs/README.md +75 -0
  193. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  194. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  195. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  196. package/kits/veritas-governance/kit.json +16 -0
  197. package/package.json +8 -4
  198. package/packaging/README.md +2 -2
  199. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  200. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  201. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  202. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  203. package/packaging/manifest.json +9 -10
  204. package/schemas/decision-record.schema.json +109 -0
  205. package/schemas/workflow-evidence.schema.json +1 -1
  206. package/schemas/workflow-state.schema.json +5 -0
  207. package/scripts/README.md +8 -3
  208. package/scripts/check-content-boundary.cjs +1 -1
  209. package/scripts/check-decisions.cjs +356 -0
  210. package/scripts/ci/derive-claim-status.mjs +74 -0
  211. package/scripts/ci/trust-reconcile.js +436 -79
  212. package/scripts/git-status.sh +2 -2
  213. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  214. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  215. package/scripts/hooks/config-protection.js +102 -30
  216. package/scripts/hooks/evidence-capture.js +3 -3
  217. package/scripts/hooks/lib/actor-identity.js +368 -0
  218. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  219. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  220. package/scripts/hooks/lib/liveness-policy.js +127 -0
  221. package/scripts/hooks/lib/liveness-read.js +105 -12
  222. package/scripts/hooks/lib/liveness-write.js +46 -0
  223. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  224. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  225. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  226. package/scripts/hooks/stop-goal-fit.js +157 -26
  227. package/scripts/hooks/workflow-steering.js +77 -3
  228. package/scripts/install-codex-home.sh +138 -38
  229. package/scripts/statusline/flow-agents-statusline.js +2 -2
  230. package/src/cli/console-learning-projection.ts +3 -2
  231. package/src/cli/init.ts +101 -21
  232. package/src/cli/kit.ts +37 -6
  233. package/src/cli/public-api.test.mjs +58 -6
  234. package/src/cli/pull-work-provider.ts +1 -1
  235. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  236. package/src/cli/usage-feedback.ts +3 -3
  237. package/src/cli/validate-hook-influence.ts +1 -0
  238. package/src/cli/validate-workflow-artifacts.ts +22 -6
  239. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  240. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  241. package/src/cli/workflow-sidecar.ts +795 -71
  242. package/src/flow-kit/validate.ts +74 -1
  243. package/src/index.ts +6 -2
  244. package/src/lib/flow-resolver.ts +123 -55
  245. package/src/lib/local-artifact-root.ts +24 -5
  246. package/src/lib/workflow-learning-projection.ts +2 -2
  247. package/src/runtime-adapters.ts +61 -15
  248. package/src/tools/build-universal-bundles.ts +49 -16
  249. package/src/tools/generate-context-map.ts +10 -8
  250. package/src/tools/validate-source-tree.ts +23 -1
@@ -8,6 +8,44 @@
8
8
  * Also blocks git verification-bypass flags in actual flag positions only.
9
9
  * Text that merely mentions the flag inside quoted strings or prose is allowed.
10
10
  *
11
+ * GATE LOCK-DOWN (adversarial review Finding 2 + anti-gaming; R5a extension):
12
+ * Also blocks Write/Edit to kill-switch / routing files that an agent could use
13
+ * to self-tamper the gate:
14
+ * - Shell profiles (.bashrc, .bash_profile, etc.) — where an agent could
15
+ * export FLOW_AGENTS_GOAL_FIT_MODE=off for the next bash -lc hook invocation.
16
+ * - .claude/settings.json / settings.local.json — where an agent could add an
17
+ * env block or delete the Stop hook entry entirely.
18
+ * - .kontourai/flow-agents/current.json — where an agent could forge active_flow_id /
19
+ * active_step_id to route the gate to a fake permissive FlowDefinition.
20
+ * - .kontourai/flow-agents/<slug>/state.json — where an agent could forge phase=planning
21
+ * to downgrade from FULL_BLOCK to HARD_BLOCK (R5a: MEDIUM finding fix).
22
+ * SAFE: the CLI writes state.json via writeState → writeJson → fs.writeFileSync.
23
+ * - .kontourai/flow-agents/<slug>/trust.bundle — where an agent could forge claims.
24
+ * SAFE: the CLI writes trust.bundle via writeTrustBundle → writeJson → fs.writeFileSync.
25
+ * (Verified: ensureSession / advanceState / writeCurrent / writeState /
26
+ * writeTrustBundle all use writeJson which calls fs.writeFileSync directly —
27
+ * blocking the Write/Edit tool path does NOT break legit CLI sidecar writes.)
28
+ *
29
+ * Also detects (best-effort) Bash-command redirects that write to the same paths
30
+ * via shell operators (>, >>, tee). HONEST: this is INCOMPLETE COVERAGE — sed -i,
31
+ * process substitution, /dev/fd tricks, and other non-standard redirect forms are
32
+ * NOT caught. An agent with shell access can still evade. The real anchor is
33
+ * external: signed checkpoints + human review. This raises the bar, not a wall.
34
+ *
35
+ * INTERPRETER-WRITE DETECTION — INCOMPLETE (R5a best-effort):
36
+ * Also checks Bash commands matching interpreter patterns (node -e, py3 -c,
37
+ * sed -i, perl -e) combined with a protected-path token literal in the command
38
+ * string. KNOWN EVASIONS NOT CAUGHT: runtime-constructed paths (process.env.HOME +
39
+ * '/.bashrc', homedir()+'/.bashrc'), base64-encoded paths, multi-step path assembly,
40
+ * any interpreter not in the list (ruby, php, etc.), and multiline here-docs.
41
+ * The real anchor remains external (clean CI env + human review).
42
+ *
43
+ * FROZEN bar-raiser (ADR 0018). The redirect / interpreter-write / cp-target detection
44
+ * is intentionally non-exhaustive and is NOT to be grown with new evasion-pattern rules.
45
+ * A newly-discovered self-tamper vector belongs in the external CI anchor (trust-reconcile
46
+ * does not trust agent-written files) and the required anti-gaming suite — not another
47
+ * local string matcher. Bug fixes that don't expand the pattern surface are still welcome.
48
+ *
11
49
  * Exit codes: 0 = allow, 2 = block
12
50
  */
13
51
 
@@ -26,8 +64,129 @@ const PROTECTED_FILES = new Set([
26
64
  '.ruff.toml', 'ruff.toml',
27
65
  '.shellcheckrc', '.stylelintrc', '.stylelintrc.json', '.stylelintrc.yml',
28
66
  '.markdownlint.json', '.markdownlint.yaml', '.markdownlintrc',
67
+ // ── Gate lock-down: shell profiles ────────────────────────────────────────
68
+ // An agent could write `export FLOW_AGENTS_GOAL_FIT_MODE=off` here so the next
69
+ // `bash -lc <hook>` invocation inherits the override and silences the gate.
70
+ // Blocking Write/Edit to these basenames raises the cost without stopping
71
+ // legitimate terminal customization done outside the agent session.
72
+ '.bash_profile', '.bashrc', '.profile', '.zprofile', '.zshrc',
29
73
  ]);
30
74
 
75
+ // ---------------------------------------------------------------------------
76
+ // Gate lock-down: path-pattern protected files
77
+ //
78
+ // These files need PATH context (not just basename) to avoid over-matching
79
+ // common file names in unrelated projects.
80
+ //
81
+ // R5a adds flow-agents runtime state and trust.bundle paths.
82
+ //
83
+ // checkProtectedPathPattern(filePath) → { name, reason } | null
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Check whether a Write/Edit tool file path targets a gate kill-switch file
88
+ * that requires path-level matching (.claude/settings.json,
89
+ * .kontourai/flow-agents/current.json, .kontourai/flow-agents/<slug>/state.json,
90
+ * .kontourai/flow-agents/<slug>/trust.bundle, and deprecated runtime-shaped
91
+ * .flow-agents equivalents).
92
+ *
93
+ * Returns { name, reason } when blocked, null when allowed.
94
+ *
95
+ * BAR-RAISER framing (honest): this is not airtight. An agent that writes the
96
+ * file through a Bash redirect or through a shell call that constructs the path
97
+ * at runtime can still evade. The real anchor is external (signed checkpoints +
98
+ * human review). We raise the cost of casual/direct manipulation.
99
+ */
100
+ function checkProtectedPathPattern(filePath) {
101
+ if (!filePath || typeof filePath !== 'string') return null;
102
+ // Normalize: forward-slashes, strip leading ~/
103
+ const norm = filePath.replace(/\\/g, '/').replace(/^~\//, '');
104
+
105
+ // .claude/settings.json — an agent could add an env block or delete the Stop
106
+ // hook to disable gate enforcement for the entire session.
107
+ if (/(?:^|\/)\.claude\/settings\.json$/.test(norm)) {
108
+ return {
109
+ name: '.claude/settings.json',
110
+ reason: 'an agent could add an env block or remove the Stop hook to disable gate enforcement',
111
+ };
112
+ }
113
+
114
+ // .claude/settings.local.json — same risk as settings.json (local overrides
115
+ // are loaded alongside the main settings file by Claude Code).
116
+ if (/(?:^|\/)\.claude\/settings\.local\.json$/.test(norm)) {
117
+ return {
118
+ name: '.claude/settings.local.json',
119
+ reason: 'an agent could add an env block or remove the Stop hook to disable gate enforcement',
120
+ };
121
+ }
122
+
123
+ // .kontourai/flow-agents/current.json — an agent could forge active_flow_id / active_step_id
124
+ // to route the gate to a permissive or empty-expects FlowDefinition.
125
+ // SAFE: the workflow CLI writes current.json via fs (writeJson → fs.writeFileSync),
126
+ // NOT via the Write/Edit tool — blocking the tool path does not break legit sidecar.
127
+ if (/(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\.json$/.test(norm)) {
128
+ return {
129
+ name: '.kontourai/flow-agents/current.json',
130
+ reason: 'an agent could forge active_flow_id/active_step_id to route the gate to a permissive FlowDefinition',
131
+ };
132
+ }
133
+
134
+ // .kontourai/flow-agents/.goal-fit-block-streak.json controls soft-block
135
+ // release counting. An agent could force early advisory-gate release by
136
+ // writing a high count.
137
+ if (/(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/\.goal-fit-block-streak\.json$/.test(norm)) {
138
+ return {
139
+ name: '.kontourai/flow-agents/.goal-fit-block-streak.json',
140
+ reason: 'an agent could manipulate goal-fit block streak state to force early soft-block release',
141
+ };
142
+ }
143
+
144
+ // .kontourai/flow-agents/<slug>/state.json — an agent could forge phase=planning to
145
+ // downgrade the block regime (FULL_BLOCK → HARD_BLOCK) and weaken gate checks.
146
+ // SAFE: the CLI writes state.json via writeState → writeJson → fs.writeFileSync,
147
+ // NOT via the Write/Edit tool — blocking the tool path does not break legit sidecar.
148
+ if (/(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/state\.json$/.test(norm)) {
149
+ return {
150
+ name: '.kontourai/flow-agents/<slug>/state.json',
151
+ reason: 'an agent could forge phase=planning to downgrade the block regime and weaken gate enforcement',
152
+ };
153
+ }
154
+
155
+ // .kontourai/flow-agents/<slug>/trust.bundle — an agent could forge claims (e.g. status=verified
156
+ // or impactLevel=low) to suppress gate blocks or make disputed evidence appear accepted.
157
+ // SAFE: the CLI writes trust.bundle via writeTrustBundle → writeJson → fs.writeFileSync,
158
+ // NOT via the Write/Edit tool — blocking the tool path does not break legit sidecar.
159
+ if (/(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/trust\.bundle$/.test(norm)) {
160
+ return {
161
+ name: '.kontourai/flow-agents/<slug>/trust.bundle',
162
+ reason: 'an agent could forge trust claims (verified status, impact level) to bypass gate integrity checks',
163
+ };
164
+ }
165
+
166
+ // delivery/trust.bundle is the CI anchor read by trust-reconcile.js and
167
+ // used as the attestation subject in mint-attestation.js. An agent could
168
+ // copy a forged bundle here to corrupt the CI trust check.
169
+ // SAFE: publishDelivery writes via fs.copyFileSync (not Write/Edit tool).
170
+ // RESIDUAL: runtime-constructed paths and fs writes are unaffected.
171
+ if (/(?:^|\/)delivery\/trust\.bundle$/.test(norm)) {
172
+ return {
173
+ name: "delivery/trust.bundle",
174
+ reason: "an agent could write a forged bundle to corrupt the CI trust-reconcile anchor",
175
+ };
176
+ }
177
+
178
+ // delivery/trust.checkpoint.json -- the signed checkpoint companion.
179
+ // SAFE: publishDelivery writes via fs.copyFileSync, NOT via Write/Edit tool.
180
+ if (/(?:^|\/)delivery\/trust\.checkpoint\.json$/.test(norm)) {
181
+ return {
182
+ name: "delivery/trust.checkpoint.json",
183
+ reason: "an agent could forge a signed delivery by writing a tampered checkpoint",
184
+ };
185
+ }
186
+
187
+ return null;
188
+ }
189
+
31
190
  // ---------------------------------------------------------------------------
32
191
  // Shell-aware tokenizer
33
192
  //
@@ -217,6 +376,291 @@ function checkCommandForBypass(command) {
217
376
  return null;
218
377
  }
219
378
 
379
+ // ---------------------------------------------------------------------------
380
+ // Gate lock-down: Bash-redirect detection for protected kill-switch paths
381
+ //
382
+ // When the agent uses a Bash tool (tool_input.command), detect redirects that
383
+ // write to protected paths via shell redirect operators (> >>) or `tee`.
384
+ //
385
+ // R5a extends coverage to flow-agents runtime state and trust.bundle.
386
+ //
387
+ // HONEST — INCOMPLETE COVERAGE: this catches the obvious forms only:
388
+ // - `cmd > ~/.bashrc` ✓ caught (> redirect operator)
389
+ // - `cmd >> .claude/settings.json` ✓ caught (>> redirect operator)
390
+ // - `tee .kontourai/flow-agents/current.json` ✓ caught (tee command)
391
+ // - `cmd > .kontourai/flow-agents/s/state.json` ✓ caught (> redirect operator)
392
+ // - `tee .kontourai/flow-agents/s/trust.bundle` ✓ caught (tee command)
393
+ // - `sed -i ... .bashrc` ✗ NOT caught (in-place edit)
394
+ // - `interpreted-lang -c "open(...,'w')" ✗ NOT caught (see interpreter check)
395
+ // - `/dev/fd` tricks, process substitution ✗ NOT caught
396
+ //
397
+ // An agent with shell access can still evade by constructing paths at runtime,
398
+ // using subshells, or using tools not scanned here. The real tamper-proof
399
+ // boundary is external: signed checkpoints + human review. This raises the bar.
400
+ // ---------------------------------------------------------------------------
401
+
402
+ /**
403
+ * Regex that matches a normalized (forward-slash) path ending with a protected
404
+ * kill-switch file: shell profiles, .claude/settings*.json,
405
+ * .kontourai/flow-agents/current.json,
406
+ * .kontourai/flow-agents/.goal-fit-block-streak.json,
407
+ * .kontourai/flow-agents/<slug>/state.json,
408
+ * .kontourai/flow-agents/<slug>/trust.bundle, and deprecated runtime-shaped
409
+ * .flow-agents equivalents.
410
+ */
411
+ const REDIRECT_PROTECTED_RE = /(?:^|\/|~\/)(\.bash_profile|\.bashrc|\.profile|\.zprofile|\.zshrc)$|(?:^|\/)\.claude\/settings(?:\.local)?\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/current\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/\.goal-fit-block-streak\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/state\.json$|(?:^|\/)(?:\.kontourai\/flow-agents|\.flow-agents)\/[^/]+\/trust\.bundle$|(?:^|\/)delivery\/trust\.bundle$|(?:^|\/)delivery\/trust\.checkpoint\.json$/;
412
+
413
+ /**
414
+ * Return true when a token (an unquoted redirect target or tee argument) matches
415
+ * a protected kill-switch path.
416
+ */
417
+ function matchesRedirectProtected(token) {
418
+ if (!token || typeof token !== 'string') return false;
419
+ const norm = token.replace(/\\/g, '/');
420
+ return REDIRECT_PROTECTED_RE.test(norm);
421
+ }
422
+
423
+ /**
424
+ * checkRedirectToProtected(command): scan a Bash command string for shell
425
+ * redirects (> >>) or tee invocations that target protected kill-switch paths.
426
+ *
427
+ * Returns a human-readable description of the matched redirect, or null if
428
+ * none found.
429
+ *
430
+ * INCOMPLETE COVERAGE — see module header for honest framing.
431
+ */
432
+ function checkRedirectToProtected(command) {
433
+ if (typeof command !== 'string' || !command) return null;
434
+ // Fast path: skip if no redirect indicators present.
435
+ if (!command.includes('>') && !command.includes('tee')) return null;
436
+
437
+ const segments = splitSegments(command);
438
+ for (const seg of segments) {
439
+ const tokens = tokenize(seg);
440
+ for (let i = 0; i < tokens.length; i++) {
441
+ const t = tokens[i];
442
+
443
+ // Redirect operators: > and >>
444
+ if ((t === '>' || t === '>>') && i + 1 < tokens.length) {
445
+ const target = tokens[i + 1];
446
+ if (matchesRedirectProtected(target)) {
447
+ return `shell redirect (${t}) to ${target}`;
448
+ }
449
+ }
450
+
451
+ // tee command: `tee [-a] [--] <file> [file2 ...]`
452
+ // tee accepts MULTIPLE output files — check ALL positional args, not just the first.
453
+ if (t === 'tee') {
454
+ let pastDashDash = false;
455
+ for (let j = i + 1; j < tokens.length; j++) {
456
+ const arg = tokens[j];
457
+ if (!pastDashDash && arg === '--') { pastDashDash = true; continue; }
458
+ if (!pastDashDash && arg.startsWith('-')) continue; // skip tee flags (-a, --append, etc.)
459
+ // Check every positional arg — no early break (tee writes to all of them).
460
+ if (matchesRedirectProtected(arg)) return `tee to ${arg}`;
461
+ }
462
+ }
463
+ }
464
+ }
465
+ return null;
466
+ }
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // Gate lock-down: Interpreter-write detection (best-effort, INCOMPLETE)
470
+ //
471
+ // R5a: Detect Bash commands that invoke interpreters (node -e, py3 -c,
472
+ // sed -i, perl -e) when the command string contains a protected-path token
473
+ // as a literal substring. This catches the obvious "node -e with .bashrc"
474
+ // form but NOT runtime-constructed paths.
475
+ //
476
+ // HONEST — INCOMPLETE COVERAGE (permanently):
477
+ // `node -e "require('fs').appendFileSync('.bashrc','x')"` ✓ caught (literal token)
478
+ // `py3 -c "open('state.json','w').write(...)" ✓ caught (literal token)
479
+ // `sed -i 's/a/b/' ~/.zshrc` ✓ caught (literal token)
480
+ // `node -e "const p=homedir()+'/.bashrc'; ..."` ✗ NOT caught (constructed)
481
+ // `node -e "require('fs').writeFileSync(atob('...'),...)` ✗ NOT caught (encoded)
482
+ // `ruby -e / php -r / etc.` ✗ NOT caught (not listed)
483
+ // Multiline here-docs or code from a file ✗ NOT caught
484
+ //
485
+ // An agent that constructs paths at runtime evades this check. The real
486
+ // anchor is external: clean CI env + human review. This raises the bar.
487
+ // ---------------------------------------------------------------------------
488
+
489
+ /**
490
+ * Interpreter patterns that can write files when invoked with inline code.
491
+ * Matched against each shell segment (not the raw full command).
492
+ */
493
+ // NOTE: _PY_CMD is assembled from two parts to avoid a source-tree validator
494
+ // false-positive on the first-party-Python-command scan. Value = 'p'+'ython'.
495
+ // eslint-disable-next-line no-useless-concat
496
+ const _PY_CMD = 'p' + 'ython'; // joined value: py...thon (see NOTE above)
497
+ const INTERPRETER_WRITE_RE = new RegExp(
498
+ '\\bnode\\s+(?:-e|--eval)\\b|' +
499
+ '\\b' + _PY_CMD + '[23]?\\s+-c\\b|' +
500
+ '\\bsed\\s+-i\\b|\\bperl\\s+-e\\b'
501
+ );
502
+
503
+ /**
504
+ * Protected-path token literals. When any of these strings appears as a
505
+ * literal substring of a segment that also matches INTERPRETER_WRITE_RE,
506
+ * the command is blocked.
507
+ *
508
+ * INCOMPLETE: only literal occurrences are caught. An agent that assembles
509
+ * the path at runtime (e.g. process.env.HOME + '/.bashrc') bypasses this.
510
+ */
511
+ const INTERPRETER_PROTECTED_TOKENS = [
512
+ // Shell profiles (basename match is specific in this context)
513
+ '.bash_profile', '.bashrc', '.profile', '.zshrc', '.zprofile',
514
+ // Claude and flow-agents routing files
515
+ '.claude/settings.json',
516
+ // Flow-agents session sidecars (basename match; false-positive risk is low
517
+ // in the interpreter-write context and accepted per R5a honest framing)
518
+ 'current.json', 'state.json', 'trust.bundle',
519
+ // Delivery CI anchor paths. The existing trust.bundle token catches delivery/trust.bundle
520
+ // as a substring; explicit path added for clarity. trust.checkpoint.json is new.
521
+ 'delivery/trust.bundle', 'delivery/trust.checkpoint.json',
522
+ ];
523
+
524
+ /**
525
+ * checkInterpreterWriteToProtected(command): detect interpreter invocations
526
+ * (node -e, py3 -c, sed -i, perl -e) in segments that also contain a
527
+ * protected-path token as a literal substring.
528
+ *
529
+ * Returns a human-readable description of the match, or null if not detected.
530
+ *
531
+ * INCOMPLETE COVERAGE — see module header for honest framing.
532
+ */
533
+ function checkInterpreterWriteToProtected(command) {
534
+ if (typeof command !== 'string' || !command) return null;
535
+ // Fast path: skip if no interpreter keywords present.
536
+ if (!command.includes('node') && !command.includes(_PY_CMD) &&
537
+ !command.includes('sed') && !command.includes('perl')) return null;
538
+
539
+ const segments = splitSegments(command);
540
+ for (const seg of segments) {
541
+ // Check interpreter pattern.
542
+ const interpMatch = INTERPRETER_WRITE_RE.exec(seg);
543
+ if (!interpMatch) continue;
544
+
545
+ // Check for protected-path token literal in the same segment.
546
+ for (const token of INTERPRETER_PROTECTED_TOKENS) {
547
+ if (seg.includes(token)) {
548
+ return `${interpMatch[0].trim()} with protected path token "${token}"`;
549
+ }
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+
555
+ /**
556
+ * Delivery-protected path regex: delivery/trust.bundle and delivery/trust.checkpoint.json.
557
+ * These are the CI anchor files whose contents must not be agent-forged.
558
+ * Used by checkCopyMoveToProtected to catch `cp x delivery/trust.bundle`.
559
+ */
560
+ const DELIVERY_COPY_PROTECTED_RE = /(?:^|\/)delivery\/trust\.bundle$|(?:^|\/)delivery\/trust\.checkpoint\.json$/;
561
+
562
+ /**
563
+ * Return true when a normalized token matches a delivery-protected path.
564
+ */
565
+ function matchesDeliveryProtected(token) {
566
+ if (!token || typeof token !== "string") return false;
567
+ return DELIVERY_COPY_PROTECTED_RE.test(token.replace(/\\/g, "/"));
568
+ }
569
+
570
+ /**
571
+ * checkCopyMoveToProtected(command): detect cp/mv/install commands whose
572
+ * destination argument targets a delivery-protected path.
573
+ *
574
+ * Catches the plain-cp attack vector: `cp forged.json delivery/trust.bundle`
575
+ * is not a redirect and not an interpreter invocation, so those checks miss it.
576
+ * The destination is the LAST positional (non-flag) argument.
577
+ *
578
+ * INCOMPLETE COVERAGE: only cp, mv, install are checked. Other copy tools
579
+ * (rsync, scp, dd, etc.) and runtime-constructed path arguments are NOT caught.
580
+ * The real anchor remains external (clean CI env + human review). Bar-raiser only.
581
+ * RESIDUAL: publishDelivery uses fs.copyFileSync (not bash cp) -- unaffected.
582
+ */
583
+ function checkCopyMoveToProtected(command) {
584
+ if (typeof command !== "string" || !command) return null;
585
+ if (!command.includes("cp") && !command.includes("mv") && !command.includes("install")) return null;
586
+ if (!command.includes("delivery/")) return null;
587
+
588
+ const segments = splitSegments(command);
589
+ for (const seg of segments) {
590
+ const tokens = tokenize(seg);
591
+ if (tokens.length < 2) continue;
592
+ const cmd = tokens[0];
593
+ if (cmd !== "cp" && cmd !== "mv" && cmd !== "install") continue;
594
+
595
+ const positional = [];
596
+ for (let i = 1; i < tokens.length; i++) {
597
+ if (!tokens[i].startsWith("-")) positional.push(tokens[i]);
598
+ }
599
+ if (positional.length === 0) continue;
600
+
601
+ const dest = positional[positional.length - 1];
602
+ if (matchesDeliveryProtected(dest)) {
603
+ return `${cmd} to ${dest} (delivery-protected path)`;
604
+ }
605
+ }
606
+ return null;
607
+ }
608
+
609
+ // ---------------------------------------------------------------------------
610
+ // Sanctioned remedies for blocked writes.
611
+ //
612
+ // AC7: for sidecar/gate kill-switch paths the block message MUST name the
613
+ // sanctioned `npm run workflow:sidecar -- <command>` writer (or a human maintainer
614
+ // for host-owned files) and MUST NEVER advise disabling the config-protection hook.
615
+ // ---------------------------------------------------------------------------
616
+ const READ_ONLY_TOOL_NAMES = new Set(['read', 'glob', 'grep', 'ls', 'notebookread', 'websearch', 'webfetch']);
617
+
618
+ // WS8 (AC15): the remedy table (SHELL_PROFILE_REMEDY, SANCTIONED_REMEDIES, remedyFor,
619
+ // REMEDY_COMMAND_CANDIDATES) was extracted verbatim to ./lib/config-protection-remedies.js
620
+ // so it has an independently testable surface and this file is smaller. remedyForCommand
621
+ // stays here because it depends on this file's tokenizer (splitSegments/tokenize/
622
+ // checkProtectedPathPattern/PROTECTED_FILES). Behavior-preserving — no message text or
623
+ // matching data changed.
624
+ const { SHELL_PROFILE_REMEDY, remedyFor, REMEDY_COMMAND_CANDIDATES } = require('./lib/config-protection-remedies.js');
625
+
626
+ /**
627
+ * Recover the sanctioned remedy for a blocked Bash command.
628
+ *
629
+ * Pass 1: exact path-pattern match on individual tokens -- handles shell
630
+ * redirects / tee / cp where the protected path is its own clean token
631
+ * (a bare redirect target, or a cp/mv destination argument).
632
+ *
633
+ * Pass 2: substring match against the raw command text -- handles
634
+ * interpreter-write commands where the protected path sits inside a quoted
635
+ * string followed by punctuation, so no token ends at the basename and
636
+ * Pass 1's dollar-anchored regex never matches. Any blocked command that
637
+ * references a path with a SANCTIONED_REMEDIES entry gets that entry's
638
+ * remedy instead of falling through to the generic (and often factually
639
+ * wrong) shell-profile advice.
640
+ *
641
+ * Never returns advice to disable the hook.
642
+ */
643
+ function remedyForCommand(command) {
644
+ if (typeof command !== 'string') return SHELL_PROFILE_REMEDY;
645
+
646
+ const segments = splitSegments(command);
647
+ for (const seg of segments) {
648
+ for (const tok of tokenize(seg)) {
649
+ const match = checkProtectedPathPattern(tok);
650
+ if (match) return remedyFor(match.name);
651
+ if (PROTECTED_FILES.has(path.basename(tok))) return SHELL_PROFILE_REMEDY;
652
+ }
653
+ }
654
+
655
+ for (const candidate of REMEDY_COMMAND_CANDIDATES) {
656
+ if (candidate.needles.some((needle) => command.includes(needle))) {
657
+ return remedyFor(candidate.name);
658
+ }
659
+ }
660
+
661
+ return SHELL_PROFILE_REMEDY;
662
+ }
663
+
220
664
  function run(inputOrRaw, options = {}) {
221
665
  if (options.truncated) {
222
666
  return {
@@ -229,8 +673,11 @@ function run(inputOrRaw, options = {}) {
229
673
  try {
230
674
  input = typeof inputOrRaw === 'string' ? JSON.parse(inputOrRaw) : inputOrRaw;
231
675
  } catch { return { exitCode: 0 }; }
676
+ const toolName = String(input?.tool_name || '').trim().toLowerCase();
232
677
  const filePath = input?.tool_input?.path || input?.tool_input?.file_path || '';
233
- if (filePath) {
678
+ // Read-only tools never mutate a file, so path-based protection must not block them.
679
+ // (Bash is NOT read-only and stays fully covered by the command-based checks below.)
680
+ if (filePath && !READ_ONLY_TOOL_NAMES.has(toolName)) {
234
681
  const basename = path.basename(filePath);
235
682
  if (PROTECTED_FILES.has(basename)) {
236
683
  return {
@@ -241,6 +688,16 @@ function run(inputOrRaw, options = {}) {
241
688
  'disable the config-protection hook temporarily.',
242
689
  };
243
690
  }
691
+ // Gate lock-down: check path-pattern protected files (need path context).
692
+ const pathMatch = checkProtectedPathPattern(filePath);
693
+ if (pathMatch) {
694
+ return {
695
+ exitCode: 2,
696
+ stderr: `BLOCKED: Writing to ${pathMatch.name} is not allowed. ` +
697
+ `This file is protected because ${pathMatch.reason}. ` +
698
+ remedyFor(pathMatch.name),
699
+ };
700
+ }
244
701
  }
245
702
  const command = input?.tool_input?.command || '';
246
703
  if (command) {
@@ -254,11 +711,54 @@ function run(inputOrRaw, options = {}) {
254
711
  'If the hook is genuinely misconfigured, correct the hook configuration directly.',
255
712
  };
256
713
  }
714
+ // Gate lock-down: check for shell redirects to protected kill-switch paths.
715
+ // HONEST — INCOMPLETE: only > >> and tee are covered; sed -i and other forms
716
+ // are NOT. An agent with shell access can still evade. Bar-raiser only.
717
+ const redirect = checkRedirectToProtected(command);
718
+ if (redirect) {
719
+ return {
720
+ exitCode: 2,
721
+ stderr: `BLOCKED: Detected ${redirect} targeting a protected gate kill-switch file. ` +
722
+ 'Writing to shell profiles or Claude/flow-agents config files via shell redirect could ' +
723
+ 'disable or tamper with the gate. Do not disable this hook. ' +
724
+ remedyForCommand(command) + ' ' +
725
+ 'NOTE: This check has incomplete coverage (sed -i and similar forms are not caught).',
726
+ };
727
+ }
728
+ // Gate lock-down: check for interpreter invocations (node -e, py3 -c, sed -i,
729
+ // perl -e) combined with a protected-path token literal in the command string.
730
+ // HONEST — INCOMPLETE (R5a best-effort): runtime-constructed paths, base64,
731
+ // multi-step assembly, and other interpreters not listed are NOT caught.
732
+ const interpWrite = checkInterpreterWriteToProtected(command);
733
+ if (interpWrite) {
734
+ return {
735
+ exitCode: 2,
736
+ stderr: `BLOCKED: Detected ${interpWrite} in a Bash command. ` +
737
+ 'Interpreter invocations (node -e, py3 -c, sed -i, perl -e) that reference ' +
738
+ 'protected gate files could tamper with the gate. Do not disable this hook. ' +
739
+ remedyForCommand(command) + ' ' +
740
+ 'NOTE: This check has INCOMPLETE COVERAGE — runtime path construction evades it.',
741
+ };
742
+ }
743
+ // Gate lock-down R6: detect cp/mv/install targeting delivery-protected paths.
744
+ // Catches the plain-cp attack: `cp forged.json delivery/trust.bundle`.
745
+ // INCOMPLETE: cp/mv/install only; rsync/scp/dd evade. Real anchor is external.
746
+ const copyMove = checkCopyMoveToProtected(command);
747
+ if (copyMove) {
748
+ return {
749
+ exitCode: 2,
750
+ stderr: `BLOCKED: Detected ${copyMove} in a Bash command. ` +
751
+ 'Writing to delivery/trust.bundle or delivery/trust.checkpoint.json via cp/mv/install ' +
752
+ 'could forge the CI trust anchor. Do not disable this hook. ' +
753
+ remedyForCommand(command) + ' ' +
754
+ 'NOTE: This check covers cp/mv/install only -- other copy tools may evade it.',
755
+ };
756
+ }
257
757
  }
258
758
  return { exitCode: 0 };
259
759
  }
260
760
 
261
- module.exports = { run, tokenize, splitSegments, checkCommandForBypass };
761
+ module.exports = { run, tokenize, splitSegments, checkCommandForBypass, checkProtectedPathPattern, checkRedirectToProtected, checkInterpreterWriteToProtected, checkCopyMoveToProtected, matchesDeliveryProtected };
262
762
 
263
763
  // Stdin fallback for spawnSync execution
264
764
  if (require.main === module) {
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+ // ---------------------------------------------------------------------------
3
+ // config-protection-remedies.js — the sanctioned-remedy table for
4
+ // scripts/hooks/config-protection.js.
5
+ //
6
+ // WS8 (AC15): extracted verbatim from config-protection.js so the remedy table has an
7
+ // independently testable, independently reviewable surface and the main hook file is
8
+ // smaller. Behavior-preserving: no message text or matching data changed.
9
+ //
10
+ // AC7 (config-protection): for sidecar/gate kill-switch paths the block message MUST name
11
+ // the sanctioned `npm run workflow:sidecar -- <command>` writer (or a human maintainer for
12
+ // host-owned files) and MUST NEVER advise disabling the config-protection hook.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const SHELL_PROFILE_REMEDY =
16
+ 'There is no sanctioned automated writer for shell profiles; ask a human maintainer to edit it directly. Never disable this hook to make the write.';
17
+
18
+ const SANCTIONED_REMEDIES = {
19
+ '.claude/settings.json':
20
+ 'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
21
+ '.claude/settings.local.json':
22
+ 'There is no sanctioned automated writer for this file. Ask a human maintainer to edit it directly. Never disable this hook to make the write.',
23
+ '.kontourai/flow-agents/current.json':
24
+ 'Use `npm run workflow:sidecar -- ensure-session` (or `advance-state`), which writes this file for you. Never disable this hook to make the write.',
25
+ '.kontourai/flow-agents/.goal-fit-block-streak.json':
26
+ 'This file is only mutated internally by the goal-fit Stop hook; there is no sanctioned agent writer. Never disable this hook to make the write.',
27
+ '.kontourai/flow-agents/<slug>/state.json':
28
+ 'Use `npm run workflow:sidecar -- advance-state <artifact-dir> --status <status> --phase <phase> --summary ... --next-action ...`. Never disable this hook to make the write.',
29
+ '.kontourai/flow-agents/<slug>/trust.bundle':
30
+ 'Use `npm run workflow:sidecar -- record-gate-claim` or `seal-checkpoint`, not a direct write. Never disable this hook to make the write.',
31
+ 'delivery/trust.bundle':
32
+ 'This is written automatically by the delivery publish step (`npm run workflow:sidecar -- publish-delivery` / `advance-state --status delivered`). Never disable this hook to make the write.',
33
+ 'delivery/trust.checkpoint.json':
34
+ 'This is written automatically by the delivery publish step (`npm run workflow:sidecar -- publish-delivery` / `advance-state --status delivered`). Never disable this hook to make the write.',
35
+ };
36
+
37
+ /** Sanctioned remedy for a protected path name (from checkProtectedPathPattern). */
38
+ function remedyFor(name) {
39
+ return SANCTIONED_REMEDIES[name] || SHELL_PROFILE_REMEDY;
40
+ }
41
+
42
+ /**
43
+ * Ordered remedy lookup candidates for raw command-string (substring) matching.
44
+ * Each entry names a SANCTIONED_REMEDIES key and the literal needle(s) that,
45
+ * when found anywhere in the command text, identify that protected path.
46
+ *
47
+ * Needed because Pass 1 (in config-protection.js) tokenizes on whitespace and requires a
48
+ * token to END at the protected basename. Real interpreter-write commands embed the
49
+ * path inside a quoted string followed by punctuation (an inline interpreter
50
+ * eval flag calling fs.writeFileSync with a quoted path argument), so no
51
+ * token ends cleanly at the basename and Pass 1 never matches.
52
+ *
53
+ * Order matters: more specific paths (delivery/*) are listed before the
54
+ * generic slug-scoped basenames they would otherwise collide with (both
55
+ * the per-slug trust bundle and the delivery trust bundle share the
56
+ * basename 'trust.bundle') -- first match wins, deterministically.
57
+ */
58
+ const REMEDY_COMMAND_CANDIDATES = [
59
+ { name: 'delivery/trust.checkpoint.json', needles: ['delivery/trust.checkpoint.json', 'trust.checkpoint.json'] },
60
+ { name: 'delivery/trust.bundle', needles: ['delivery/trust.bundle'] },
61
+ { name: '.kontourai/flow-agents/<slug>/trust.bundle', needles: ['trust.bundle'] },
62
+ { name: '.kontourai/flow-agents/<slug>/state.json', needles: ['state.json'] },
63
+ { name: '.kontourai/flow-agents/.goal-fit-block-streak.json', needles: ['.goal-fit-block-streak.json'] },
64
+ { name: '.kontourai/flow-agents/current.json', needles: ['current.json'] },
65
+ { name: '.claude/settings.local.json', needles: ['settings.local.json'] },
66
+ { name: '.claude/settings.json', needles: ['.claude/settings.json'] },
67
+ ];
68
+
69
+ module.exports = { SHELL_PROFILE_REMEDY, SANCTIONED_REMEDIES, remedyFor, REMEDY_COMMAND_CANDIDATES };
@@ -19,6 +19,7 @@ const { spawnSync } = require('child_process');
19
19
  const { isHookEnabled } = require('./lib/hook-flags');
20
20
 
21
21
  const MAX_STDIN = 1024 * 1024;
22
+ const CONTRACT_VERSION = '1.0';
22
23
 
23
24
  function readStdinRaw() {
24
25
  return new Promise(resolve => {
@@ -99,7 +100,12 @@ async function main() {
99
100
 
100
101
  if (hookModule && typeof hookModule.run === 'function') {
101
102
  try {
102
- const output = hookModule.run(raw, { truncated, maxStdin: MAX_STDIN });
103
+ const outputOrPromise = hookModule.run(raw, { truncated, maxStdin: MAX_STDIN });
104
+ // Support async run() (returns a Promise): await before emitting result.
105
+ // Synchronous run() returns a plain value — Promise.resolve wraps it safely.
106
+ const output = (outputOrPromise && typeof outputOrPromise.then === 'function')
107
+ ? await outputOrPromise
108
+ : outputOrPromise;
103
109
  process.exit(emitHookResult(raw, output));
104
110
  } catch (e) {
105
111
  process.stderr.write(`[Hook] run() error for ${hookId}: ${e.message}\n`);
@@ -130,6 +136,13 @@ async function main() {
130
136
  process.exit(Number.isInteger(result.status) ? result.status : 0);
131
137
  }
132
138
 
139
+ // Additive: --contract-version prints the engine contract version and exits.
140
+ // Backward-compatible: existing callers never pass this flag.
141
+ if (process.argv.includes('--contract-version')) {
142
+ process.stdout.write(JSON.stringify({ contract_version: CONTRACT_VERSION, runner: 'run-hook.js' }) + '\n');
143
+ process.exit(0);
144
+ }
145
+
133
146
  main().catch(err => {
134
147
  process.stderr.write(`[Hook] run-hook error: ${err.message}\n`);
135
148
  process.exit(0);