codex-workflow-v2 2.0.0-beta.13.9 → 2.0.0-beta.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +98 -34
  3. package/dist/src/alpha6/adoption.d.ts +3 -1
  4. package/dist/src/alpha6/adoption.js +48 -8
  5. package/dist/src/alpha6/adoption.js.map +1 -1
  6. package/dist/src/alpha6/captured-check-evidence.d.ts +51 -0
  7. package/dist/src/alpha6/captured-check-evidence.js +152 -0
  8. package/dist/src/alpha6/captured-check-evidence.js.map +1 -0
  9. package/dist/src/alpha6/component-owner.d.ts +15 -1
  10. package/dist/src/alpha6/component-owner.js +44 -2
  11. package/dist/src/alpha6/component-owner.js.map +1 -1
  12. package/dist/src/alpha6/corrective-decision-boundary.d.ts +4 -0
  13. package/dist/src/alpha6/corrective-decision-boundary.js +53 -0
  14. package/dist/src/alpha6/corrective-decision-boundary.js.map +1 -0
  15. package/dist/src/alpha6/downstream-proof.d.ts +1 -25
  16. package/dist/src/alpha6/downstream-proof.js +0 -171
  17. package/dist/src/alpha6/downstream-proof.js.map +1 -1
  18. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  19. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  20. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  21. package/dist/src/alpha6/milestone.d.ts +2 -0
  22. package/dist/src/alpha6/milestone.js +10 -1
  23. package/dist/src/alpha6/milestone.js.map +1 -1
  24. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  25. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  26. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  27. package/dist/src/alpha6/plan-integrity.js +7 -5
  28. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  29. package/dist/src/alpha6/remediation.d.ts +8 -21
  30. package/dist/src/alpha6/remediation.js +225 -474
  31. package/dist/src/alpha6/remediation.js.map +1 -1
  32. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  33. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  34. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  35. package/dist/src/alpha7/autonomy.d.ts +9 -0
  36. package/dist/src/alpha7/autonomy.js +40 -1
  37. package/dist/src/alpha7/autonomy.js.map +1 -1
  38. package/dist/src/beta1/project-transaction.d.ts +3 -1
  39. package/dist/src/beta1/project-transaction.js +20 -6
  40. package/dist/src/beta1/project-transaction.js.map +1 -1
  41. package/dist/src/checks/runner.d.ts +26 -0
  42. package/dist/src/checks/runner.js +223 -0
  43. package/dist/src/checks/runner.js.map +1 -0
  44. package/dist/src/checks/task-sync.d.ts +10 -0
  45. package/dist/src/checks/task-sync.js +41 -0
  46. package/dist/src/checks/task-sync.js.map +1 -0
  47. package/dist/src/checks/worker.d.ts +1 -0
  48. package/dist/src/checks/worker.js +154 -0
  49. package/dist/src/checks/worker.js.map +1 -0
  50. package/dist/src/cli-actions.d.ts +3 -3
  51. package/dist/src/cli-actions.js +8 -15
  52. package/dist/src/cli-actions.js.map +1 -1
  53. package/dist/src/cli.js +43 -97
  54. package/dist/src/cli.js.map +1 -1
  55. package/dist/src/contracts.d.ts +54 -34
  56. package/dist/src/delegation.d.ts +5 -1
  57. package/dist/src/delegation.js +4 -0
  58. package/dist/src/delegation.js.map +1 -1
  59. package/dist/src/dependency-provenance.d.ts +2 -2
  60. package/dist/src/dependency-provenance.js +12 -69
  61. package/dist/src/dependency-provenance.js.map +1 -1
  62. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  63. package/dist/src/domain/base-sync-conflict.js +32 -0
  64. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  65. package/dist/src/domain/step-start-admission.d.ts +4 -0
  66. package/dist/src/domain/step-start-admission.js +9 -0
  67. package/dist/src/domain/step-start-admission.js.map +1 -0
  68. package/dist/src/domain/virgin-registration.d.ts +3 -0
  69. package/dist/src/domain/virgin-registration.js +65 -0
  70. package/dist/src/domain/virgin-registration.js.map +1 -0
  71. package/dist/src/errors.d.ts +1 -1
  72. package/dist/src/errors.js.map +1 -1
  73. package/dist/src/failed-checkpoint-update.d.ts +69 -0
  74. package/dist/src/failed-checkpoint-update.js +171 -0
  75. package/dist/src/failed-checkpoint-update.js.map +1 -0
  76. package/dist/src/gateway-handshake.js +0 -8
  77. package/dist/src/gateway-handshake.js.map +1 -1
  78. package/dist/src/git.d.ts +3 -1
  79. package/dist/src/git.js +41 -14
  80. package/dist/src/git.js.map +1 -1
  81. package/dist/src/graph.js +25 -3
  82. package/dist/src/graph.js.map +1 -1
  83. package/dist/src/index.d.ts +1 -0
  84. package/dist/src/lifecycle/corrective-replan-credential-core.d.ts +2 -0
  85. package/dist/src/lifecycle/corrective-replan-credential-core.js +45 -9
  86. package/dist/src/lifecycle/corrective-replan-credential-core.js.map +1 -1
  87. package/dist/src/lifecycle/corrective-replan.js +12 -8
  88. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  89. package/dist/src/lifecycle/semantic-registry.js +4 -2
  90. package/dist/src/lifecycle/semantic-registry.js.map +1 -1
  91. package/dist/src/navigation-actions.d.ts +9 -0
  92. package/dist/src/navigation-actions.js +72 -0
  93. package/dist/src/navigation-actions.js.map +1 -0
  94. package/dist/src/navigation-update-artifact.d.ts +5 -0
  95. package/dist/src/navigation-update-artifact.js +227 -0
  96. package/dist/src/navigation-update-artifact.js.map +1 -0
  97. package/dist/src/navigation-update.d.ts +39 -0
  98. package/dist/src/navigation-update.js +82 -0
  99. package/dist/src/navigation-update.js.map +1 -0
  100. package/dist/src/observation.js +52 -29
  101. package/dist/src/observation.js.map +1 -1
  102. package/dist/src/observed-routes.js +10 -6
  103. package/dist/src/observed-routes.js.map +1 -1
  104. package/dist/src/pending-review-update.d.ts +0 -13
  105. package/dist/src/pending-review-update.js +1 -6
  106. package/dist/src/pending-review-update.js.map +1 -1
  107. package/dist/src/reviewer.d.ts +1 -1
  108. package/dist/src/reviewer.js +20 -17
  109. package/dist/src/reviewer.js.map +1 -1
  110. package/dist/src/runtime.d.ts +3 -0
  111. package/dist/src/runtime.js +6 -0
  112. package/dist/src/runtime.js.map +1 -0
  113. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  114. package/dist/src/state/corrective-replan-executor.js +38 -1
  115. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  116. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  117. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  118. package/dist/src/state/corrective-replan-public.d.ts +9 -1
  119. package/dist/src/state/corrective-replan-public.js +50 -8
  120. package/dist/src/state/corrective-replan-public.js.map +1 -1
  121. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  122. package/dist/src/state/corrective-replan-transaction.js +12 -11
  123. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  124. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  125. package/dist/src/state/corrective-yield-transaction.js +11 -10
  126. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  127. package/dist/src/state/lock.d.ts +4 -0
  128. package/dist/src/state/lock.js +21 -0
  129. package/dist/src/state/lock.js.map +1 -1
  130. package/dist/src/version.d.ts +1 -1
  131. package/dist/src/version.js +1 -1
  132. package/dist/src/workflow-blocker-route.d.ts +8 -0
  133. package/dist/src/workflow-blocker-route.js +106 -0
  134. package/dist/src/workflow-blocker-route.js.map +1 -0
  135. package/dist/src/workflow.d.ts +88 -64
  136. package/dist/src/workflow.js +832 -1456
  137. package/dist/src/workflow.js.map +1 -1
  138. package/docs/autonomy-guardrails.md +26 -304
  139. package/docs/decisions.md +14 -112
  140. package/docs/development-flow.md +36 -237
  141. package/docs/project-memory.md +31 -50
  142. package/docs/release-app-evidence.md +190 -0
  143. package/docs/release.md +101 -388
  144. package/docs/updating-existing-project.md +65 -726
  145. package/package.json +11 -13
  146. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  147. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +88 -200
  148. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  149. package/plugins/codex-workflow-gateway/references/protocol.md +51 -458
  150. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +20 -4
  151. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +21 -34
  152. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  153. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +92 -778
  154. package/references/state-machine.md +7 -5
  155. package/roles/delivery-coordinator.md +31 -11
  156. package/roles/scope-lead.md +1 -1
  157. package/roles/technical-planner.md +5 -2
  158. package/schemas/adoption-posture-event.schema.json +11 -0
  159. package/schemas/authorization-event.schema.json +1 -1
  160. package/schemas/corrective-decision-event.schema.json +26 -4
  161. package/schemas/corrective-replan-credentials.private.schema.json +8 -5
  162. package/schemas/corrective-replan-execution-event.private.schema.json +9 -0
  163. package/schemas/corrective-replan-public.schema.json +2 -1
  164. package/schemas/delegation-grant.schema.json +1 -1
  165. package/schemas/project-knowledge-map.schema.json +3 -1
  166. package/schemas/remediation-event.schema.json +10 -1
  167. package/schemas/task.schema.json +64 -2
  168. package/schemas/transition-definition.schema.json +4 -0
  169. package/schemas/transition-payloads.schema.json +16 -1
  170. package/src/alpha6/adoption.ts +1146 -0
  171. package/src/alpha6/captured-check-evidence.ts +132 -0
  172. package/src/alpha6/check-support-anchor.ts +128 -0
  173. package/src/alpha6/component-owner.ts +168 -0
  174. package/src/alpha6/corrective-decision-boundary.ts +45 -0
  175. package/src/alpha6/downstream-proof.ts +520 -0
  176. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  177. package/src/alpha6/handoff.ts +1443 -0
  178. package/src/alpha6/journal.ts +473 -0
  179. package/src/alpha6/literal-test-invocation.ts +84 -0
  180. package/src/alpha6/mechanical-feasibility.ts +488 -0
  181. package/src/alpha6/milestone.ts +2192 -0
  182. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  183. package/src/alpha6/npm-check-contract.ts +47 -0
  184. package/src/alpha6/plan-integrity.ts +298 -0
  185. package/src/alpha6/plan-risk.ts +1480 -0
  186. package/src/alpha6/preexecution-replan.ts +187 -0
  187. package/src/alpha6/remediation-cause.ts +98 -0
  188. package/src/alpha6/remediation.ts +2505 -0
  189. package/src/alpha6/review.ts +1198 -0
  190. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  191. package/src/alpha6/store-sidecars.ts +335 -0
  192. package/src/alpha7/autonomy.ts +449 -0
  193. package/src/alpha7/corrective-recovery.ts +1332 -0
  194. package/src/artifacts.ts +130 -0
  195. package/src/beta1/project-transaction.ts +364 -0
  196. package/src/change-explanation.ts +153 -0
  197. package/src/checks/runner.ts +245 -0
  198. package/src/checks/task-sync.ts +41 -0
  199. package/src/checks/worker.ts +149 -0
  200. package/src/cli-actions.ts +112 -0
  201. package/src/cli.ts +1490 -0
  202. package/src/contracts.ts +1512 -0
  203. package/src/credential-output.ts +89 -0
  204. package/src/credential-transport.ts +215 -0
  205. package/src/delegation.ts +194 -0
  206. package/src/dependency-provenance.ts +472 -0
  207. package/src/diagnostics.ts +93 -0
  208. package/src/domain/base-sync-conflict.ts +35 -0
  209. package/src/domain/completed-step-carryover.ts +76 -0
  210. package/src/domain/discovery.ts +27 -0
  211. package/src/domain/plan-semantics.ts +58 -0
  212. package/src/domain/step-start-admission.ts +10 -0
  213. package/src/domain/validation.ts +177 -0
  214. package/src/domain/virgin-registration.ts +46 -0
  215. package/src/errors.ts +24 -0
  216. package/src/failed-checkpoint-update.ts +203 -0
  217. package/src/fs-utils.ts +61 -0
  218. package/src/gateway-handshake.ts +95 -0
  219. package/src/git.ts +183 -0
  220. package/src/graph.ts +342 -0
  221. package/src/historical-step-provenance.ts +136 -0
  222. package/src/index.ts +23 -0
  223. package/src/lifecycle/canonical-hash.ts +28 -0
  224. package/src/lifecycle/catalog.ts +202 -0
  225. package/src/lifecycle/compiler-inspection.ts +29 -0
  226. package/src/lifecycle/core-static-readiness.ts +132 -0
  227. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  228. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  229. package/src/lifecycle/corrective-replan-credential-core.ts +437 -0
  230. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  231. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  232. package/src/lifecycle/corrective-replan.ts +847 -0
  233. package/src/lifecycle/evaluator.ts +48 -0
  234. package/src/lifecycle/fingerprint.ts +488 -0
  235. package/src/lifecycle/immutable.ts +8 -0
  236. package/src/lifecycle/implementation-table.ts +118 -0
  237. package/src/lifecycle/index.ts +8 -0
  238. package/src/lifecycle/schema-artifact.ts +263 -0
  239. package/src/lifecycle/semantic-registry.ts +574 -0
  240. package/src/lifecycle/types.ts +838 -0
  241. package/src/memory.ts +273 -0
  242. package/src/migration.ts +161 -0
  243. package/src/navigation-actions.ts +70 -0
  244. package/src/navigation-update-artifact.ts +198 -0
  245. package/src/navigation-update.ts +121 -0
  246. package/src/observation.ts +225 -0
  247. package/src/observed-routes.ts +665 -0
  248. package/src/operational-contract.ts +125 -0
  249. package/src/pending-review-update.ts +175 -0
  250. package/src/repository.ts +99 -0
  251. package/src/reviewer.ts +1879 -0
  252. package/src/runtime.ts +6 -0
  253. package/src/state/corrective-replan-executor.ts +819 -0
  254. package/src/state/corrective-replan-public-schema.ts +83 -0
  255. package/src/state/corrective-replan-public.ts +952 -0
  256. package/src/state/corrective-replan-transaction.ts +949 -0
  257. package/src/state/corrective-yield-executor.ts +327 -0
  258. package/src/state/corrective-yield-transaction.ts +730 -0
  259. package/src/state/lock.ts +902 -0
  260. package/src/state/store.ts +567 -0
  261. package/src/transition-core.ts +330 -0
  262. package/src/ulid.ts +24 -0
  263. package/src/version.ts +2 -0
  264. package/src/workflow-blocker-route.ts +109 -0
  265. package/src/workflow.ts +10342 -0
  266. package/docs/alpha7.1-implementation-brief.md +0 -268
  267. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  268. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  269. package/docs/beta1-stabilization-brief.md +0 -165
  270. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  271. package/docs/beta13.2-signal-review-recovery.md +0 -38
  272. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  273. package/docs/change-model.md +0 -118
  274. package/docs/delegated-approval.md +0 -254
  275. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  276. package/docs/pdf/README.md +0 -24
  277. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  278. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  279. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  280. package/docs/pdf/requirements.txt +0 -1
  281. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  282. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
  283. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  284. package/docs/pending-review-update.md +0 -15
  285. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  286. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  287. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  288. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  289. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  290. package/docs/problem-briefs/README.md +0 -120
  291. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  292. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  293. package/docs/split-required-recovery.md +0 -47
  294. package/docs/stable-release-defect-register.md +0 -730
  295. package/docs/validation-report.md +0 -182
  296. package/scripts/generate-pdf-docs.py +0 -524
  297. package/scripts/run-pdf-docs.mjs +0 -62
