codex-workflow-v2 2.0.0-beta.13.8 → 2.0.0-beta.14

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 (281) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +96 -32
  3. package/dist/src/alpha6/adoption.d.ts +2 -0
  4. package/dist/src/alpha6/adoption.js +22 -0
  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 +47 -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/failed-step-planning-recovery.d.ts +10 -0
  19. package/dist/src/alpha6/failed-step-planning-recovery.js +93 -0
  20. package/dist/src/alpha6/failed-step-planning-recovery.js.map +1 -0
  21. package/dist/src/alpha6/literal-test-invocation.d.ts +2 -0
  22. package/dist/src/alpha6/literal-test-invocation.js +106 -0
  23. package/dist/src/alpha6/literal-test-invocation.js.map +1 -0
  24. package/dist/src/alpha6/milestone.d.ts +2 -0
  25. package/dist/src/alpha6/milestone.js +10 -1
  26. package/dist/src/alpha6/milestone.js.map +1 -1
  27. package/dist/src/alpha6/navigation-dirty-carryover.d.ts +11 -0
  28. package/dist/src/alpha6/navigation-dirty-carryover.js +130 -0
  29. package/dist/src/alpha6/navigation-dirty-carryover.js.map +1 -0
  30. package/dist/src/alpha6/plan-integrity.js +7 -5
  31. package/dist/src/alpha6/plan-integrity.js.map +1 -1
  32. package/dist/src/alpha6/plan-risk.d.ts +2 -0
  33. package/dist/src/alpha6/plan-risk.js +10 -3
  34. package/dist/src/alpha6/plan-risk.js.map +1 -1
  35. package/dist/src/alpha6/remediation.d.ts +6 -21
  36. package/dist/src/alpha6/remediation.js +161 -476
  37. package/dist/src/alpha6/remediation.js.map +1 -1
  38. package/dist/src/alpha6/root-cause-replan-carryover.d.ts +4 -1
  39. package/dist/src/alpha6/root-cause-replan-carryover.js +29 -5
  40. package/dist/src/alpha6/root-cause-replan-carryover.js.map +1 -1
  41. package/dist/src/beta1/project-transaction.d.ts +3 -1
  42. package/dist/src/beta1/project-transaction.js +6 -2
  43. package/dist/src/beta1/project-transaction.js.map +1 -1
  44. package/dist/src/checks/runner.d.ts +26 -0
  45. package/dist/src/checks/runner.js +223 -0
  46. package/dist/src/checks/runner.js.map +1 -0
  47. package/dist/src/checks/task-sync.d.ts +10 -0
  48. package/dist/src/checks/task-sync.js +41 -0
  49. package/dist/src/checks/task-sync.js.map +1 -0
  50. package/dist/src/checks/worker.d.ts +1 -0
  51. package/dist/src/checks/worker.js +154 -0
  52. package/dist/src/checks/worker.js.map +1 -0
  53. package/dist/src/cli-actions.d.ts +2 -2
  54. package/dist/src/cli-actions.js +3 -15
  55. package/dist/src/cli-actions.js.map +1 -1
  56. package/dist/src/cli.js +14 -98
  57. package/dist/src/cli.js.map +1 -1
  58. package/dist/src/contracts.d.ts +35 -32
  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/gateway-handshake.js +0 -8
  74. package/dist/src/gateway-handshake.js.map +1 -1
  75. package/dist/src/git.d.ts +3 -1
  76. package/dist/src/git.js +41 -14
  77. package/dist/src/git.js.map +1 -1
  78. package/dist/src/graph.js +25 -3
  79. package/dist/src/graph.js.map +1 -1
  80. package/dist/src/index.d.ts +1 -0
  81. package/dist/src/lifecycle/corrective-replan.js +3 -3
  82. package/dist/src/lifecycle/corrective-replan.js.map +1 -1
  83. package/dist/src/navigation-actions.d.ts +9 -0
  84. package/dist/src/navigation-actions.js +72 -0
  85. package/dist/src/navigation-actions.js.map +1 -0
  86. package/dist/src/navigation-update-artifact.d.ts +5 -0
  87. package/dist/src/navigation-update-artifact.js +227 -0
  88. package/dist/src/navigation-update-artifact.js.map +1 -0
  89. package/dist/src/navigation-update.d.ts +39 -0
  90. package/dist/src/navigation-update.js +82 -0
  91. package/dist/src/navigation-update.js.map +1 -0
  92. package/dist/src/observation.js +52 -29
  93. package/dist/src/observation.js.map +1 -1
  94. package/dist/src/observed-routes.js +5 -6
  95. package/dist/src/observed-routes.js.map +1 -1
  96. package/dist/src/pending-review-update.d.ts +0 -13
  97. package/dist/src/pending-review-update.js +1 -6
  98. package/dist/src/pending-review-update.js.map +1 -1
  99. package/dist/src/reviewer.d.ts +1 -1
  100. package/dist/src/reviewer.js +20 -17
  101. package/dist/src/reviewer.js.map +1 -1
  102. package/dist/src/runtime.d.ts +3 -0
  103. package/dist/src/runtime.js +6 -0
  104. package/dist/src/runtime.js.map +1 -0
  105. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  106. package/dist/src/state/corrective-replan-executor.js +37 -1
  107. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  108. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  109. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  110. package/dist/src/state/corrective-replan-public.js +6 -5
  111. package/dist/src/state/corrective-replan-public.js.map +1 -1
  112. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  113. package/dist/src/state/corrective-replan-transaction.js +12 -11
  114. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  115. package/dist/src/state/corrective-yield-executor.js +2 -2
  116. package/dist/src/state/corrective-yield-executor.js.map +1 -1
  117. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  118. package/dist/src/state/corrective-yield-transaction.js +11 -10
  119. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  120. package/dist/src/state/lock.d.ts +4 -0
  121. package/dist/src/state/lock.js +21 -0
  122. package/dist/src/state/lock.js.map +1 -1
  123. package/dist/src/version.d.ts +1 -1
  124. package/dist/src/version.js +1 -1
  125. package/dist/src/version.js.map +1 -1
  126. package/dist/src/workflow-blocker-route.d.ts +8 -0
  127. package/dist/src/workflow-blocker-route.js +106 -0
  128. package/dist/src/workflow-blocker-route.js.map +1 -0
  129. package/dist/src/workflow.d.ts +48 -63
  130. package/dist/src/workflow.js +698 -1470
  131. package/dist/src/workflow.js.map +1 -1
  132. package/docs/autonomy-guardrails.md +26 -304
  133. package/docs/decisions.md +14 -112
  134. package/docs/development-flow.md +35 -238
  135. package/docs/project-memory.md +31 -50
  136. package/docs/release-app-evidence.md +190 -0
  137. package/docs/release.md +95 -363
  138. package/docs/updating-existing-project.md +19 -717
  139. package/package.json +11 -13
  140. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  141. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  142. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  143. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  144. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  145. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  146. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  147. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -740
  148. package/references/state-machine.md +4 -4
  149. package/roles/technical-planner.md +1 -1
  150. package/schemas/corrective-decision-event.schema.json +3 -1
  151. package/schemas/project-knowledge-map.schema.json +3 -1
  152. package/schemas/remediation-event.schema.json +10 -1
  153. package/schemas/task.schema.json +46 -2
  154. package/schemas/transition-payloads.schema.json +16 -1
  155. package/src/alpha6/adoption.ts +1123 -0
  156. package/src/alpha6/captured-check-evidence.ts +132 -0
  157. package/src/alpha6/check-support-anchor.ts +128 -0
  158. package/src/alpha6/component-owner.ts +168 -0
  159. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  160. package/src/alpha6/downstream-proof.ts +520 -0
  161. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  162. package/src/alpha6/handoff.ts +1443 -0
  163. package/src/alpha6/journal.ts +473 -0
  164. package/src/alpha6/literal-test-invocation.ts +84 -0
  165. package/src/alpha6/mechanical-feasibility.ts +488 -0
  166. package/src/alpha6/milestone.ts +2192 -0
  167. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  168. package/src/alpha6/npm-check-contract.ts +47 -0
  169. package/src/alpha6/plan-integrity.ts +298 -0
  170. package/src/alpha6/plan-risk.ts +1480 -0
  171. package/src/alpha6/preexecution-replan.ts +187 -0
  172. package/src/alpha6/remediation-cause.ts +98 -0
  173. package/src/alpha6/remediation.ts +2438 -0
  174. package/src/alpha6/review.ts +1198 -0
  175. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  176. package/src/alpha6/store-sidecars.ts +335 -0
  177. package/src/alpha7/autonomy.ts +411 -0
  178. package/src/alpha7/corrective-recovery.ts +1332 -0
  179. package/src/artifacts.ts +130 -0
  180. package/src/beta1/project-transaction.ts +355 -0
  181. package/src/change-explanation.ts +153 -0
  182. package/src/checks/runner.ts +245 -0
  183. package/src/checks/task-sync.ts +41 -0
  184. package/src/checks/worker.ts +149 -0
  185. package/src/cli-actions.ts +107 -0
  186. package/src/cli.ts +1457 -0
  187. package/src/contracts.ts +1494 -0
  188. package/src/credential-output.ts +89 -0
  189. package/src/credential-transport.ts +215 -0
  190. package/src/delegation.ts +190 -0
  191. package/src/dependency-provenance.ts +472 -0
  192. package/src/diagnostics.ts +93 -0
  193. package/src/domain/base-sync-conflict.ts +35 -0
  194. package/src/domain/completed-step-carryover.ts +76 -0
  195. package/src/domain/discovery.ts +27 -0
  196. package/src/domain/plan-semantics.ts +58 -0
  197. package/src/domain/step-start-admission.ts +10 -0
  198. package/src/domain/validation.ts +177 -0
  199. package/src/domain/virgin-registration.ts +46 -0
  200. package/src/errors.ts +24 -0
  201. package/src/fs-utils.ts +61 -0
  202. package/src/gateway-handshake.ts +95 -0
  203. package/src/git.ts +183 -0
  204. package/src/graph.ts +342 -0
  205. package/src/historical-step-provenance.ts +136 -0
  206. package/src/index.ts +23 -0
  207. package/src/lifecycle/canonical-hash.ts +28 -0
  208. package/src/lifecycle/catalog.ts +202 -0
  209. package/src/lifecycle/compiler-inspection.ts +29 -0
  210. package/src/lifecycle/core-static-readiness.ts +132 -0
  211. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  212. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  213. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  214. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  215. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  216. package/src/lifecycle/corrective-replan.ts +843 -0
  217. package/src/lifecycle/evaluator.ts +48 -0
  218. package/src/lifecycle/fingerprint.ts +488 -0
  219. package/src/lifecycle/immutable.ts +8 -0
  220. package/src/lifecycle/implementation-table.ts +118 -0
  221. package/src/lifecycle/index.ts +8 -0
  222. package/src/lifecycle/schema-artifact.ts +263 -0
  223. package/src/lifecycle/semantic-registry.ts +572 -0
  224. package/src/lifecycle/types.ts +838 -0
  225. package/src/memory.ts +273 -0
  226. package/src/migration.ts +161 -0
  227. package/src/navigation-actions.ts +70 -0
  228. package/src/navigation-update-artifact.ts +198 -0
  229. package/src/navigation-update.ts +121 -0
  230. package/src/observation.ts +225 -0
  231. package/src/observed-routes.ts +660 -0
  232. package/src/operational-contract.ts +125 -0
  233. package/src/pending-review-update.ts +175 -0
  234. package/src/repository.ts +99 -0
  235. package/src/reviewer.ts +1879 -0
  236. package/src/runtime.ts +6 -0
  237. package/src/state/corrective-replan-executor.ts +818 -0
  238. package/src/state/corrective-replan-public-schema.ts +83 -0
  239. package/src/state/corrective-replan-public.ts +908 -0
  240. package/src/state/corrective-replan-transaction.ts +949 -0
  241. package/src/state/corrective-yield-executor.ts +327 -0
  242. package/src/state/corrective-yield-transaction.ts +730 -0
  243. package/src/state/lock.ts +902 -0
  244. package/src/state/store.ts +567 -0
  245. package/src/transition-core.ts +330 -0
  246. package/src/ulid.ts +24 -0
  247. package/src/version.ts +2 -0
  248. package/src/workflow-blocker-route.ts +109 -0
  249. package/src/workflow.ts +10172 -0
  250. package/docs/alpha7.1-implementation-brief.md +0 -268
  251. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  252. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  253. package/docs/beta1-stabilization-brief.md +0 -165
  254. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  255. package/docs/beta13.2-signal-review-recovery.md +0 -38
  256. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  257. package/docs/change-model.md +0 -118
  258. package/docs/delegated-approval.md +0 -254
  259. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  260. package/docs/pdf/README.md +0 -24
  261. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  262. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  263. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  264. package/docs/pdf/requirements.txt +0 -1
  265. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  266. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -508
  267. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  268. package/docs/pending-review-update.md +0 -15
  269. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  270. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  271. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  272. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  273. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  274. package/docs/problem-briefs/README.md +0 -120
  275. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  276. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  277. package/docs/split-required-recovery.md +0 -47
  278. package/docs/stable-release-defect-register.md +0 -730
  279. package/docs/validation-report.md +0 -182
  280. package/scripts/generate-pdf-docs.py +0 -524
  281. 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,39 @@
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
+
11
+ /** Checkout eligibility only: never an audit, writer credential, or permission to widen scope. */
12
+ export function correctiveDecisionCheckoutEligible(
13
+ store: FileStateStore, repository: string, task: TaskState, stepId: string,
14
+ ): boolean {
15
+ if (!task.taskBranch || currentBranch(repository) !== task.taskBranch) return false;
16
+ // Includes completed Step commits and recorded system/history commits. A new
17
+ // unrecorded HEAD must not become a corrective audit's implicit baseline.
18
+ validateTaskHistory(repository, task);
19
+ const dirty = changedFiles(repository).sort();
20
+ if (dirty.length === 0) return true;
21
+ if (task.workspaceOwner !== 'local' || task.steps.some(step => step.status === 'in_progress')) return false;
22
+ const step = task.steps.find(candidate => candidate.id === stepId);
23
+ const pause = step?.failurePause;
24
+ if (step?.status !== 'failed' || !pause) return false;
25
+ const terminal = readRemediationEvents(store, task).filter(event => event.stepId === stepId).at(-1);
26
+ if (!terminal || terminal.failureKind !== 'checks-failed'
27
+ || terminal.failureEvidence.completionCommit !== headCommit(repository)
28
+ || terminal.failureEvidence.checksHash !== pause.checksHash
29
+ || terminal.failureEvidence.dirtyWorktreeHash !== pause.dirtyWorktreeHash
30
+ || JSON.stringify(dirty) !== JSON.stringify([...pause.dirtyFiles].sort())) return false;
31
+ const allowedWrites = effectiveStepAllowedWrites(store, task, stepId);
32
+ for (const file of dirty) {
33
+ if (!pathAllowed(file, allowedWrites) || pathAllowed(file, step.forbiddenScope)) return false;
34
+ // Do not admit symlinks (including dangling links) or directory substitutions.
35
+ try { if (!lstatSync(path.join(repository, file)).isFile()) return false; }
36
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
37
+ }
38
+ return hashDirtyWorktree(repository, dirty) === pause.dirtyWorktreeHash;
39
+ }