@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
@@ -4,18 +4,20 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
6
  const KONTOURAI_DIR = '.kontourai';
7
- const LEGACY_FLOW_AGENTS_DIR = '.flow-agents';
7
+ const FLOW_AGENTS_RUNTIME_SUBDIR = 'flow-agents';
8
+ const FLOW_AGENTS_RUNTIME_DIR = `${KONTOURAI_DIR}/${FLOW_AGENTS_RUNTIME_SUBDIR}`;
9
+ const DURABLE_FLOW_AGENTS_DIR = '.flow-agents';
8
10
 
9
11
  function flowAgentsArtifactRoot(cwd = process.cwd()) {
10
- return path.resolve(cwd, KONTOURAI_DIR, 'flow-agents');
12
+ return path.resolve(cwd, FLOW_AGENTS_RUNTIME_DIR);
11
13
  }
12
14
 
13
- function legacyFlowAgentsArtifactRoot(cwd = process.cwd()) {
14
- return path.resolve(cwd, LEGACY_FLOW_AGENTS_DIR);
15
+ function durableFlowAgentsRoot(cwd = process.cwd()) {
16
+ return path.resolve(cwd, DURABLE_FLOW_AGENTS_DIR);
15
17
  }
16
18
 
17
19
  function flowAgentsArtifactRootsForRead(cwd = process.cwd()) {
18
- const roots = [flowAgentsArtifactRoot(cwd), legacyFlowAgentsArtifactRoot(cwd)];
20
+ const roots = [flowAgentsArtifactRoot(cwd)];
19
21
  return roots.filter((root, index) => roots.indexOf(root) === index && fs.existsSync(root));
20
22
  }
21
23
 
@@ -25,8 +27,12 @@ function defaultArtifactRootForRead(cwd = process.cwd()) {
25
27
  }
26
28
 
27
29
  module.exports = {
30
+ DURABLE_FLOW_AGENTS_DIR,
31
+ FLOW_AGENTS_RUNTIME_DIR,
32
+ FLOW_AGENTS_RUNTIME_SUBDIR,
33
+ KONTOURAI_DIR,
34
+ durableFlowAgentsRoot,
28
35
  flowAgentsArtifactRoot,
29
- legacyFlowAgentsArtifactRoot,
30
36
  flowAgentsArtifactRootsForRead,
31
37
  defaultArtifactRootForRead,
32
38
  };
@@ -66,9 +66,35 @@ async function main() {
66
66
  const [, , eventArg = 'unknown', agentName = 'dev'] = process.argv;
67
67
  const raw = await readStdinRaw();
68
68
  const payload = parseJson(raw);
69
+ const canonical = canonicalEvent(eventArg, payload);
69
70
  const telemetryScript = path.resolve(__dirname, '..', 'telemetry', 'telemetry.sh');
70
71
 
71
- const result = spawnSync('bash', [telemetryScript, canonicalEvent(eventArg, payload), agentName], {
72
+ if (canonical === 'postToolUse') {
73
+ try {
74
+ const heartbeatResult = require('./lib/liveness-heartbeat').maybeEmitHeartbeat({
75
+ cwd: process.cwd(),
76
+ env: process.env,
77
+ });
78
+ const conflict = heartbeatResult && heartbeatResult.conflict;
79
+ // Degraded-runtime conflict surfacing (issue #320, AC4): this runtime's generated
80
+ // plugin/extension does not consume this wrapper's stdout for context injection today
81
+ // (confirmed by reading the generated bundle's tool-result handler) — a disclosed,
82
+ // pre-existing gap (see plan Unresolved Questions), not a silent one. Until that
83
+ // structural gap is closed, emit a stderr diagnostic only; the next-turn
84
+ // workflow-steering.js liveness warning remains the fallback corrector. Guarded on a
85
+ // well-formed `conflict` shape so a malformed value degrades to no diagnostic, never a
86
+ // thrown error (AC8, fail-open).
87
+ if (conflict && typeof conflict.actor === 'string' && typeof conflict.lastAt === 'string') {
88
+ process.stderr.write(
89
+ `[OpencodeTelemetryHook] liveness conflict: actor "${conflict.actor}" claimed this subject at "${conflict.lastAt}" — no mid-turn injection available on this runtime (see docs/spec/runtime-hook-surface.md); relying on next-turn workflow-steering warning.\n`
90
+ );
91
+ }
92
+ } catch (err) {
93
+ process.stderr.write(`[OpencodeTelemetryHook] liveness heartbeat error: ${err.message}\n`);
94
+ }
95
+ }
96
+
97
+ const result = spawnSync('bash', [telemetryScript, canonical, agentName], {
72
98
  input: raw,
73
99
  encoding: 'utf8',
74
100
  cwd: process.cwd(),
@@ -70,9 +70,35 @@ async function main() {
70
70
  const [, , eventArg = 'unknown', agentName = 'dev'] = process.argv;
71
71
  const raw = await readStdinRaw();
72
72
  const payload = parseJson(raw);
73
+ const canonical = canonicalEvent(eventArg, payload);
73
74
  const telemetryScript = path.resolve(__dirname, '..', 'telemetry', 'telemetry.sh');
74
75
 
75
- const result = spawnSync('bash', [telemetryScript, canonicalEvent(eventArg, payload), agentName], {
76
+ if (canonical === 'postToolUse') {
77
+ try {
78
+ const heartbeatResult = require('./lib/liveness-heartbeat').maybeEmitHeartbeat({
79
+ cwd: process.cwd(),
80
+ env: process.env,
81
+ });
82
+ const conflict = heartbeatResult && heartbeatResult.conflict;
83
+ // Degraded-runtime conflict surfacing (issue #320, AC4): this runtime's generated
84
+ // plugin/extension does not consume this wrapper's stdout for context injection today
85
+ // (confirmed by reading the generated bundle's tool-result handler) — a disclosed,
86
+ // pre-existing gap (see plan Unresolved Questions), not a silent one. Until that
87
+ // structural gap is closed, emit a stderr diagnostic only; the next-turn
88
+ // workflow-steering.js liveness warning remains the fallback corrector. Guarded on a
89
+ // well-formed `conflict` shape so a malformed value degrades to no diagnostic, never a
90
+ // thrown error (AC8, fail-open).
91
+ if (conflict && typeof conflict.actor === 'string' && typeof conflict.lastAt === 'string') {
92
+ process.stderr.write(
93
+ `[PiTelemetryHook] liveness conflict: actor "${conflict.actor}" claimed this subject at "${conflict.lastAt}" — no mid-turn injection available on this runtime (see docs/spec/runtime-hook-surface.md); relying on next-turn workflow-steering warning.\n`
94
+ );
95
+ }
96
+ } catch (err) {
97
+ process.stderr.write(`[PiTelemetryHook] liveness heartbeat error: ${err.message}\n`);
98
+ }
99
+ }
100
+
101
+ const result = spawnSync('bash', [telemetryScript, canonical, agentName], {
76
102
  input: raw,
77
103
  encoding: 'utf8',
78
104
  cwd: process.cwd(),
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Stop Hook: warn when an active workflow is about to stop short of its goal.
4
4
  *
5
- * The hook reads .flow-agents artifacts, looks for the most recent active
5
+ * The hook reads .kontourai/flow-agents artifacts, looks for the most recent active
6
6
  * delivery/session file, and reports missing Definition Of Done, Goal Fit, or
7
7
  * Final Acceptance state.
8
8
  *
@@ -15,7 +15,7 @@
15
15
  * Code at L2) set block so the installed product enforces while the engine
16
16
  * default and conformance contract stay warn.
17
17
  *
18
- * Scope: the gate evaluates the session's current task (.flow-agents/current.json)
18
+ * Scope: the gate evaluates the session's current task (.kontourai/flow-agents/current.json)
19
19
  * when set, so an unrelated active workflow elsewhere in the repo does not gate
20
20
  * this stop. It also never hard-blocks a pre-execution (not-yet-started) task on
21
21
  * mere incompleteness — only genuine false-completion signals (a claimed pass the
@@ -315,6 +315,30 @@ function loadActiveFlowStep(flowAgentsDir) {
315
315
  }
316
316
  }
317
317
 
318
+ /**
319
+ * Message-assembly only: derive the user-facing gate identity prefix from the
320
+ * resolved active FlowDefinition step (loadActiveFlowStep). Produces
321
+ * "[<flowId>/<gateId>]" (e.g. "[builder.build/verify-gate]") when a
322
+ * FlowDefinition gate is active, else the generic "[stop-gate]" fallback.
323
+ * Purely cosmetic — never affects claim selection, HARD_BLOCK/FULL_BLOCK
324
+ * classification, block counting, or exit codes.
325
+ */
326
+ function gateLabel(activeFlowStep) {
327
+ return activeFlowStep && activeFlowStep.flowId && activeFlowStep.gateId
328
+ ? `[${activeFlowStep.flowId}/${activeFlowStep.gateId}]`
329
+ : '[stop-gate]';
330
+ }
331
+
332
+ /**
333
+ * Derive the declared claimType Set from an active FlowDefinition's gate expects[].
334
+ * Returns null when no FlowDefinition is active (callers fall back to workflow.* only).
335
+ */
336
+ function declaredClaimTypesFor(activeFlowStep) {
337
+ return activeFlowStep && Array.isArray(activeFlowStep.gateExpects)
338
+ ? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
339
+ : null;
340
+ }
341
+
318
342
  function safeOneLine(value, maxLength = 220) {
319
343
  const text = String(value || '').replace(/\s+/g, ' ').trim();
320
344
  if (text.length <= maxLength) return text;
@@ -527,9 +551,7 @@ function bundlePendingCriteriaCount(claims, declaredClaimTypes) {
527
551
  function sidecarGuidance(root, artifactDir, activeFlowStep) {
528
552
  // Build the declared claimType set from the FlowDefinition gate expects[] (P-c).
529
553
  // Null when no FlowDefinition is active (fallback: helpers use workflow.* prefix only).
530
- const declaredClaimTypes = activeFlowStep && Array.isArray(activeFlowStep.gateExpects)
531
- ? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
532
- : null;
554
+ const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
533
555
  const warnings = [];
534
556
  const state = readJsonFile(path.join(artifactDir, 'state.json'));
535
557
  const base = relative(root, artifactDir);
@@ -890,10 +912,39 @@ function verifyCommandLogChain(artifactDir) {
890
912
  * FLOW_AGENTS_GOAL_FIT_RECHECK=true (the RCE-risky opt-in path).
891
913
  * Returns { argv, cwd, source } or null when nothing trusted resolves.
892
914
  */
915
+ // WS8 (AC10b): a kind:"command" evidence ref's excerpt/command must be a literally runnable
916
+ // shell command, not a prose description of a manual verification step. This heuristic
917
+ // rejects prose so the goal-fit backstop never spawns `bash -lc "<a sentence>"` and
918
+ // misreports the resulting shell error as a caught false-completion. Pairs with the rule
919
+ // stated in context/contracts/planning-contract.md (AC11).
920
+ function isRunnableCommandText(text) {
921
+ const s = typeof text === 'string' ? text.trim() : '';
922
+ if (!s) return false;
923
+ // Sentence-like prose (a . ! or ? followed by more words) is not a single command.
924
+ if (/[.!?]\s+\S/.test(s)) return false;
925
+ const first = s.split(/\s+/)[0] || '';
926
+ // WS8 (AC10b, iteration 2): the first-token allowlist is broadened to the common
927
+ // real-world verify binaries a project's runnable evidence names (git, tsc, eslint,
928
+ // python/python3, docker, curl, jq, diff, grep, test) so honest command evidence is not
929
+ // misclassified as prose. Still fail-closed for genuinely non-command prose: a bare
930
+ // sentence whose first token is none of these (and carries no path prefix or shell
931
+ // metacharacter) is rejected rather than executed.
932
+ const knownBinary = /^(npm|npx|pnpm|yarn|node|bash|sh|zsh|make|just|task|cargo|go|pytest|tox|deno|bun|ruby|rake|mvn|gradle|dotnet|swift|flutter|dart|git|tsc|eslint|python|python3|docker|curl|jq|diff|grep|test)$/.test(first)
933
+ || first.startsWith('./') || first.startsWith('/') || first.includes('/');
934
+ const hasShellMeta = /[|&;<>()$`]/.test(s);
935
+ return knownBinary || hasShellMeta;
936
+ }
937
+
893
938
  function resolveTrustedCommand(root, artifactDir, check, acceptance) {
894
939
  // (a) acceptance criterion command for the matching criterion.
895
940
  const fromAcceptance = acceptanceCommandFor(check, acceptance);
896
- if (fromAcceptance) return { argv: ['bash', '-lc', fromAcceptance], cwd: root, source: 'acceptance' };
941
+ if (fromAcceptance) {
942
+ // WS8 (AC10b): never spawn a prose "excerpt" as bash. A kind:"command" ref whose text
943
+ // is not a runnable shell command is malformed-evidence — reported distinctly, not
944
+ // executed, and not conflated with a caught false-completion.
945
+ if (!isRunnableCommandText(fromAcceptance)) return { malformed: fromAcceptance };
946
+ return { argv: ['bash', '-lc', fromAcceptance], cwd: root, source: 'acceptance' };
947
+ }
897
948
 
898
949
  // (b) declared manifest target. Map the check command/id to a declared script.
899
950
  const declared = declaredManifestTarget(root, check);
@@ -1020,9 +1071,7 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
1020
1071
  // Build the declared claimType set from the FlowDefinition gate expects[] (P-c).
1021
1072
  // Null when no FlowDefinition is active (fallback: bundleClaimedPassCommandChecks
1022
1073
  // uses workflow.check.* prefix only — no regression for non-FlowDefinition sessions).
1023
- const declaredClaimTypes = activeFlowStep && Array.isArray(activeFlowStep.gateExpects)
1024
- ? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
1025
- : null;
1074
+ const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
1026
1075
  const bundle = readJsonFile(path.join(artifactDir, 'trust.bundle'));
1027
1076
  const acceptance = readJsonFile(path.join(artifactDir, 'acceptance.json'));
1028
1077
  const log = readLatestCommandLog(artifactDir); // Fix C: latest-wins; genuine fix-then-rerun-to-pass clears the block
@@ -1157,6 +1206,12 @@ function captureCrossReference(root, artifactDir, activeFlowStep) {
1157
1206
  continue;
1158
1207
  }
1159
1208
  const trusted = resolveTrustedCommand(root, artifactDir, check, acceptance);
1209
+ if (trusted && trusted.malformed) {
1210
+ // WS8 (AC10b): the matching acceptance criterion named a kind:"command" evidence ref
1211
+ // whose text is prose, not a runnable command. Do NOT execute it; classify it.
1212
+ warnings.push(`${base} evidence check ${id}: malformed-evidence — acceptance criterion names a kind:"command" evidence ref whose text is not a runnable shell command ("${safeOneLine(trusted.malformed, 120)}"); it was NOT executed. Use a literal runnable command, or record the check as not_verified/accepted_gap (see context/contracts/planning-contract.md).`);
1213
+ continue;
1214
+ }
1160
1215
  if (!trusted) {
1161
1216
  warnings.push(`${base} evidence check ${id}: claimed pass but NOT_VERIFIED — command "${safeOneLine(cmd, 120)}" was never captured and no trusted command (acceptance criterion / declared manifest target) resolves to re-run it. Set FLOW_AGENTS_GOAL_FIT_RECHECK=true to opt into re-running the model's free-form command.`);
1162
1217
  continue;
@@ -1360,9 +1415,7 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
1360
1415
  // When activeFlowStep is non-null, select claims whose claimType is in the
1361
1416
  // gate's declared set. When null, fall back to the existing workflow.* prefix
1362
1417
  // filter so no-FlowDefinition sessions are unaffected.
1363
- const declaredClaimTypes = activeFlowStep && Array.isArray(activeFlowStep.gateExpects)
1364
- ? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
1365
- : null;
1418
+ const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
1366
1419
 
1367
1420
  // SECURITY (Layer 2 — gate-bypass-chain fix): use UNION form instead of if/else.
1368
1421
  // With the old if/else, an empty declaredClaimTypes (Set{}) from a fake flow with
@@ -1473,9 +1526,9 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
1473
1526
  }
1474
1527
 
1475
1528
  /**
1476
- * Scope to the session's current task when .flow-agents/current.json points at
1529
+ * Scope to the session's current task when .kontourai/flow-agents/current.json points at
1477
1530
  * one (mirroring evidence-capture.js). Returns the slug dir, or null to fall back
1478
- * to scanning all of .flow-agents (newest-mtime).
1531
+ * to scanning all of .kontourai/flow-agents (newest-mtime).
1479
1532
  */
1480
1533
  function preferredArtifactDir(flowAgentsDir) {
1481
1534
  const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
@@ -1487,6 +1540,24 @@ function preferredArtifactDir(flowAgentsDir) {
1487
1540
  return dir.startsWith(flowAgentsDir + path.sep) && fs.existsSync(dir) ? dir : null;
1488
1541
  }
1489
1542
 
1543
+ /** WS8 (AC10a): a session dir is eligible as "active" only with real sidecar presence. */
1544
+ function hasSidecarPresence(artifactDir) {
1545
+ return fs.existsSync(path.join(artifactDir, 'state.json')) || fs.existsSync(path.join(artifactDir, 'trust.bundle'));
1546
+ }
1547
+
1548
+ // WS8 (AC10a): when current.json names a slug whose session directory does NOT exist,
1549
+ // return that slug so analyze() can log the staleness rather than silently falling back to
1550
+ // a global mtime scan that could resurface an abandoned/never-real session as active.
1551
+ function staleCurrentSlug(flowAgentsDir) {
1552
+ const current = readJsonFile(path.join(flowAgentsDir, 'current.json'));
1553
+ if (!current) return null;
1554
+ const slug = current.artifact_dir || current.active_slug;
1555
+ if (typeof slug !== 'string' || !slug.trim()) return null;
1556
+ const safe = slug.replace(/\.\.+/g, '').replace(/^[/\\]+/, '');
1557
+ const dir = path.join(flowAgentsDir, safe);
1558
+ return (dir.startsWith(flowAgentsDir + path.sep) && !fs.existsSync(dir)) ? slug : null;
1559
+ }
1560
+
1490
1561
  /**
1491
1562
  * A task is pre-execution (work not yet started) when its state.json status/phase
1492
1563
  * is still in the idea→planning band, or (no state.json) its markdown status is.
@@ -1516,9 +1587,7 @@ function isPreExecution(artifactDir, markdownStatus) {
1516
1587
  // ADR 0016 P-c: pass activeFlowStep so bundlePendingCriteriaCount includes declared types.
1517
1588
  function missingBundleOrStateSignal(artifactDir, activeFlowStep) {
1518
1589
  // Build the declared claimType set from the FlowDefinition gate expects[] (P-c).
1519
- const declaredClaimTypes = activeFlowStep && Array.isArray(activeFlowStep.gateExpects)
1520
- ? new Set(activeFlowStep.gateExpects.map(e => e && e.bundle_claim && e.bundle_claim.claimType).filter(Boolean))
1521
- : null;
1590
+ const declaredClaimTypes = declaredClaimTypesFor(activeFlowStep);
1522
1591
  const warnings = [];
1523
1592
  const hasBundle = fs.existsSync(path.join(artifactDir, 'trust.bundle'));
1524
1593
  const state = readJsonFile(path.join(artifactDir, 'state.json'));
@@ -1576,19 +1645,29 @@ function missingBundleOrStateSignal(artifactDir, activeFlowStep) {
1576
1645
  // MAX_BLOCKS hard-block guard (preventing auto-release of hard blocks).
1577
1646
  const HARD_BLOCK = /contradicts evidence\.json|caught false-completion|evidence verdict:|evidence check .+ status:|critique status|critique open|required sidecar is missing|command-log integrity check FAILED|gate misconfiguration:|exit-code-laundered/;
1578
1647
  // FULL_BLOCK adds: workflow-state hygiene, surface-unavailable fail-closed, missing log.
1579
- const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action|caught false-completion|NOT_VERIFIED —|command-log integrity check FAILED|gate misconfiguration:|surface unavailable —|expected capture log is missing|exit-code-laundered/;
1648
+ const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action|caught false-completion|NOT_VERIFIED —|command-log integrity check FAILED|gate misconfiguration:|surface unavailable —|expected capture log is missing|exit-code-laundered|malformed-evidence/;
1580
1649
 
1581
1650
  async function analyze(root, now = Date.now()) {
1582
1651
  const flowAgentsDirs = flowAgentsArtifactRootsForRead(root);
1583
1652
  // Scope to the session's current task when current.json names one, so an
1584
1653
  // unrelated active workflow elsewhere in the repo does not gate this stop.
1585
1654
  const scoped = flowAgentsDirs.map(preferredArtifactDir).find(Boolean);
1655
+ // WS8 (AC10a): if current.json points at a nonexistent slug, LOG the staleness and, in
1656
+ // the global fallback, require sidecar presence so a stale pointer cannot resurface an
1657
+ // abandoned/never-real markdown-only directory as "the active session".
1658
+ const staleSlug = scoped ? null : flowAgentsDirs.map(staleCurrentSlug).find(Boolean);
1659
+ if (staleSlug) {
1660
+ process.stderr.write(`[Hook] Goal Fit: current.json names slug "${safeOneLine(staleSlug, 80)}" but no such session directory exists — ignoring the stale pointer instead of resurfacing an abandoned session via a global mtime scan.\n`);
1661
+ }
1586
1662
  const searchDirs = scoped ? [scoped] : flowAgentsDirs;
1587
- const artifacts = searchDirs
1663
+ let artifacts = searchDirs
1588
1664
  .flatMap(dir => walkMarkdown(dir))
1589
1665
  .map(readArtifact)
1590
1666
  .filter(isWorkflowArtifact)
1591
1667
  .sort((a, b) => b.mtimeMs - a.mtimeMs);
1668
+ if (staleSlug) {
1669
+ artifacts = artifacts.filter(a => a && hasSidecarPresence(path.dirname(a.file)));
1670
+ }
1592
1671
 
1593
1672
  if (artifacts.length === 0) return { warnings: [], blocking: false };
1594
1673
 
@@ -1664,7 +1743,7 @@ async function analyze(root, now = Date.now()) {
1664
1743
  if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
1665
1744
  return blockRe.test(w);
1666
1745
  });
1667
- return { warnings, blocking, preExecution };
1746
+ return { warnings, blocking, preExecution, gatePrefix: gateLabel(activeFlowStep) };
1668
1747
  }
1669
1748
 
1670
1749
  /**
@@ -1717,6 +1796,52 @@ function bumpBlockStreak(root, hash) {
1717
1796
  return count;
1718
1797
  }
1719
1798
 
1799
+ /**
1800
+ * Message-assembly only: per-gap remediation guidance (linter-style " → " line)
1801
+ * appended under a matching warning. Fix-first / accept-second ordering per
1802
+ * class; the "accept" path always names a reason requirement and is never the
1803
+ * only or primary option. Does not affect which warnings are emitted, HARD_BLOCK/
1804
+ * FULL_BLOCK classification, block counting, or exit codes — text only.
1805
+ * Returns null when a warning does not match one of the four known classes.
1806
+ */
1807
+ function remediationFor(warning) {
1808
+ const w = String(warning || '');
1809
+
1810
+ // (1) claimed pass but the trusted re-run failed — reuse the exact command
1811
+ // already named in the warning text; never invent a different one.
1812
+ // NOTE: anchor on the quoted command substring + fixed tail, NOT on
1813
+ // `trusted backstop (<source>)` paren-matching. trusted.source can itself
1814
+ // contain parens (e.g. "model-command (FLOW_AGENTS_GOAL_FIT_RECHECK)"), and a
1815
+ // non-nesting `[^)]*` silently fails to match that nested-paren source,
1816
+ // dropping the guidance line for the RECHECK backstop path.
1817
+ const rerun = /re-run of "([\s\S]+?)" FAILED with exit \d+, contradicting the claimed pass/.exec(w);
1818
+ if (rerun) {
1819
+ return ` → run: ${rerun[1]} — fix the failures, then re-record the check`;
1820
+ }
1821
+
1822
+ // (2) claimed pass never captured (no trusted command resolved, backstop
1823
+ // disabled via FLOW_AGENTS_GOAL_FIT_BACKSTOP=skip, or the backstop could not launch).
1824
+ // Same nested-paren fix as (1): match on the invariant "could not run" tail
1825
+ // rather than paren-counting the `(<source>)` segment.
1826
+ if (/claimed pass but NOT_VERIFIED — command ".+?" was never captured/.test(w)
1827
+ || /claimed pass but NOT_VERIFIED — trusted backstop .*could not run/.test(w)) {
1828
+ return " → run the command in this session (evidence capture is active), then re-record the check with the exact command string via: npm run workflow:sidecar -- record-evidence <artifact-dir> --verdict <v> --check-json '...'";
1829
+ }
1830
+
1831
+ // (3) critique verdict fail / open findings.
1832
+ if (/critique status:/.test(w) || /critique open /.test(w)) {
1833
+ return ' → resolve the findings and record a passing critique (record-critique), or mark each finding accepted with a reason';
1834
+ }
1835
+
1836
+ // (4) evidence verdict fail / missing acceptance evidence.
1837
+ if (/evidence verdict:/.test(w) || /evidence check .+ status:/.test(w)
1838
+ || /evidence NOT_VERIFIED gap:/.test(w) || /Final Acceptance:/.test(w)) {
1839
+ return ' → fix the failing check, or record it as an accepted gap with justification in the session artifact';
1840
+ }
1841
+
1842
+ return null;
1843
+ }
1844
+
1720
1845
  async function run(rawInput) {
1721
1846
  const input = parseJson(rawInput);
1722
1847
  const root = findRepoRoot(input.cwd || process.cwd());
@@ -1728,9 +1853,15 @@ async function run(rawInput) {
1728
1853
  return rawInput;
1729
1854
  }
1730
1855
 
1856
+ const gatePrefix = result.gatePrefix || '[stop-gate]';
1731
1857
  const message = [
1732
- '[Hook] Goal Fit warning:',
1733
- ...result.warnings.map(w => ` - ${w}`),
1858
+ `${gatePrefix} Goal Fit warning:`,
1859
+ ...result.warnings.flatMap(w => {
1860
+ const lines = [` - ${w}`];
1861
+ const guidance = remediationFor(w);
1862
+ if (guidance) lines.push(guidance);
1863
+ return lines;
1864
+ }),
1734
1865
  ].join('\n');
1735
1866
 
1736
1867
  if (mode !== 'block' || !result.blocking) {
@@ -1746,7 +1877,7 @@ async function run(rawInput) {
1746
1877
  // N identical hard blocks to escape via the streak counter must not work.
1747
1878
  //
1748
1879
  // BAR-RAISER, NOT AIRTIGHT: an agent with shell access can still reset the streak
1749
- // by deleting .flow-agents/.goal-fit-block-streak.json or by modifying the warning
1880
+ // with runtime-constructed paths or by modifying the warning
1750
1881
  // text so the hash changes. The real anchor is external (signed checkpoints + human
1751
1882
  // review). This raises the cost of the burn-through-the-counter escape vector.
1752
1883
  const isHardBlock = result.warnings.some(w => {
@@ -1757,20 +1888,20 @@ async function run(rawInput) {
1757
1888
  // Do NOT clear the streak — keep accumulating so the same hard block stays visible.
1758
1889
  return {
1759
1890
  stdout: rawInput,
1760
- stderr: `${message}\n[Hook] Goal Fit: max-blocks reached but the block is a caught false-completion / integrity failure — not auto-releasing; requires a real fix or operator override.`,
1891
+ stderr: `${message}\n${gatePrefix} max-blocks reached but the block is a caught false-completion / integrity failure — not auto-releasing; requires a real fix or operator override.`,
1761
1892
  exitCode: 2,
1762
1893
  };
1763
1894
  }
1764
1895
  clearBlockStreak(root);
1765
1896
  return {
1766
1897
  stdout: rawInput,
1767
- stderr: `${message}\n[Hook] Goal Fit block RELEASED after ${count} consecutive identical blocks (FLOW_AGENTS_GOAL_FIT_MAX_BLOCKS=${maxBlocks}): the same gap persists, surfacing to the human instead of looping.`,
1898
+ stderr: `${message}\n${gatePrefix} released the same gap(s) blocked ${count}x without progress; needs your decision (listed above).`,
1768
1899
  exitCode: 0,
1769
1900
  };
1770
1901
  }
1771
1902
  return {
1772
1903
  stdout: rawInput,
1773
- stderr: `${message}\n[Hook] Goal Fit BLOCK ${count}/${maxBlocks}.`,
1904
+ stderr: `${message}\n${gatePrefix} Stop blocked ${result.warnings.length} evidence gap(s) (block ${count}; after ${maxBlocks} identical blocks I stop blocking and hand this to you)`,
1774
1905
  exitCode: 2,
1775
1906
  };
1776
1907
  }
@@ -17,6 +17,7 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
20
+ const { resolveActor } = require('./lib/actor-identity');
20
21
  const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
21
22
 
22
23
  const STEERING = {
@@ -205,6 +206,52 @@ function contextMapSteering(root) {
205
206
  ].join(' ');
206
207
  }
207
208
 
209
+ function promptText(input) {
210
+ const candidates = [
211
+ input && input.prompt,
212
+ input && input.user_prompt,
213
+ input && input.message,
214
+ input && input.text,
215
+ input && input.tool_input && input.tool_input.prompt,
216
+ ];
217
+ for (const candidate of candidates) {
218
+ if (typeof candidate === 'string' && candidate.trim()) return candidate;
219
+ }
220
+ return '';
221
+ }
222
+
223
+ function looksLikeBuilderWork(text) {
224
+ const normalized = safeStateText(text, 2000).toLowerCase();
225
+ if (!normalized) return false;
226
+ const readOnlyIntent = /\b(read[- ]only|review[- ]only|no (source |code |file )?(changes?|edits?|modifications?)|do not (modify|edit|change|write|fix|implement|touch)|don't (modify|edit|change|write|fix|implement|touch)|dont (modify|edit|change|write|fix|implement|touch))\b/.test(normalized);
227
+ if (readOnlyIntent) return false;
228
+ const questionOnlyIntent = /^(what|which|who|when|where|why|how)\b/.test(normalized);
229
+ if (questionOnlyIntent) return false;
230
+ const noCodeQuestion = /\b(explain|describe|summari[sz]e|what is|how does|how do i|why does|review|critique)\b/.test(normalized)
231
+ && !/\b(build|create|implement|ship|deliver|fix|debug|refactor|add|update|change|modify|wire|migrate|install|scaffold|write|code)\b/.test(normalized);
232
+ if (noCodeQuestion) return false;
233
+ const verificationOnlyIntent = /\b(validate|test|verify|check)\b/.test(normalized)
234
+ && !/\b(build|create|implement|ship|deliver|fix|debug|refactor|add|update|change|modify|wire|migrate|install|scaffold|write|code)\b/.test(normalized);
235
+ if (verificationOnlyIntent) return false;
236
+ const hasWorkVerb = /\b(build|create|implement|ship|deliver|fix|debug|refactor|add|update|change|modify|wire|migrate|install|scaffold|write|code)\b/.test(normalized);
237
+ const hasWorkObject = /\b(api|endpoint|component|feature|bug|failing test|regression|integration|hook|script|cli|config|schema|migration)\b/.test(normalized);
238
+ const hasRequestLanguage = /\b(can you|please|let's|lets|need|should|make|ensure|support|add|update|fix|work on)\b/.test(normalized);
239
+ return hasWorkVerb || (hasWorkObject && hasRequestLanguage);
240
+ }
241
+
242
+ function builderWorkflowSteering(input) {
243
+ if (!looksLikeBuilderWork(promptText(input))) return '';
244
+ return [
245
+ 'BUILDER WORKFLOW ROUTE: this user prompt looks like coding/build work.',
246
+ 'Builder lifecycle: shape raw ideas -> build selected work -> publish verified branch/PR -> learn correction feedback.',
247
+ 'Before source edits or implementation commands, activate Builder Kit delivery workflow.',
248
+ 'If the user explicitly requested TDD, activate `tdd-workflow`; otherwise activate `deliver` and keep the session on `builder.build`.',
249
+ 'Use `npm run workflow:sidecar -- ensure-session --flow-id builder.build ...` when the repo provides the sidecar writer.',
250
+ 'After local verification, continue to publish/release-readiness and learning-review; do not treat local verification as terminal delivery.',
251
+ 'Do not bypass plan-work -> execute-plan -> review-work -> verify-work for coding tasks; direct implementation is only acceptable when the user explicitly asks for no workflow or explanation-only help.',
252
+ ].join(' ');
253
+ }
254
+
208
255
  /**
209
256
  * Compose the RESUME block for SessionStart.
210
257
  *
@@ -297,15 +344,20 @@ function resumeSteering(root, current) {
297
344
 
298
345
  // Liveness advisory
299
346
  try {
347
+ const resolved = resolveActor(process.env);
348
+ const selfActor = resolved.actor || 'local';
300
349
  const events = flowAgentsArtifactRootsForRead(root)
301
350
  .flatMap(artifactRoot => readLivenessEvents(path.join(artifactRoot, 'liveness', 'events.jsonl')));
302
351
  if (events.length > 0) {
303
- const selfActor = (process.env.FLOW_AGENTS_ACTOR || '').trim() || 'local';
352
+ // selfActor (for exclusion matching) stays raw here; only the display strings pushed
353
+ // into `lines` are sanitized below, since holders come from the shared multi-writer
354
+ // liveness stream and must be treated as untrusted display input (#287 fix iteration 1).
304
355
  const holders = freshHolders(events, slug, selfActor, Date.now());
305
356
  for (const h of holders) {
306
- lines.push(`[LIVENESS WARNING: another agent appears live on this work: actor ${h.actor}, last seen ${h.lastAt}]`);
357
+ lines.push(`[LIVENESS WARNING: another agent appears live on this work: actor ${safeStateText(h.actor)}, last seen ${h.lastAt}]`);
307
358
  }
308
359
  }
360
+ lines.push(`ACTOR: ${safeStateText(selfActor)} (${resolved.source})`);
309
361
  } catch { /* skip */ }
310
362
 
311
363
  // Pull-work route hint
@@ -348,6 +400,15 @@ function run(rawInput) {
348
400
  }
349
401
  }
350
402
 
403
+ if (event === 'UserPromptSubmit') {
404
+ const builderHint = builderWorkflowSteering(input);
405
+ if (builderHint) {
406
+ hints.push(builderHint);
407
+ const contextHint = contextMapSteering(root);
408
+ if (contextHint) hints.push(contextHint);
409
+ }
410
+ }
411
+
351
412
  // SessionStart only: append the RESUME block for richer situational awareness
352
413
  if (event === 'SessionStart' && current) {
353
414
  const resumeBlock = resumeSteering(root, current);
@@ -379,4 +440,17 @@ if (require.main === module) {
379
440
  });
380
441
  }
381
442
 
382
- module.exports = { run, stateSteering, critiqueSteering, contextMapSteering, latestWorkflowState, findRepoRoot, safeStateText, stateNeedsAmbientSteering, resumeSteering };
443
+ module.exports = {
444
+ run,
445
+ stateSteering,
446
+ critiqueSteering,
447
+ contextMapSteering,
448
+ latestWorkflowState,
449
+ findRepoRoot,
450
+ safeStateText,
451
+ stateNeedsAmbientSteering,
452
+ resumeSteering,
453
+ promptText,
454
+ looksLikeBuilderWork,
455
+ builderWorkflowSteering,
456
+ };