@kontourai/flow-agents 2.3.0 → 3.0.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 (301) hide show
  1. package/.github/CODEOWNERS +16 -0
  2. package/.github/workflows/ci.yml +44 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/AGENTS.md +4 -4
  5. package/CHANGELOG.md +62 -0
  6. package/CONTEXT.md +35 -0
  7. package/README.md +9 -3
  8. package/agents/dev.json +1 -1
  9. package/agents/tool-code-reviewer.json +1 -1
  10. package/agents/tool-planner.json +3 -3
  11. package/agents/tool-verifier.json +3 -3
  12. package/build/src/cli/assignment-provider.d.ts +1 -0
  13. package/build/src/cli/assignment-provider.js +748 -0
  14. package/build/src/cli/console-learning-projection.js +3 -2
  15. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  16. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  17. package/build/src/cli/init.js +104 -21
  18. package/build/src/cli/kit.js +37 -6
  19. package/build/src/cli/pull-work-provider.js +1 -1
  20. package/build/src/cli/usage-feedback.js +3 -3
  21. package/build/src/cli/validate-hook-influence.js +1 -0
  22. package/build/src/cli/validate-workflow-artifacts.js +27 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  24. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  25. package/build/src/cli/workflow-sidecar.js +938 -168
  26. package/build/src/cli.js +6 -0
  27. package/build/src/flow-kit/validate.d.ts +23 -0
  28. package/build/src/flow-kit/validate.js +67 -1
  29. package/build/src/index.d.ts +1 -1
  30. package/build/src/index.js +1 -1
  31. package/build/src/lib/flow-resolver.d.ts +6 -4
  32. package/build/src/lib/flow-resolver.js +125 -58
  33. package/build/src/lib/local-artifact-root.d.ts +14 -2
  34. package/build/src/lib/local-artifact-root.js +22 -5
  35. package/build/src/lib/workflow-learning-projection.js +2 -2
  36. package/build/src/runtime-adapters.d.ts +12 -0
  37. package/build/src/runtime-adapters.js +56 -16
  38. package/build/src/tools/build-universal-bundles.js +49 -16
  39. package/build/src/tools/generate-context-map.js +10 -8
  40. package/build/src/tools/validate-source-tree.js +24 -1
  41. package/console.telemetry.json +22 -0
  42. package/context/contracts/artifact-contract.md +40 -7
  43. package/context/contracts/assignment-provider-contract.md +239 -0
  44. package/context/contracts/builder-kit-workflow-state-contract.md +5 -2
  45. package/context/contracts/decision-registry-contract.md +265 -0
  46. package/context/contracts/delivery-contract.md +3 -1
  47. package/context/contracts/execution-contract.md +25 -0
  48. package/context/contracts/governance-adapter-contract.md +2 -0
  49. package/context/contracts/knowledge-store-contract.md +197 -0
  50. package/context/contracts/planning-contract.md +4 -1
  51. package/context/contracts/review-contract.md +7 -3
  52. package/context/contracts/sandbox-policy.md +2 -0
  53. package/context/contracts/standing-directives.md +13 -0
  54. package/context/contracts/verification-contract.md +11 -1
  55. package/context/contracts/work-item-contract.md +3 -1
  56. package/context/deferred/parallelization.md +1 -1
  57. package/context/gate-awareness.md +4 -4
  58. package/context/scripts/git-status.sh +2 -2
  59. package/context/scripts/hooks/config-protection.js +502 -2
  60. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  61. package/context/scripts/hooks/run-hook.js +14 -1
  62. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  63. package/context/scripts/hooks/workflow-steering.js +212 -6
  64. package/context/settings/assignment-provider-settings.json +33 -0
  65. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  66. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  67. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  68. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  69. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  70. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +463 -0
  71. package/docs/agent-system-guidebook.md +3 -3
  72. package/docs/agent-usage-feedback-loop.md +6 -6
  73. package/docs/context-map.md +10 -7
  74. package/docs/decisions/decision-records.md +57 -0
  75. package/docs/decisions/index.md +17 -0
  76. package/docs/decisions/knowledge-store-provider.md +51 -0
  77. package/docs/decisions/model-routing.md +63 -0
  78. package/docs/decisions/promotion-gate.md +52 -0
  79. package/docs/decisions/standing-directives.md +66 -0
  80. package/docs/developer-architecture.md +1 -1
  81. package/docs/fixture-ownership.md +4 -0
  82. package/docs/flow-kit-repository-contract.md +5 -5
  83. package/docs/getting-started.md +12 -2
  84. package/docs/integrations/harness-install.md +2 -1
  85. package/docs/kit-authoring-guide.md +64 -5
  86. package/docs/knowledge-kit.md +5 -1
  87. package/docs/learnings/2026-07-improvement-program.md +203 -0
  88. package/docs/migrations.md +6 -5
  89. package/docs/north-star.md +2 -2
  90. package/docs/operating-layers.md +4 -3
  91. package/docs/repository-structure.md +5 -5
  92. package/docs/skills-map.md +8 -8
  93. package/docs/spec/runtime-hook-surface.md +19 -15
  94. package/docs/spikes/graph-provider-2026-07.md +155 -0
  95. package/docs/standards-register.md +7 -7
  96. package/docs/trust-anchor-adoption.md +105 -4
  97. package/docs/work-item-adapters.md +1 -1
  98. package/docs/workflow-artifact-lifecycle.md +72 -9
  99. package/docs/workflow-eval-strategy.md +3 -3
  100. package/docs/workflow-shared-contracts.md +3 -2
  101. package/docs/workflow-usage-guide.md +260 -21
  102. package/evals/acceptance/DEMO-false-completion.md +20 -11
  103. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  104. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  105. package/evals/acceptance/test_kiro_harness.sh +1 -1
  106. package/evals/ci/antigaming-suite.sh +5 -0
  107. package/evals/ci/run-baseline.sh +71 -1
  108. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  109. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  110. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  111. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  112. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  113. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  114. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  115. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  116. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  117. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  118. package/evals/fixtures/hook-influence/cases.json +32 -0
  119. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  120. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  121. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  122. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  123. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  124. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  125. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  126. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  127. package/evals/integration/test_actor_identity.sh +254 -0
  128. package/evals/integration/test_assignment_provider_github.sh +318 -0
  129. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  130. package/evals/integration/test_builder_step_producers.sh +33 -23
  131. package/evals/integration/test_bundle_install.sh +22 -16
  132. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  133. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  134. package/evals/integration/test_checkpoint_signing.sh +43 -0
  135. package/evals/integration/test_claim_lookup.sh +4 -4
  136. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  137. package/evals/integration/test_command_log_concurrency.sh +8 -8
  138. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  139. package/evals/integration/test_command_log_integrity.sh +16 -16
  140. package/evals/integration/test_console_learning_projection.sh +1 -1
  141. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  142. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  143. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  144. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  145. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  146. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  147. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  148. package/evals/integration/test_flow_kit_repository.sh +2 -0
  149. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  150. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  151. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  152. package/evals/integration/test_gate_lockdown.sh +97 -73
  153. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  154. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  155. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  156. package/evals/integration/test_goal_fit_hook.sh +32 -32
  157. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  158. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  159. package/evals/integration/test_hook_influence_cases.sh +1 -0
  160. package/evals/integration/test_install_merge.sh +262 -1
  161. package/evals/integration/test_kit_identity_trust.sh +6 -6
  162. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  163. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  164. package/evals/integration/test_liveness_verdict.sh +394 -0
  165. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  166. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  167. package/evals/integration/test_promote_gate.sh +178 -0
  168. package/evals/integration/test_publish_delivery.sh +23 -6
  169. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  170. package/evals/integration/test_pull_work_liveness_preflight.sh +394 -0
  171. package/evals/integration/test_pull_work_provider.sh +2 -2
  172. package/evals/integration/test_reconcile_soundness.sh +37 -13
  173. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  174. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  175. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  176. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  177. package/evals/integration/test_trust_reconcile.sh +11 -10
  178. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  179. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  180. package/evals/integration/test_trust_reconcile_negatives.sh +759 -0
  181. package/evals/integration/test_usage_feedback_global.sh +5 -5
  182. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  183. package/evals/integration/test_verify_cli.sh +2 -2
  184. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  185. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  186. package/evals/integration/test_workflow_artifacts.sh +33 -0
  187. package/evals/integration/test_workflow_sidecar_writer.sh +802 -74
  188. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  189. package/evals/lib/codex-provider.sh +1 -1
  190. package/evals/lib/node.sh +19 -1
  191. package/evals/run.sh +32 -0
  192. package/evals/static/test_decisions.sh +150 -0
  193. package/evals/static/test_flowdef_codeowners_coverage.sh +62 -0
  194. package/evals/static/test_knowledge_providers.sh +23 -0
  195. package/evals/static/test_package.sh +19 -10
  196. package/evals/static/test_universal_bundles.sh +48 -3
  197. package/evals/static/test_workflow_skills.sh +41 -8
  198. package/kits/builder/flows/build.flow.json +3 -74
  199. package/kits/builder/flows/publish-learn.flow.json +90 -0
  200. package/kits/builder/kit.json +11 -0
  201. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  202. package/kits/builder/skills/deliver/SKILL.md +32 -11
  203. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  204. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  205. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  206. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  207. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  208. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  209. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  210. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  211. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  212. package/kits/builder/skills/pull-work/SKILL.md +148 -5
  213. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  214. package/kits/builder/skills/review-work/SKILL.md +24 -1
  215. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  216. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  217. package/kits/catalog.json +6 -0
  218. package/kits/knowledge/kit.json +35 -0
  219. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  220. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  221. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  222. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  223. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  224. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  225. package/kits/knowledge/providers/conformance/suite.test.js +125 -0
  226. package/kits/knowledge/providers/git-repo/index.js +236 -0
  227. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  228. package/kits/knowledge/providers/health/index.js +153 -0
  229. package/kits/knowledge/providers/index.js +24 -0
  230. package/kits/knowledge/providers/lib/model.js +91 -0
  231. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  232. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  233. package/kits/knowledge/providers/work-item/index.js +204 -0
  234. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  235. package/kits/veritas-governance/docs/README.md +75 -0
  236. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  237. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  238. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  239. package/kits/veritas-governance/kit.json +16 -0
  240. package/package.json +11 -5
  241. package/packaging/README.md +2 -2
  242. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  243. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  244. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  245. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  246. package/packaging/manifest.json +9 -10
  247. package/schemas/assignment-provider-settings.schema.json +125 -0
  248. package/schemas/decision-record.schema.json +109 -0
  249. package/schemas/knowledge/edge.schema.json +54 -0
  250. package/schemas/knowledge/health-report.schema.json +45 -0
  251. package/schemas/knowledge/node.schema.json +49 -0
  252. package/schemas/knowledge/proposal.schema.json +53 -0
  253. package/schemas/workflow-evidence.schema.json +1 -1
  254. package/schemas/workflow-state.schema.json +5 -0
  255. package/scripts/README.md +8 -3
  256. package/scripts/check-content-boundary.cjs +1 -1
  257. package/scripts/check-decisions.cjs +356 -0
  258. package/scripts/ci/derive-claim-status.mjs +74 -0
  259. package/scripts/ci/trust-reconcile.js +957 -103
  260. package/scripts/git-status.sh +2 -2
  261. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  262. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  263. package/scripts/hooks/config-protection.js +102 -30
  264. package/scripts/hooks/evidence-capture.js +3 -3
  265. package/scripts/hooks/lib/actor-identity.js +368 -0
  266. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  267. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  268. package/scripts/hooks/lib/liveness-policy.js +127 -0
  269. package/scripts/hooks/lib/liveness-read.js +105 -12
  270. package/scripts/hooks/lib/liveness-write.js +46 -0
  271. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  272. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  273. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  274. package/scripts/hooks/stop-goal-fit.js +157 -26
  275. package/scripts/hooks/workflow-steering.js +77 -3
  276. package/scripts/install-codex-home.sh +138 -38
  277. package/scripts/statusline/flow-agents-statusline.js +2 -2
  278. package/src/cli/assignment-provider.ts +845 -0
  279. package/src/cli/console-learning-projection.ts +3 -2
  280. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  281. package/src/cli/init.ts +101 -21
  282. package/src/cli/kit.ts +37 -6
  283. package/src/cli/public-api.test.mjs +58 -6
  284. package/src/cli/pull-work-provider.ts +1 -1
  285. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  286. package/src/cli/usage-feedback.ts +3 -3
  287. package/src/cli/validate-hook-influence.ts +1 -0
  288. package/src/cli/validate-workflow-artifacts.ts +27 -7
  289. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  290. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  291. package/src/cli/workflow-sidecar.ts +933 -168
  292. package/src/cli.ts +6 -0
  293. package/src/flow-kit/validate.ts +74 -1
  294. package/src/index.ts +6 -2
  295. package/src/lib/flow-resolver.ts +123 -55
  296. package/src/lib/local-artifact-root.ts +24 -5
  297. package/src/lib/workflow-learning-projection.ts +2 -2
  298. package/src/runtime-adapters.ts +61 -15
  299. package/src/tools/build-universal-bundles.ts +49 -16
  300. package/src/tools/generate-context-map.ts +10 -8
  301. package/src/tools/validate-source-tree.ts +24 -1