@@ -0,0 +1,132 @@
1
+ import { lstatSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { StepFailurePause, TaskState } from '../contracts.js';
4
+ import { WorkflowError } from '../errors.js';
5
+ import type { CheckResult } from '../git.js';
6
+ import type { FileStateStore } from '../state/store.js';
7
+ import { canonicalJsonStringify, sha256Hex } from './store-sidecars.js';
8
+
9
+ const KIND = 'captured-checks/v1' as const;
10
+ const HASH = /^[a-f0-9]{64}$/;
11
+ function corrupt(): never {
12
+ // Never include captured output or parsing errors in public diagnostics.
13
+ throw new WorkflowError('STATE_CORRUPT', 'Captured check evidence is missing, unsafe, or does not match its binding.');
14
+ }
15
+ function inspect(file: string) {
16
+ try { return lstatSync(file); } catch (error) {
17
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
18
+ return corrupt();
19
+ }
20
+ }
21
+ function artifactFile(store: FileStateStore, task: TaskState, checksHash: string): string {
22
+ if (!HASH.test(checksHash)) corrupt();
23
+ const root = store.taskRoot(task.projectId, task.id);
24
+ const directory = path.join(root, 'check-results');
25
+ for (const item of [root, directory]) {
26
+ const stat = inspect(item);
27
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0)) corrupt();
28
+ }
29
+ return path.join(directory, `${checksHash}.json`);
30
+ }
31
+ function readBytes(file: string, maxBytes: number): string | null {
32
+ const stat = inspect(file);
33
+ if (!stat) return null;
34
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.size > maxBytes) corrupt();
35
+ try { return readFileSync(file, 'utf8'); } catch { return corrupt(); }
36
+ }
37
+ function validateChecks(value: unknown): asserts value is CheckResult[] {
38
+ if (!Array.isArray(value) || value.length === 0) corrupt();
39
+ for (const item of value) {
40
+ if (!item || typeof item !== 'object' || Array.isArray(item)
41
+ || Object.keys(item).sort().join(',') !== 'command,exitCode,output,passed'
42
+ || typeof item.command !== 'string' || typeof item.output !== 'string' || item.output.length > 12_000
43
+ || typeof item.passed !== 'boolean' || !Number.isInteger(item.exitCode)
44
+ || item.passed !== (item.exitCode === 0)) corrupt();
45
+ }
46
+ if (!value.some(item => !item.passed)) corrupt();
47
+ }
48
+
49
+ /** Prepare one target for the existing failed-Step transaction. Performs no writes. */
50
+ export function prepareCapturedCheckEvidence(store: FileStateStore, task: TaskState, checks: CheckResult[]) {
51
+ validateChecks(checks);
52
+ const canonical = canonicalJsonStringify(checks);
53
+ const checksHash = sha256Hex(canonical);
54
+ const content = `${canonical}\n`;
55
+ const file = artifactFile(store, task, checksHash);
56
+ const previous = readBytes(file, Buffer.byteLength(content));
57
+ if (previous !== null && previous !== content) corrupt();
58
+ return {
59
+ checksHash,
60
+ reference: { kind: KIND, artifactHash: sha256Hex(content) },
61
+ target: previous === null
62
+ ? { path: path.relative(store.projectRoot(task.projectId), file).split(path.sep).join('/'), content }
63
+ : null,
64
+ };
65
+ }
66
+
67
+ /** Exact readback for the transaction; captured bytes never escape in its return value. */
68
+ export function assertCapturedCheckEvidence(
69
+ store: FileStateStore, task: TaskState, pause: StepFailurePause, expected: CheckResult[],
70
+ ): void {
71
+ const checks = readVerifiedChecks(store, task, pause, expected.map(check => check.command));
72
+ if (!checks || canonicalJsonStringify(checks) !== canonicalJsonStringify(expected)) corrupt();
73
+ }
74
+
75
+ function readVerifiedChecks(store: FileStateStore, task: TaskState, pause: StepFailurePause, commands: string[]): CheckResult[] | null {
76
+ if (pause.checksEvidence === undefined) return null;
77
+ const ref = pause.checksEvidence;
78
+ if (!ref || Object.keys(ref).sort().join(',') !== 'artifactHash,kind'
79
+ || ref.kind !== KIND || !HASH.test(ref.artifactHash)) corrupt();
80
+ // JSON escaping can expand each captured character to six bytes.
81
+ const maxBytes = commands.reduce((size, command) => size + command.length * 6 + 12_000 * 6 + 256, 2);
82
+ const content = readBytes(artifactFile(store, task, pause.checksHash), maxBytes);
83
+ if (content === null || sha256Hex(content) !== ref.artifactHash) corrupt();
84
+ let value: unknown;
85
+ try { value = JSON.parse(content); } catch { return corrupt(); }
86
+ validateChecks(value);
87
+ const canonical = canonicalJsonStringify(value);
88
+ if (content !== `${canonical}\n` || sha256Hex(canonical) !== pause.checksHash
89
+ || JSON.stringify(value.map(check => check.command)) !== JSON.stringify(commands)) corrupt();
90
+ return value;
91
+ }
92
+
93
+ /** Verify native saved check evidence for admission without deriving authority from display diagnostics. */
94
+ export function assertCurrentCapturedCheckEvidence(store: FileStateStore, task: TaskState, stepId: string): void {
95
+ const step = task.steps.find(candidate => candidate.id === stepId);
96
+ if (!step?.failurePause || !readVerifiedChecks(store, task, step.failurePause, step.checks)) corrupt();
97
+ }
98
+
99
+ /** Best-effort diagnostic display only. Never used in cause bindings, scope, or navigation. */
100
+ export function capturedCheckExcerpt(output: string, sensitiveValues: readonly string[] = []): string[] {
101
+ let text = output.replace(/\u001b\[[0-9;]*m/g, '');
102
+ for (const value of sensitiveValues.filter(Boolean)) text = text.replaceAll(value, '[REDACTED]');
103
+ const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
104
+ const errors = lines.filter(line => /\b(?:error|failed|failure|exception|assertion|fatal|panic)\b|\bTS\d{3,5}\b/i.test(line));
105
+ return (errors.length ? errors : lines).slice(-8).map(line => {
106
+ // Prefer hiding the whole secret-bearing line over preserving surrounding error text.
107
+ if (/\b(?:authorization|bearer|password|passwd|secret|token|api[_-]?key|private[_-]?key)\b|-----BEGIN .*PRIVATE KEY/i.test(line)) {
108
+ return '[REDACTED sensitive diagnostic line]';
109
+ }
110
+ return line
111
+ .replace(/\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s]+/g, '[REDACTED_URL]')
112
+ .replace(/\b(?:CREF|DGT|WLT|CLAIM)-[^\s]+/g, '[REDACTED]')
113
+ .replace(/[A-Za-z0-9_+/=-]{40,}/g, '[REDACTED_VALUE]')
114
+ .slice(0, 240);
115
+ });
116
+ }
117
+
118
+ /** Read-only inspection of a current saved failure; no raw logs enter Task/status JSON. */
119
+ export function readCapturedCheckDiagnostics(store: FileStateStore, task: TaskState, stepId: string) {
120
+ const step = task.steps.find(candidate => candidate.id === stepId);
121
+ if (!step?.failurePause) return { state: 'unavailable' as const, reason: 'no-current-failure' as const };
122
+ const checks = readVerifiedChecks(store, task, step.failurePause, step.checks);
123
+ if (!checks) return { state: 'unavailable' as const, reason: 'legacy-not-captured' as const };
124
+ return {
125
+ state: 'verified' as const,
126
+ checksHash: step.failurePause.checksHash,
127
+ artifactHash: step.failurePause.checksEvidence!.artifactHash,
128
+ authority: 'diagnostic-only' as const,
129
+ capture: 'bounded-output-tail-last-12000-characters' as const,
130
+ failures: checks.flatMap((check, index) => check.passed ? [] : [{ index, exitCode: check.exitCode, excerpt: capturedCheckExcerpt(check.output) }]),
131
+ };
132
+ }
@@ -0,0 +1,128 @@
1
+ import path from 'node:path';
2
+ import type { PlanStepInput } from '../contracts.js';
3
+ import { pathAllowed } from '../domain/validation.js';
4
+ import { parseNpmRunCheck } from './npm-check-contract.js';
5
+
6
+ const SAFE_PATH = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
7
+ const RUNNER = /(?:^|[;&|]\s*|\s)(?:node|tsx)\s+([A-Za-z0-9._/-]+\.(?:cjs|js|mjs|ts|tsx))(?=\s|$)/g;
8
+ const IMPORT = /(?:\bfrom\s*|\bimport\s*)['"](\.[^'"]+)['"]/g;
9
+ const HASH_64 = /\b[a-f0-9]{64}\b/g;
10
+ const MIGRATION_NAME = /\b[0-9]{4}_[a-z0-9_]+\.sql\b/g;
11
+ const MIGRATION_BASENAME = /^[0-9]{4}_[a-z0-9_]+\.sql$/;
12
+
13
+ export interface CheckSupportAnchorConflict {
14
+ readonly reasonCode: 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES';
15
+ readonly script: string;
16
+ readonly command: string;
17
+ readonly scriptCommand: string;
18
+ readonly runnerPath: string;
19
+ readonly runnerHash: string;
20
+ readonly supportPath: string;
21
+ readonly supportHash: string;
22
+ readonly importSpecifier: string;
23
+ readonly expectedOutputPaths: readonly string[];
24
+ readonly anchoredMigrationNames: readonly string[];
25
+ }
26
+
27
+ export interface CheckSupportAnchorInspectionInput {
28
+ readonly step: Pick<PlanStepInput, 'checks' | 'expectedOutputs' | 'allowedWrites'>;
29
+ readonly scripts: ReadonlyMap<string, string>;
30
+ readonly readFile: (relativePath: string) => { content: string | null; hash: string | null };
31
+ readonly pathExists: (relativePath: string) => boolean;
32
+ }
33
+
34
+ export function inspectCheckSupportAnchors(
35
+ input: CheckSupportAnchorInspectionInput,
36
+ ): CheckSupportAnchorConflict[] {
37
+ const absentOutputs = input.step.expectedOutputs
38
+ .map(decodeDeclaredPath)
39
+ .filter((value): value is string => value !== null)
40
+ .filter((value) => pathAllowed(value, input.step.allowedWrites) && !input.pathExists(value));
41
+ const absentMigrationOutputs = absentOutputs.filter((value) => MIGRATION_BASENAME.test(path.posix.basename(value)));
42
+ if (absentMigrationOutputs.length === 0) return [];
43
+
44
+ const conflicts: CheckSupportAnchorConflict[] = [];
45
+ for (const check of input.step.checks) {
46
+ const invocation = parseNpmRunCheck(check);
47
+ if (!invocation) continue;
48
+ const scriptCommand = input.scripts.get(invocation.script);
49
+ if (!scriptCommand) continue;
50
+ for (const runnerPath of extractRunnerPaths(scriptCommand)) {
51
+ const runner = input.readFile(runnerPath);
52
+ if (!runner.content || !runner.hash) continue;
53
+ for (const importSpecifier of extractImports(runner.content)) {
54
+ const resolved = resolveImport(input.readFile, runnerPath, importSpecifier);
55
+ if (!resolved || pathAllowed(resolved.path, input.step.allowedWrites)) continue;
56
+ const anchoredMigrationNames = [...new Set(resolved.content.match(MIGRATION_NAME) ?? [])].sort();
57
+ if (
58
+ (resolved.content.match(HASH_64) ?? []).length === 0
59
+ || anchoredMigrationNames.length === 0
60
+ || !/(?:approved|expected|manifest|checksum|sha256|catalog)/i.test(resolved.content)
61
+ ) continue;
62
+ const expectedOutputPaths = absentMigrationOutputs.filter((output) => {
63
+ const basename = path.posix.basename(output);
64
+ return !anchoredMigrationNames.includes(basename)
65
+ && anchoredMigrationNames.some((name) => name.slice(-4) === basename.slice(-4));
66
+ });
67
+ if (expectedOutputPaths.length === 0) continue;
68
+ conflicts.push({
69
+ reasonCode: 'CHECK_SUPPORT_ANCHOR_OUTSIDE_ALLOWED_WRITES',
70
+ script: invocation.script,
71
+ command: check.trim(),
72
+ scriptCommand,
73
+ runnerPath,
74
+ runnerHash: runner.hash,
75
+ supportPath: resolved.path,
76
+ supportHash: resolved.hash,
77
+ importSpecifier,
78
+ expectedOutputPaths: [...expectedOutputPaths].sort(),
79
+ anchoredMigrationNames,
80
+ });
81
+ }
82
+ }
83
+ }
84
+ return conflicts.sort((left, right) => left.command.localeCompare(right.command)
85
+ || left.supportPath.localeCompare(right.supportPath));
86
+ }
87
+
88
+ function decodeDeclaredPath(value: string): string | null {
89
+ const candidate = value.startsWith('path:') ? value.slice(5) : value;
90
+ if (!SAFE_PATH.test(candidate) || candidate.split('/').some((segment) => segment === '.' || segment === '..')) {
91
+ return null;
92
+ }
93
+ return candidate;
94
+ }
95
+
96
+ function extractRunnerPaths(command: string): string[] {
97
+ const result = new Set<string>();
98
+ for (const match of command.matchAll(RUNNER)) {
99
+ const candidate = match[1]!;
100
+ if (!path.posix.isAbsolute(candidate) && !candidate.split('/').some((segment) => segment === '..')) {
101
+ result.add(candidate);
102
+ }
103
+ }
104
+ return [...result].sort();
105
+ }
106
+
107
+ function extractImports(content: string): string[] {
108
+ return [...new Set([...content.matchAll(IMPORT)].map((match) => match[1]!))].sort();
109
+ }
110
+
111
+ function resolveImport(
112
+ readFile: CheckSupportAnchorInspectionInput['readFile'],
113
+ runnerPath: string,
114
+ specifier: string,
115
+ ): { path: string; content: string; hash: string } | null {
116
+ const direct = path.posix.normalize(path.posix.join(path.posix.dirname(runnerPath), specifier));
117
+ if (direct.startsWith('../') || path.posix.isAbsolute(direct)) return null;
118
+ const candidates = [direct];
119
+ if (/\.js$/.test(direct)) candidates.push(direct.slice(0, -3) + '.ts');
120
+ if (!path.posix.extname(direct)) candidates.push(`${direct}.ts`, `${direct}.js`, `${direct}.mjs`);
121
+ for (const candidate of candidates) {
122
+ const inspected = readFile(candidate);
123
+ if (inspected.content !== null && inspected.hash !== null) {
124
+ return { path: candidate, content: inspected.content, hash: inspected.hash };
125
+ }
126
+ }
127
+ return null;
128
+ }
@@ -0,0 +1,168 @@
1
+ import { createHash } from 'node:crypto';
2
+ import path from 'node:path';
3
+ import type { PlanStepInput, StepFailurePause } from '../contracts.js';
4
+ import { pathAllowed } from '../domain/validation.js';
5
+ import { literalMigrationCatalogNames, literalVitestInvocations } from './literal-test-invocation.js';
6
+ import { parseNpmRunCheck } from './npm-check-contract.js';
7
+
8
+ const RUNNER = /(?:^|[;&|]\s*|\s)(?:node|tsx)\s+([A-Za-z0-9._/-]+\.(?:cjs|js|mjs|ts|tsx))(?=\s|$)/g;
9
+ const IMPORT = /(?:\bfrom\s*|\bimport\s*)['"](\.[^'"]+)['"]/g;
10
+
11
+ export interface FailedCheckComponentOwnerConflict {
12
+ reasonCode: 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES';
13
+ command: string;
14
+ runnerPath: string;
15
+ ownerPath: string;
16
+ ownerHash: string;
17
+ importChain: string[];
18
+ diagnosticPath: string;
19
+ componentRoot: string;
20
+ causeFingerprint: string;
21
+ testInvocation?: { kind: 'literal-vitest-catalog'; runnerHash: string; argv: string[]; migrationOutputs: string[]; catalogNames: string[] };
22
+ }
23
+
24
+ export function inspectFailedCheckComponentOwners(input: {
25
+ step: Pick<PlanStepInput, 'checks' | 'allowedWrites' | 'forbiddenScope'> & Partial<Pick<PlanStepInput, 'expectedOutputs'>> & { failurePause?: StepFailurePause | null };
26
+ scripts: ReadonlyMap<string, string>;
27
+ readFile: (relativePath: string) => { content: string | null; hash: string | null };
28
+ isTracked?: (relativePath: string) => boolean;
29
+ }): FailedCheckComponentOwnerConflict[] {
30
+ const failures = input.step.failurePause?.failedChecks ?? [];
31
+ const conflicts: FailedCheckComponentOwnerConflict[] = [];
32
+ for (const failure of failures) {
33
+ const invocation = parseNpmRunCheck(failure.command);
34
+ if (!invocation) continue;
35
+ const scriptCommand = input.scripts.get(invocation.script);
36
+ if (!scriptCommand) continue;
37
+ for (const runnerPath of extractRunnerPaths(scriptCommand)) {
38
+ const graph = buildImportGraph(runnerPath, input.readFile);
39
+ for (const diagnosticPath of failure.diagnosticPaths) {
40
+ let node = graph.get(diagnosticPath);
41
+ let testInvocation: FailedCheckComponentOwnerConflict['testInvocation'];
42
+ if (!node && /^(?:node|tsx) [A-Za-z0-9._/-]+\.(?:cjs|js|mjs|ts|tsx)$/.test(scriptCommand.trim())
43
+ && input.isTracked?.(runnerPath) && input.isTracked(diagnosticPath)
44
+ && !pathAllowed(diagnosticPath, input.step.forbiddenScope)) {
45
+ const runner = input.readFile(runnerPath), spec = input.readFile(diagnosticPath);
46
+ const argv = runner.content && literalVitestInvocations(runner.content).find(args => args.slice(2).includes(diagnosticPath));
47
+ const catalogNames = literalMigrationCatalogNames(spec.content ?? '');
48
+ const migrationOutputs = (input.step.expectedOutputs ?? []).map(value => value.startsWith('path:') ? value.slice(5) : value)
49
+ .filter(value => /^[A-Za-z0-9._/-]+$/.test(value) && !value.split('/').some(part => part === '..' || part === '.')
50
+ && /^[0-9]{4}_[a-z0-9_]+\.sql$/.test(path.posix.basename(value))
51
+ && componentBoundary(value) === componentBoundary(diagnosticPath)
52
+ && pathAllowed(value, input.step.allowedWrites)
53
+ && !catalogNames.includes(path.posix.basename(value))).sort();
54
+ if (argv && runner.hash && spec.hash && catalogNames.length && migrationOutputs.length) {
55
+ testInvocation = { kind: 'literal-vitest-catalog', runnerHash: runner.hash, argv, migrationOutputs, catalogNames };
56
+ node = { hash: spec.hash, chain: [runnerPath] };
57
+ }
58
+ }
59
+ if (!node || pathAllowed(diagnosticPath, input.step.allowedWrites)) continue;
60
+ const componentRoot = componentBoundary(diagnosticPath);
61
+ if (!input.step.allowedWrites.some((owned) => componentBoundary(owned) === componentRoot)) continue;
62
+ conflicts.push({
63
+ reasonCode: 'FAILED_CHECK_COMPONENT_OWNER_OUTSIDE_ALLOWED_WRITES',
64
+ command: failure.command,
65
+ runnerPath,
66
+ ownerPath: diagnosticPath,
67
+ ownerHash: node.hash,
68
+ importChain: node.chain,
69
+ diagnosticPath,
70
+ componentRoot,
71
+ causeFingerprint: failure.causeFingerprint,
72
+ ...(testInvocation ? { testInvocation } : {}),
73
+ });
74
+ }
75
+ }
76
+ }
77
+ // A diagnostic list may include passing suites. Only a unique missing-catalog
78
+ // anchor for this failed command qualifies; ambiguity grants no new scope.
79
+ const unambiguous = conflicts.filter(conflict => !conflict.testInvocation
80
+ || conflicts.filter(other => other.command === conflict.command && other.testInvocation).length === 1);
81
+ return unambiguous.sort((left, right) => left.command.localeCompare(right.command)
82
+ || left.ownerPath.localeCompare(right.ownerPath));
83
+ }
84
+
85
+ function buildImportGraph(
86
+ runnerPath: string,
87
+ readFile: (relativePath: string) => { content: string | null; hash: string | null },
88
+ ): Map<string, { hash: string; chain: string[] }> {
89
+ const result = new Map<string, { hash: string; chain: string[] }>();
90
+ const queue: Array<{ file: string; chain: string[] }> = [{ file: runnerPath, chain: [runnerPath] }];
91
+ while (queue.length > 0) {
92
+ const current = queue.shift()!;
93
+ if (result.has(current.file) || current.chain.length > 6) continue;
94
+ const inspected = readFile(current.file);
95
+ if (!inspected.content || !inspected.hash) continue;
96
+ result.set(current.file, { hash: inspected.hash, chain: current.chain });
97
+ for (const specifier of extractImports(inspected.content)) {
98
+ const resolved = resolveImport(current.file, specifier, readFile);
99
+ if (resolved && !result.has(resolved)) queue.push({ file: resolved, chain: [...current.chain, resolved] });
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+
105
+ function extractRunnerPaths(command: string): string[] {
106
+ return [...new Set([...command.matchAll(RUNNER)].map((match) => match[1]!))].sort();
107
+ }
108
+
109
+ function extractImports(content: string): string[] {
110
+ return [...new Set([...content.matchAll(IMPORT)].map((match) => match[1]!))].sort();
111
+ }
112
+
113
+ function resolveImport(
114
+ importer: string,
115
+ specifier: string,
116
+ readFile: (relativePath: string) => { content: string | null; hash: string | null },
117
+ ): string | null {
118
+ const direct = path.posix.normalize(path.posix.join(path.posix.dirname(importer), specifier));
119
+ if (direct.startsWith('../') || path.posix.isAbsolute(direct)) return null;
120
+ const candidates = [direct];
121
+ if (/\.js$/.test(direct)) candidates.push(direct.slice(0, -3) + '.ts');
122
+ if (!path.posix.extname(direct)) candidates.push(`${direct}.ts`, `${direct}.tsx`, `${direct}.js`, `${direct}.mjs`);
123
+ return candidates.find((candidate) => readFile(candidate).content !== null) ?? null;
124
+ }
125
+
126
+ function componentBoundary(relativePath: string): string {
127
+ const segments = relativePath.split('/');
128
+ if (segments.length >= 2 && ['apps', 'libs', 'packages', 'services', 'tools'].includes(segments[0]!)) {
129
+ return segments.slice(0, 2).join('/');
130
+ }
131
+ return segments[0]!;
132
+ }
133
+
134
+ export function semanticBoundaryHash(task: {
135
+ planObjective: string | null;
136
+ requirements: string[];
137
+ acceptance: string[];
138
+ risks: string[];
139
+ }): string {
140
+ const material = JSON.stringify({
141
+ objective: task.planObjective,
142
+ requirements: [...task.requirements].sort(),
143
+ acceptance: [...task.acceptance].sort(),
144
+ securityBoundary: task.risks.filter((risk) => /security|auth|secret|credential|permission/i.test(risk)).sort(),
145
+ });
146
+ return createHash('sha256').update(material).digest('hex');
147
+ }
148
+
149
+ /** Stored invocation evidence is structurally distinct from an import chain. The
150
+ * containing conflict hash binds it; fresh recovery rederives it from tracked bytes. */
151
+ export function validLiteralTestInvocationEvidence(conflict: {
152
+ ownerPath: string; testInvocation?: FailedCheckComponentOwnerConflict['testInvocation'];
153
+ }): boolean {
154
+ const proof = conflict.testInvocation;
155
+ if (!proof || proof.kind !== 'literal-vitest-catalog' || !/^[a-f0-9]{64}$/.test(proof.runnerHash)) return false;
156
+ const paths = proof.argv.slice(2).filter(arg => !arg.startsWith('--'));
157
+ return proof.argv[0] === 'vitest' && proof.argv[1] === 'run'
158
+ && paths.includes(conflict.ownerPath)
159
+ && paths.every(value => /^(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+\.(?:spec|test)\.[cm]?[jt]sx?$/.test(value))
160
+ && proof.argv.slice(2 + paths.length).every(arg => /^--maxWorkers=[1-9][0-9]*$/.test(arg))
161
+ && proof.argv.length - paths.length <= 3
162
+ && proof.catalogNames.length > 0 && proof.catalogNames.every(value => /^[0-9]{4}_[a-z0-9_]+\.sql$/.test(value))
163
+ && proof.migrationOutputs.length > 0 && proof.migrationOutputs.every(value =>
164
+ /^[A-Za-z0-9._/-]+$/.test(value) && !value.split('/').some(part => part === '..' || part === '.')
165
+ && /^[0-9]{4}_[a-z0-9_]+\.sql$/.test(path.posix.basename(value))
166
+ && componentBoundary(value) === componentBoundary(conflict.ownerPath)
167
+ && !proof.catalogNames.includes(path.posix.basename(value)));
168
+ }
@@ -0,0 +1,45 @@
1
+ import { lstatSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { TaskState } from '../contracts.js';
4
+ import { pathAllowed } from '../domain/validation.js';
5
+ import { validateTaskHistory } from '../git.js';
6
+ import { changedFiles, currentBranch, headCommit } from '../repository.js';
7
+ import type { FileStateStore } from '../state/store.js';
8
+ import { hashDirtyWorktree } from './downstream-proof.js';
9
+ import { effectiveStepAllowedWrites, readRemediationEvents } from './remediation.js';
10
+ import { readTaskC1Posture } from './handoff.js';
11
+ import { readFailedCheckpointDirtyTransport } from '../failed-checkpoint-update.js';
12
+
13
+ /** Checkout eligibility only: never an audit, writer credential, or permission to widen scope. */
14
+ export function correctiveDecisionCheckoutEligible(
15
+ store: FileStateStore, repository: string, task: TaskState, stepId: string,
16
+ ): boolean {
17
+ if (!task.taskBranch || currentBranch(repository) !== task.taskBranch) return false;
18
+ // Includes completed Step commits and recorded system/history commits. A new
19
+ // unrecorded HEAD must not become a corrective audit's implicit baseline.
20
+ validateTaskHistory(repository, task);
21
+ const dirty = changedFiles(repository).sort();
22
+ if (dirty.length === 0) return true;
23
+ if (!['local', 'external'].includes(task.workspaceOwner ?? '') || task.steps.some(step => step.status === 'in_progress')) return false;
24
+ if (task.workspaceOwner === 'external' && readTaskC1Posture(store, task).state !== 'claimed') return false;
25
+ const step = task.steps.find(candidate => candidate.id === stepId);
26
+ const pause = step?.failurePause;
27
+ if (step?.status !== 'failed' || !pause) return false;
28
+ const terminal = readRemediationEvents(store, task).filter(event => event.stepId === stepId).at(-1);
29
+ const transport = terminal?.failureEvidence.dirtyWorktreeHash
30
+ ? readFailedCheckpointDirtyTransport(store, repository, task, stepId,
31
+ terminal.failureEvidence.completionCommit, terminal.failureEvidence.dirtyWorktreeHash) : null;
32
+ if (!terminal || terminal.failureKind !== 'checks-failed'
33
+ || (transport?.targetHead ?? terminal.failureEvidence.completionCommit) !== headCommit(repository)
34
+ || terminal.failureEvidence.checksHash !== pause.checksHash
35
+ || terminal.failureEvidence.dirtyWorktreeHash !== pause.dirtyWorktreeHash
36
+ || JSON.stringify(dirty) !== JSON.stringify([...pause.dirtyFiles].sort())) return false;
37
+ const allowedWrites = effectiveStepAllowedWrites(store, task, stepId);
38
+ for (const file of dirty) {
39
+ if (!pathAllowed(file, allowedWrites) || pathAllowed(file, step.forbiddenScope)) return false;
40
+ // Do not admit symlinks (including dangling links) or directory substitutions.
41
+ try { if (!lstatSync(path.join(repository, file)).isFile()) return false; }
42
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
43
+ }
44
+ return hashDirtyWorktree(repository, dirty) === (transport?.targetDirtyHash ?? pause.dirtyWorktreeHash);
45
+ }