@@ -0,0 +1,87 @@
1
+ // trust-bundle-policy-order.test.mjs — WS8 (AC1, iteration 2) policy-cache order regression.
2
+ //
3
+ // The verifier found that ensurePolicy cached the first-seen requiredEvidence per legacy
4
+ // claimType, so two checks of the SAME kind that differ in command-presence (browser/security/
5
+ // runtime with vs without a command) collided: the first-recorded requiredEvidence won and
6
+ // corrupted the SECOND claim's derived status (verified -> proposed) depending on record order.
7
+ //
8
+ // This proves the fix: policy construction is keyed by (claimType, requiredEvidence), so two
9
+ // same-kind checks differing in command-presence produce IDENTICAL bundles in BOTH record
10
+ // orders (and each claim's derived status is order-independent).
11
+ //
12
+ // Run: `npm run test:unit`. Requires @kontourai/surface (the bundle producer's dependency).
13
+ import test from "node:test";
14
+ import assert from "node:assert/strict";
15
+
16
+ import { buildTrustBundle } from "../../build/src/cli/workflow-sidecar.js";
17
+
18
+ const TS = "2026-07-02T00:00:00Z";
19
+
20
+ // A command-backed browser check (-> test_output) and a no-command browser check
21
+ // (-> crawl_observation): SAME legacy claimType (workflow.check.browser), DIFFERENT
22
+ // requiredEvidence. This is the exact collision the verifier reproduced.
23
+ const checkCmd = { id: "browser-cmd", kind: "browser", status: "pass", summary: "e2e smoke via command", command: "npm run e2e" };
24
+ const checkNoCmd = { id: "browser-nocmd", kind: "browser", status: "pass", summary: "manual crawl observation" };
25
+
26
+ /** Deep-sort every array-of-objects-with-id so ordering never affects equality. */
27
+ function canonicalize(bundle) {
28
+ const clone = JSON.parse(JSON.stringify(bundle));
29
+ const byId = (a, b) => String(a.id).localeCompare(String(b.id));
30
+ for (const key of ["claims", "evidence", "events", "policies"]) {
31
+ if (Array.isArray(clone[key])) clone[key].sort(byId);
32
+ }
33
+ return clone;
34
+ }
35
+
36
+ function statusByClaimId(bundle) {
37
+ const m = {};
38
+ for (const c of bundle.claims) m[c.id] = c.status;
39
+ return m;
40
+ }
41
+
42
+ test("buildTrustBundle: two same-kind checks differing in command-presence produce identical bundles in both record orders", async () => {
43
+ const forward = await buildTrustBundle("perm", TS, [checkCmd, checkNoCmd], [], []);
44
+ const reverse = await buildTrustBundle("perm", TS, [checkNoCmd, checkCmd], [], []);
45
+
46
+ // Surface is a hard dependency of the producer; if it is genuinely unavailable the whole
47
+ // bundle-write path is skipped elsewhere, so a null here is a real environment failure.
48
+ assert.ok(forward, "buildTrustBundle returned null (is @kontourai/surface installed?)");
49
+ assert.ok(reverse, "buildTrustBundle returned null (is @kontourai/surface installed?)");
50
+
51
+ // The whole bundle is order-independent once canonicalized (arrays sorted by id).
52
+ assert.deepEqual(
53
+ canonicalize(forward),
54
+ canonicalize(reverse),
55
+ "bundles differ by record order — policy-cache collision has regressed",
56
+ );
57
+
58
+ // And, specifically, each claim's derived status is order-independent (the defect's symptom
59
+ // was the second same-kind claim flipping verified -> proposed depending on order).
60
+ assert.deepEqual(statusByClaimId(forward), statusByClaimId(reverse));
61
+ });
62
+
63
+ test("buildTrustBundle: same-kind checks with different command-presence get DISTINCT policies (not a colliding shared one)", async () => {
64
+ const bundle = await buildTrustBundle("perm", TS, [checkCmd, checkNoCmd], [], []);
65
+ assert.ok(bundle, "buildTrustBundle returned null (is @kontourai/surface installed?)");
66
+
67
+ const browserPolicies = bundle.policies.filter((p) => p.claimType === "workflow.check.browser");
68
+ // Two distinct required-evidence signatures -> two distinct policies (keyed, not merged).
69
+ assert.equal(browserPolicies.length, 2, "expected two distinct workflow.check.browser policies");
70
+ const reqSets = browserPolicies.map((p) => p.requiredEvidence.slice().sort().join(",")).sort();
71
+ assert.deepEqual(reqSets, ["crawl_observation", "test_output"]);
72
+
73
+ // requiredEvidence is all-of in Surface, so the sets must NOT have been merged into one
74
+ // over-constrained policy requiring BOTH types.
75
+ for (const p of browserPolicies) {
76
+ assert.equal(p.requiredEvidence.length, 1, `policy ${p.id} must require exactly one evidence type (not a merged union)`);
77
+ }
78
+
79
+ // Each claim references its own policy, and both derive 'verified' (neither corrupted).
80
+ const statuses = statusByClaimId(bundle);
81
+ const cmdClaim = bundle.claims.find((c) => c.subjectId === "perm/browser-cmd");
82
+ const noCmdClaim = bundle.claims.find((c) => c.subjectId === "perm/browser-nocmd");
83
+ assert.ok(cmdClaim && noCmdClaim);
84
+ assert.notEqual(cmdClaim.verificationPolicyId, noCmdClaim.verificationPolicyId);
85
+ assert.equal(statuses[cmdClaim.id], "verified");
86
+ assert.equal(statuses[noCmdClaim.id], "verified");
87
+ });
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
6
- import { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, telemetryDataDir, flowAgentsArtifactRoot, legacyFlowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
6
+ import { defaultArtifactRootForRead, defaultTelemetryDirForRead, defaultTelemetryDirsForRead, telemetryDataDir, flowAgentsArtifactRoot } from "../lib/local-artifact-root.js";
7
7
 
8
8
  const VALID_RESULTS = new Set(["success", "partial", "failure", "not_verified"]);
9
9
 
@@ -146,7 +146,7 @@ function syncArtifacts(argv: string[]): number {
146
146
  const dir = telemetryDir(flags);
147
147
  ensureSafeDir(dir);
148
148
  const artifacts = flagList(flags, "artifact-dir");
149
- const records = (artifacts.length ? artifacts : [flowAgentsArtifactRoot(), legacyFlowAgentsArtifactRoot()]).flatMap((item) => fs.existsSync(item) ? artifactOutcomes(item, flags) : []);
149
+ const records = (artifacts.length ? artifacts : [flowAgentsArtifactRoot()]).flatMap((item) => fs.existsSync(item) ? artifactOutcomes(item, flags) : []);
150
150
  writeJsonlUpsert(path.join(dir, "outcomes.jsonl"), records, "outcome_id");
151
151
  if (!flagBool(flags, "quiet")) console.log(`synced ${records.length} artifact outcome(s) to ${path.join(dir, "outcomes.jsonl")}`);
152
152
  return 0;
@@ -368,7 +368,7 @@ function syncProject(project: Record<string, unknown>, globalDir: string): void
368
368
  function discoverProjects(root: string): Record<string, unknown>[] {
369
369
  if (!fs.existsSync(root)) return [];
370
370
  const candidates = [root, ...fs.readdirSync(root).map((name) => path.join(root, name))];
371
- return candidates.filter((candidate) => fs.existsSync(flowAgentsArtifactRoot(candidate)) || fs.existsSync(legacyFlowAgentsArtifactRoot(candidate))).map((repoRoot) => {
371
+ return candidates.filter((candidate) => fs.existsSync(flowAgentsArtifactRoot(candidate))).map((repoRoot) => {
372
372
  const name = path.basename(repoRoot);
373
373
  return { name, repo: name, repo_root: repoRoot, artifact_dir: defaultArtifactRootForRead(repoRoot), input_telemetry_dir: defaultTelemetryDirForRead(repoRoot), runtime: "codex", skill_ids: [] };
374
374
  });
@@ -8,6 +8,7 @@ const validEvents = new Set(["UserPromptSubmit", "PostToolUse", "Stop"]);
8
8
  const requiredCases: Record<string, { description: string; tier: string; hook?: string; event?: string; must_include?: string[]; agent_must_do?: string[] }> = {
9
9
  "dev-builder-build-requires-pickup-probe-before-plan": { description: "missing pickup Probe before plan", tier: "design-target", must_include: ["design-probe", "accepted gaps", "provider_state", "conflict_risks"], agent_must_do: ["route decision_gap back to design-probe/pickup Probe"] },
10
10
  "dev-builder-review-before-verify-after-execute": { description: "review-before-verify after execute", tier: "adapter", event: "PostToolUse", must_include: ["Next: review", "then verify", "report only"], agent_must_do: ["review-work for report-only critique before verify-work", "not count critique.json as verification evidence"] },
11
+ "dev-builder-route-fresh-coding-prompt": { description: "fresh coding prompt routes into Builder workflow", tier: "adapter", event: "UserPromptSubmit", must_include: ["BUILDER WORKFLOW ROUTE", "activate `deliver`", "--flow-id builder.build", "plan-work -> execute-plan -> review-work -> verify-work", "publish/release-readiness and learning-review"], agent_must_do: ["activate Builder Kit delivery workflow before source edits", "use deliver by default for coding/build work", "do not bypass plan-work -> execute-plan -> review-work -> verify-work", "publish, release readiness, and learning feedback"] },
11
12
  "dev-verify-fail-preserves-trace-before-rework": { description: "verify failure route-back with preserved FAIL evidence", tier: "design-target", must_include: ["If verdict=FAIL: record the FAIL artifact"], agent_must_do: ["write the FAIL artifact before routing back", "route to plan"] },
12
13
  "codex-claude-strict-stop-adapter-contract": { description: "Goal Fit stop adapter behavior", tier: "adapter", hook: "stop-goal-fit", event: "Stop", must_include: ["Goal Fit warning"], agent_must_do: ["treat strict Stop guidance as a blocker"] },
13
14
  "codex-live-context-gap": { description: "Codex documented runtime gap", tier: "documented-runtime-gap", must_include: ["STATE:", "not_verified"], agent_must_do: ["record NOT_VERIFIED for live influence"] },
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
 
5
6
  type Issue = { path: string; message: string };
6
7
 
7
- const root = path.resolve(".");
8
+ // Resolve bundled JSON Schemas relative to this compiled script's own package
9
+ // location (build/src/cli/validate-workflow-artifacts.js -> ../../../schemas), NOT
10
+ // process.cwd(), so `flow-agents-validate-artifacts` works from any repo (the artifact
11
+ // paths to validate are still taken from argv/cwd). Mirrors the package-relative pattern
12
+ // used for liveness-read.js in workflow-sidecar.ts.
13
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
8
14
  const statusRe = /\[(PASS|FAIL|NOT_VERIFIED|SKIP|PARTIAL)\]/i;
9
15
  const dateTimeRe = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
10
16
  const sidecarSchemas: Record<string, string> = {
@@ -259,12 +265,13 @@ function readJson(file: string): { value: any | undefined; issues: Issue[] } {
259
265
  catch (error) { return { value: undefined, issues: [{ path: file, message: `invalid JSON: ${(error as Error).message}` }] }; }
260
266
  }
261
267
 
262
- function validateSidecar(file: string): Issue[] {
268
+ function validateSidecar(file: string): { issues: Issue[]; warnings: Issue[] } {
263
269
  const { value, issues } = readJson(file);
264
- if (value === undefined) return issues;
270
+ const warnings: Issue[] = [];
271
+ if (value === undefined) return { issues, warnings };
265
272
  const schemaFile = sidecarSchemas[path.basename(file)];
266
273
  if (schemaFile) {
267
- const schema = JSON.parse(readText(path.join(root, schemaFile)));
274
+ const schema = JSON.parse(readText(path.join(packageRoot, schemaFile)));
268
275
  validateSchemaValue(file, value, schema, path.basename(file), issues, schema);
269
276
  }
270
277
  if (path.basename(file) === "evidence.json") {
@@ -296,7 +303,10 @@ function validateSidecar(file: string): Issue[] {
296
303
  if (Array.isArray(value.post_deploy_checks) && value.post_deploy_checks.some((c: any) => !["planned", "pass"].includes(c.status))) issues.push({ path: file, message: "deploy decision requires post_deploy_checks to be planned or pass" });
297
304
  }
298
305
  }
299
- return issues;
306
+ if (path.basename(file) === "state.json" && value && typeof value === "object" && !Array.isArray(value) && !("branch" in value)) {
307
+ warnings.push({ path: file, message: "state.json has no branch field (legacy/pre-#289 session) — re-run `ensure-session` to backfill agent/<actor>/<slug>." });
308
+ }
309
+ return { issues, warnings };
300
310
  }
301
311
 
302
312
  function validateLearningCorrections(file: string, status: unknown, records: any[], issues: Issue[]): void {
@@ -385,7 +395,11 @@ function validateSidecarGroup(inputs: string[], markdown: string[], requireSidec
385
395
  if (bundleValue) {
386
396
  const claims = Array.isArray(bundleValue.claims) ? bundleValue.claims : [];
387
397
  const critiqueClaims = claims.filter((c: any) => c && c.claimType === "workflow.critique.review");
388
- if (critiqueClaims.some((c: any) => c.value === "fail" || c.status === "disputed")) issues.push({ path: trustBundlePath, message: "required critique must pass" });
398
+ // #282: a historical fail/disputed critique that has been explicitly superseded by a later
399
+ // resolving write (metadata.superseded_by) is retained structurally as history and does NOT
400
+ // block a top-level pass — only a LIVE (non-superseded) fail/disputed critique blocks.
401
+ const isSuperseded = (c: any) => c && c.metadata && typeof c.metadata === "object" && c.metadata.superseded_by;
402
+ if (critiqueClaims.some((c: any) => (c.value === "fail" || c.status === "disputed") && !isSuperseded(c))) issues.push({ path: trustBundlePath, message: "required critique must pass" });
389
403
  }
390
404
  }
391
405
  const acceptance = path.join(dir, "acceptance.json");
@@ -413,9 +427,15 @@ function main(): number {
413
427
  const markdown = artifactPaths(pathsIn);
414
428
  const sidecars = sidecarPaths(pathsIn);
415
429
  const issues: Issue[] = [];
430
+ const warnings: Issue[] = [];
416
431
  if (!skipMarkdown) for (const file of markdown) issues.push(...validateArtifact(file));
417
- for (const file of sidecars) issues.push(...validateSidecar(file));
432
+ for (const file of sidecars) {
433
+ const result = validateSidecar(file);
434
+ issues.push(...result.issues);
435
+ warnings.push(...result.warnings);
436
+ }
418
437
  issues.push(...validateSidecarGroup(pathsIn, markdown, requireSidecars, requireCritique));
438
+ for (const w of warnings) console.error(`WARN ${w.path}: ${w.message}`);
419
439
  if (issues.length) {
420
440
  for (const issue of issues) console.error(`${issue.path}: ${issue.message}`);
421
441
  return 1;
@@ -0,0 +1,267 @@
1
+ // veritas-readiness-adapter.test.mjs — direct node:test coverage for the Veritas Governance
2
+ // Kit adapter (kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs), specifically
3
+ // hasBlockingFailure() and buildReadinessTrustBundle().
4
+ //
5
+ // WS5 iteration-2 part 2: the adapter-vs-Veritas semantics fork is SETTLED (owner-ratified +
6
+ // investigation-confirmed, see .kontourai/flow-agents/ws5-governance-kit-slice1 session
7
+ // findings). The adapter's stricter blocking-failure derivation is CORRECT: it matches
8
+ // Veritas's own PRIVATE readinessHasBlockingFailure helper (veritas/src/surface/readiness.mjs)
9
+ // and Surface's weakest-link claim derivation (tests/surface/readiness-derived-claim.test.mjs
10
+ // in veritas). The adapter intentionally does NOT apply record.promotion_allowed as a
11
+ // short-circuit — promotion_allowed is a workstream-routing hint, not a safety signal, and
12
+ // Veritas's own EXPORTED readinessSurfaceStatus()/readinessVerdict() short-circuiting on it is
13
+ // a filed Veritas bug: kontourai/veritas#106
14
+ // (https://github.com/kontourai/veritas/issues/106). These tests pin the adapter's
15
+ // blocking-failure semantics as the durable contract, and include explicit parity-pinning
16
+ // cases (below) asserting the adapter verdict against the documented blocking-failure
17
+ // semantics. Once veritas#106 is fixed upstream, Veritas's exported functions will agree with
18
+ // this adapter (and with these tests) instead of diverging from them.
19
+ //
20
+ // GLOB NOTE: `npm run test:unit` only globs `src/cli/*.test.mjs` (see package.json). The
21
+ // adapter under test lives in `kits/veritas-governance/adapter/`, which is plain JS (not
22
+ // compiled by tsc — see tsconfig.json's `include: ["src/**/*.ts"]`), so it is imported here
23
+ // directly by relative path rather than from `build/`. This test file is placed under
24
+ // `src/cli/` — not colocated with the adapter under `kits/veritas-governance/` — purely so it
25
+ // is picked up by the existing test:unit glob; there is no other test runner wired for
26
+ // `kits/**`. If a kit-scoped test glob is added later, this file (or a copy) should move
27
+ // there instead.
28
+ //
29
+ // Run: `npm run test:unit`.
30
+
31
+ import test from "node:test";
32
+ import assert from "node:assert/strict";
33
+ import { readFileSync } from "node:fs";
34
+ import { fileURLToPath } from "node:url";
35
+ import path from "node:path";
36
+
37
+ import { hasBlockingFailure, buildReadinessTrustBundle } from "../../kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs";
38
+
39
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
40
+ const FIXTURES_DIR = path.resolve(__dirname, "../../kits/veritas-governance/fixtures/readiness");
41
+
42
+ function loadFixture(name) {
43
+ return JSON.parse(readFileSync(path.join(FIXTURES_DIR, name), "utf8"));
44
+ }
45
+
46
+ // ─── hasBlockingFailure — synthetic minimal records ────────────────────────────────────────
47
+
48
+ test("hasBlockingFailure: clean record (no blocking signal) returns false", () => {
49
+ assert.equal(hasBlockingFailure({}), false);
50
+ assert.equal(
51
+ hasBlockingFailure({
52
+ uncovered_path_result: "pass",
53
+ policy_results: [{ passed: true, enforcementLevel: "Require", rule_id: "r1" }],
54
+ selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: true } }],
55
+ external_tool_results: [{ verdict: "pass", blocking: true }],
56
+ }),
57
+ false,
58
+ );
59
+ });
60
+
61
+ test("hasBlockingFailure: uncovered_path_result === 'fail' is blocking", () => {
62
+ assert.equal(hasBlockingFailure({ uncovered_path_result: "fail" }), true);
63
+ });
64
+
65
+ test("hasBlockingFailure: a failed Require-enforcement policy result is blocking", () => {
66
+ assert.equal(
67
+ hasBlockingFailure({
68
+ policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-x" }],
69
+ }),
70
+ true,
71
+ );
72
+ });
73
+
74
+ test("hasBlockingFailure: a failed policy result with a non-Require enforcement level is NOT blocking", () => {
75
+ assert.equal(
76
+ hasBlockingFailure({
77
+ policy_results: [{ passed: false, enforcementLevel: "Recommend", rule_id: "recommend-x" }],
78
+ }),
79
+ false,
80
+ );
81
+ });
82
+
83
+ test("hasBlockingFailure: a failed selected evidence check is blocking", () => {
84
+ assert.equal(
85
+ hasBlockingFailure({
86
+ selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: false } }],
87
+ }),
88
+ true,
89
+ );
90
+ });
91
+
92
+ test("hasBlockingFailure: a blocking external-tool fail/missing verdict is blocking", () => {
93
+ assert.equal(hasBlockingFailure({ external_tool_results: [{ verdict: "fail", blocking: true }] }), true);
94
+ assert.equal(hasBlockingFailure({ external_tool_results: [{ verdict: "missing" }] }), true); // blocking defaults truthy (only `false` opts out)
95
+ });
96
+
97
+ test("hasBlockingFailure: a NON-blocking external-tool fail (blocking: false) is not blocking", () => {
98
+ assert.equal(hasBlockingFailure({ external_tool_results: [{ verdict: "fail", blocking: false }] }), false);
99
+ });
100
+
101
+ // ─── hasBlockingFailure — settled promotion_allowed non-application ───────────────────────
102
+ // SETTLED: unlike Veritas's own EXPORTED readinessSurfaceStatus()/readinessVerdict(), this
103
+ // adapter does NOT short-circuit on record.promotion_allowed. A record with
104
+ // promotion_allowed: true AND a failing Require rule is still treated as blocking by the
105
+ // adapter — this is correct: promotion_allowed is a workstream-routing hint (set only in
106
+ // Veritas's src/repo/routing.mjs resolveWorkstream() from file-pattern lane resolution), not a
107
+ // safety signal, and Veritas's own short-circuit on it is a filed bug (kontourai/veritas#106).
108
+ test("hasBlockingFailure: promotion_allowed=true does NOT suppress a blocking Require failure (settled semantics; veritas#106)", () => {
109
+ const record = {
110
+ promotion_allowed: true,
111
+ policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-veritas-cli-artifacts" }],
112
+ };
113
+ assert.equal(hasBlockingFailure(record), true, "adapter correctly ignores promotion_allowed — see header comment / session findings / veritas#106");
114
+ });
115
+
116
+ test("hasBlockingFailure: promotion_allowed=false with no blocking signal is not blocking (promotion_allowed is ignored either way)", () => {
117
+ const record = {
118
+ promotion_allowed: false,
119
+ uncovered_path_result: "pass",
120
+ policy_results: [{ passed: true, enforcementLevel: "Require", rule_id: "r1" }],
121
+ selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: true } }],
122
+ external_tool_results: [{ verdict: "pass", blocking: true }],
123
+ };
124
+ assert.equal(hasBlockingFailure(record), false);
125
+ });
126
+
127
+ // ─── buildReadinessTrustBundle — synthetic records ─────────────────────────────────────────
128
+
129
+ test("buildReadinessTrustBundle: a clean record yields a ready/verified software-readiness-verdict claim", async () => {
130
+ const record = {
131
+ run_id: "test-run-ready",
132
+ timestamp: "2026-07-02T00:00:00.000Z",
133
+ source_ref: "test-subject-ready",
134
+ policy_results: [],
135
+ selected_evidence_checks: [],
136
+ external_tool_results: [],
137
+ uncovered_path_result: "pass",
138
+ };
139
+ const { bundle, verdict, derivedStatus } = await buildReadinessTrustBundle(record);
140
+
141
+ assert.equal(verdict, "ready");
142
+ assert.equal(derivedStatus, "verified");
143
+ // bundle uses the facet-renamed schemaVersion 5 shape (hachure@0.9's strict schema): see
144
+ // https://github.com/kontourai/flow-agents/issues/281.
145
+ assert.equal(bundle.schemaVersion, 5);
146
+ assert.equal(bundle.source, "veritas-governance-kit/readiness-adapter");
147
+ assert.equal(bundle.claims.length, 1);
148
+ const claim = bundle.claims[0];
149
+ assert.equal(claim.claimType, "software-readiness-verdict");
150
+ assert.equal(claim.facet, "veritas.readiness");
151
+ assert.equal(claim.subjectId, "test-subject-ready");
152
+ assert.equal(claim.value.verdict, "ready");
153
+ assert.equal(claim.status, "verified");
154
+ assert.deepEqual(claim.value.blocking, { failedRequirements: [], failedEvidenceChecks: [] });
155
+ assert.equal(bundle.evidence[0].passing, true);
156
+ assert.equal(bundle.events[0].status, "verified");
157
+ });
158
+
159
+ test("buildReadinessTrustBundle: a record with a blocking Require failure yields a not-ready/disputed claim", async () => {
160
+ const record = {
161
+ run_id: "test-run-not-ready",
162
+ timestamp: "2026-07-02T00:00:00.000Z",
163
+ source_ref: "test-subject-not-ready",
164
+ policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-thing" }],
165
+ selected_evidence_checks: [],
166
+ external_tool_results: [],
167
+ uncovered_path_result: "pass",
168
+ };
169
+ const { bundle, verdict, derivedStatus } = await buildReadinessTrustBundle(record);
170
+
171
+ assert.equal(verdict, "not-ready");
172
+ assert.equal(derivedStatus, "disputed");
173
+ const claim = bundle.claims[0];
174
+ assert.equal(claim.value.verdict, "not-ready");
175
+ assert.equal(claim.status, "disputed");
176
+ assert.deepEqual(claim.value.blocking.failedRequirements, ["required-thing"]);
177
+ assert.equal(bundle.evidence[0].passing, false);
178
+ assert.equal(bundle.events[0].status, "disputed");
179
+ });
180
+
181
+ test("buildReadinessTrustBundle: --subject-id override wins over record source_ref/integrity.sourceRef", async () => {
182
+ const record = {
183
+ run_id: "test-run-subject",
184
+ timestamp: "2026-07-02T00:00:00.000Z",
185
+ source_ref: "record-source-ref",
186
+ policy_results: [],
187
+ };
188
+ const { bundle } = await buildReadinessTrustBundle(record, { subjectId: "explicit-subject" });
189
+ assert.equal(bundle.claims[0].subjectId, "explicit-subject");
190
+ });
191
+
192
+ // ─── buildReadinessTrustBundle — real captured fixtures (settled semantics) ────────────────
193
+ // The two committed fixtures are REAL captured `veritas readiness --check evidence
194
+ // --working-tree` output (see kits/veritas-governance/fixtures/readiness/*). Both have
195
+ // promotion_allowed: true; not-ready.readiness-report.json also has a failing Require rule.
196
+ // Veritas's own EXPORTED readinessSurfaceStatus() would (today) report BOTH as verified/ready
197
+ // because it short-circuits on promotion_allowed — that is the filed bug, kontourai/veritas#106
198
+ // (https://github.com/kontourai/veritas/issues/106). This adapter correctly disagrees on the
199
+ // not-ready fixture. Once #106 is fixed upstream, Veritas's exported functions will agree with
200
+ // the adapter (and with these tests) instead of diverging from them.
201
+
202
+ test("buildReadinessTrustBundle: ready.readiness-report.json fixture -> ready/verified", async () => {
203
+ const record = loadFixture("ready.readiness-report.json");
204
+ const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
205
+ assert.equal(verdict, "ready");
206
+ assert.equal(derivedStatus, "verified");
207
+ });
208
+
209
+ test("buildReadinessTrustBundle: not-ready.readiness-report.json fixture -> not-ready/disputed despite promotion_allowed=true (settled semantics; veritas#106)", async () => {
210
+ const record = loadFixture("not-ready.readiness-report.json");
211
+ assert.equal(record.promotion_allowed, true, "fixture precondition: promotion_allowed is true");
212
+ const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
213
+ assert.equal(verdict, "not-ready");
214
+ assert.equal(derivedStatus, "disputed");
215
+ });
216
+
217
+ // ─── Parity pinning (WS5 iteration-2 part 2) ───────────────────────────────────────────────
218
+ // Explicit parity assertions: for both shipped fixtures plus two synthetic records, the
219
+ // adapter verdict must match the documented blocking-failure semantics (README "Semantics"
220
+ // section / adapter header comment) regardless of record.promotion_allowed. Once
221
+ // kontourai/veritas#106 (https://github.com/kontourai/veritas/issues/106) is fixed, Veritas's
222
+ // own exported readinessSurfaceStatus()/readinessVerdict() will independently agree with these
223
+ // same verdicts instead of short-circuiting to verified/ready on promotion_allowed===true.
224
+
225
+ test("parity: ready.readiness-report.json fixture matches documented blocking-failure semantics -> ready", async () => {
226
+ const record = loadFixture("ready.readiness-report.json");
227
+ const { verdict } = await buildReadinessTrustBundle(record);
228
+ assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
229
+ assert.equal(verdict, "ready");
230
+ });
231
+
232
+ test("parity: not-ready.readiness-report.json fixture matches documented blocking-failure semantics -> not-ready (independent of promotion_allowed=true)", async () => {
233
+ const record = loadFixture("not-ready.readiness-report.json");
234
+ const { verdict } = await buildReadinessTrustBundle(record);
235
+ assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
236
+ assert.equal(verdict, "not-ready");
237
+ });
238
+
239
+ test("parity: synthetic promotion_allowed=true + failing Require matches documented blocking-failure semantics -> not-ready", async () => {
240
+ const record = {
241
+ run_id: "parity-true-failing",
242
+ promotion_allowed: true,
243
+ policy_results: [{ passed: false, enforcementLevel: "Require", rule_id: "required-parity-check" }],
244
+ selected_evidence_checks: [],
245
+ external_tool_results: [],
246
+ uncovered_path_result: "pass",
247
+ };
248
+ const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
249
+ assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
250
+ assert.equal(verdict, "not-ready");
251
+ assert.equal(derivedStatus, "disputed");
252
+ });
253
+
254
+ test("parity: synthetic promotion_allowed=false + clean record matches documented blocking-failure semantics -> ready", async () => {
255
+ const record = {
256
+ run_id: "parity-false-clean",
257
+ promotion_allowed: false,
258
+ policy_results: [{ passed: true, enforcementLevel: "Require", rule_id: "required-parity-check" }],
259
+ selected_evidence_checks: [{ id: "c1", evidence_check_result: { passed: true } }],
260
+ external_tool_results: [{ verdict: "pass", blocking: true }],
261
+ uncovered_path_result: "pass",
262
+ };
263
+ const { verdict, derivedStatus } = await buildReadinessTrustBundle(record);
264
+ assert.equal(verdict, hasBlockingFailure(record) ? "not-ready" : "ready");
265
+ assert.equal(verdict, "ready");
266
+ assert.equal(derivedStatus, "verified");
267
+ });
@@ -41,7 +41,7 @@ const ACTIVE_STATUSES = new Set([
41
41
  const KNOWN_LEARNING_STATUSES = new Set(["pending", "learned", "followup_required", "blocked"]);
42
42
  const KNOWN_LEARNING_ROUTE_TARGETS = new Set(["rule", "skill", "power", "agent", "eval", "doc", "backlog", "knowledge", "none"]);
43
43
  const KNOWN_LEARNING_ROUTE_STATUSES = new Set(["completed", "open", "deferred", "accepted", "rejected"]);
44
- const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history"]);
44
+ const SKIPPED_ROOT_ENTRIES = new Set(["archive", "changes", "delivery-history", "liveness"]);
45
45
  const MAX_SIDECAR_BYTES = 1024 * 1024;
46
46
 
47
47
  function printHelp(): void {
@@ -50,7 +50,7 @@ function printHelp(): void {
50
50
  console.log("Read-only dry-run audit for local workflow artifact directories.");
51
51
  console.log("");
52
52
  console.log("Options:");
53
- console.log(" --artifact-root <path> Local artifact root to scan (default: .kontourai/flow-agents, with .flow-agents read fallback)");
53
+ console.log(" --artifact-root <path> Local artifact root to scan (default: .kontourai/flow-agents)");
54
54
  console.log(" --json Print stable JSON buckets instead of text");
55
55
  console.log(" --help Show this help");
56
56
  console.log("");
@@ -160,6 +160,33 @@ function invalidItem(slug: string, workflowPath: string, reason: string): AuditI
160
160
  };
161
161
  }
162
162
 
163
+ // Promote-then-archive gate (issue #312). A delivered session's durable residue
164
+ // (decisions, vocabulary, learnings, doc updates) must be PROMOTED before the session
165
+ // is archived. Promotion is recorded as a session-local promotion claim in the session
166
+ // trust.bundle (workflow-sidecar promote), detectable here via claim.metadata.promotion
167
+ // — no new manifest entry required. A delivered/accepted session that reached a terminal
168
+ // shape WITHOUT that claim is a cleanup_candidate (blocked from archive) with a remedy
169
+ // naming the promote step. Already-`archived` sessions are past the gate and are never
170
+ // re-flagged (issue #312 non-goal: do not backfill already-archived sessions).
171
+ const PROMOTE_REMEDY =
172
+ "run `flow-agents workflow-sidecar promote <dir> --evidence-path <durable-doc> ...`" +
173
+ " (or `promote --none --reason \"<why nothing durable>\"`) to record the promotion claim before archiving";
174
+
175
+ function hasPromotionClaim(workflowDir: string): boolean {
176
+ const bundlePath = path.join(workflowDir, "trust.bundle");
177
+ if (!fs.existsSync(bundlePath)) return false;
178
+ const parsed = readJson(bundlePath, "trust.bundle");
179
+ if (!parsed.ok) return false;
180
+ const bundle = objectValue(parsed.value);
181
+ if (!bundle || !Array.isArray(bundle.claims)) return false;
182
+ return bundle.claims.some((entry) => {
183
+ const claim = objectValue(entry);
184
+ if (!claim) return false;
185
+ const meta = objectValue(claim.metadata);
186
+ return meta !== null && objectValue(meta.promotion) !== null;
187
+ });
188
+ }
189
+
163
190
  function classifyWorkflow(slug: string, workflowPath: string): AuditItem {
164
191
  const statePath = path.join(workflowPath, "state.json");
165
192
  if (!fs.existsSync(statePath)) return invalidItem(slug, workflowPath, "missing state.json");
@@ -193,9 +220,15 @@ function classifyWorkflow(slug: string, workflowPath: string): AuditItem {
193
220
  return { ...base, classification: "cleanup_candidate", reasons: ["verified workflow has next_action.status done"] };
194
221
  }
195
222
  if (["delivered", "accepted", "archived"].includes(status) && phase === "done") {
223
+ if (status !== "archived" && !hasPromotionClaim(workflowPath)) {
224
+ return { ...base, classification: "cleanup_candidate", reasons: [`${status} workflow reached phase done without a promotion claim; ${PROMOTE_REMEDY}`] };
225
+ }
196
226
  return { ...base, classification: "terminal_done", reasons: [`${status} workflow is in phase done`] };
197
227
  }
198
228
  if ((status === "accepted" || status === "archived") && learning.reasons.length === 0) {
229
+ if (status !== "archived" && !hasPromotionClaim(workflowPath)) {
230
+ return { ...base, classification: "cleanup_candidate", reasons: [`${status} workflow has closed learning routing but no promotion claim; ${PROMOTE_REMEDY}`] };
231
+ }
199
232
  return { ...base, classification: "terminal_done", reasons: [`${status} workflow has no open learning routing`] };
200
233
  }
201
234
  return { ...base, classification: "invalid", reasons: [`unrecognized lifecycle shape: status=${status}, phase=${phase}, next_action.status=${nextStatus ?? "missing"}`] };