codex-workflow-v2 2.0.0-beta.13.9 → 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 (271) hide show
  1. package/README.md +27 -388
  2. package/dist/reviewer-runtime-build.json +89 -29
  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/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 +6 -21
  30. package/dist/src/alpha6/remediation.js +161 -476
  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/beta1/project-transaction.d.ts +3 -1
  36. package/dist/src/beta1/project-transaction.js +6 -2
  37. package/dist/src/beta1/project-transaction.js.map +1 -1
  38. package/dist/src/checks/runner.d.ts +26 -0
  39. package/dist/src/checks/runner.js +223 -0
  40. package/dist/src/checks/runner.js.map +1 -0
  41. package/dist/src/checks/task-sync.d.ts +10 -0
  42. package/dist/src/checks/task-sync.js +41 -0
  43. package/dist/src/checks/task-sync.js.map +1 -0
  44. package/dist/src/checks/worker.d.ts +1 -0
  45. package/dist/src/checks/worker.js +154 -0
  46. package/dist/src/checks/worker.js.map +1 -0
  47. package/dist/src/cli-actions.d.ts +2 -2
  48. package/dist/src/cli-actions.js +3 -15
  49. package/dist/src/cli-actions.js.map +1 -1
  50. package/dist/src/cli.js +14 -98
  51. package/dist/src/cli.js.map +1 -1
  52. package/dist/src/contracts.d.ts +35 -32
  53. package/dist/src/dependency-provenance.d.ts +2 -2
  54. package/dist/src/dependency-provenance.js +12 -69
  55. package/dist/src/dependency-provenance.js.map +1 -1
  56. package/dist/src/domain/base-sync-conflict.d.ts +13 -0
  57. package/dist/src/domain/base-sync-conflict.js +32 -0
  58. package/dist/src/domain/base-sync-conflict.js.map +1 -0
  59. package/dist/src/domain/step-start-admission.d.ts +4 -0
  60. package/dist/src/domain/step-start-admission.js +9 -0
  61. package/dist/src/domain/step-start-admission.js.map +1 -0
  62. package/dist/src/domain/virgin-registration.d.ts +3 -0
  63. package/dist/src/domain/virgin-registration.js +65 -0
  64. package/dist/src/domain/virgin-registration.js.map +1 -0
  65. package/dist/src/errors.d.ts +1 -1
  66. package/dist/src/errors.js.map +1 -1
  67. package/dist/src/gateway-handshake.js +0 -8
  68. package/dist/src/gateway-handshake.js.map +1 -1
  69. package/dist/src/git.d.ts +3 -1
  70. package/dist/src/git.js +41 -14
  71. package/dist/src/git.js.map +1 -1
  72. package/dist/src/graph.js +25 -3
  73. package/dist/src/graph.js.map +1 -1
  74. package/dist/src/index.d.ts +1 -0
  75. package/dist/src/navigation-actions.d.ts +9 -0
  76. package/dist/src/navigation-actions.js +72 -0
  77. package/dist/src/navigation-actions.js.map +1 -0
  78. package/dist/src/navigation-update-artifact.d.ts +5 -0
  79. package/dist/src/navigation-update-artifact.js +227 -0
  80. package/dist/src/navigation-update-artifact.js.map +1 -0
  81. package/dist/src/navigation-update.d.ts +39 -0
  82. package/dist/src/navigation-update.js +82 -0
  83. package/dist/src/navigation-update.js.map +1 -0
  84. package/dist/src/observation.js +52 -29
  85. package/dist/src/observation.js.map +1 -1
  86. package/dist/src/observed-routes.js +5 -6
  87. package/dist/src/observed-routes.js.map +1 -1
  88. package/dist/src/pending-review-update.d.ts +0 -13
  89. package/dist/src/pending-review-update.js +1 -6
  90. package/dist/src/pending-review-update.js.map +1 -1
  91. package/dist/src/reviewer.d.ts +1 -1
  92. package/dist/src/reviewer.js +20 -17
  93. package/dist/src/reviewer.js.map +1 -1
  94. package/dist/src/runtime.d.ts +3 -0
  95. package/dist/src/runtime.js +6 -0
  96. package/dist/src/runtime.js.map +1 -0
  97. package/dist/src/state/corrective-replan-executor.d.ts +10 -0
  98. package/dist/src/state/corrective-replan-executor.js +37 -1
  99. package/dist/src/state/corrective-replan-executor.js.map +1 -1
  100. package/dist/src/state/corrective-replan-public-schema.js +15 -2
  101. package/dist/src/state/corrective-replan-public-schema.js.map +1 -1
  102. package/dist/src/state/corrective-replan-public.js +1 -1
  103. package/dist/src/state/corrective-replan-public.js.map +1 -1
  104. package/dist/src/state/corrective-replan-transaction.d.ts +1 -0
  105. package/dist/src/state/corrective-replan-transaction.js +12 -11
  106. package/dist/src/state/corrective-replan-transaction.js.map +1 -1
  107. package/dist/src/state/corrective-yield-transaction.d.ts +1 -0
  108. package/dist/src/state/corrective-yield-transaction.js +11 -10
  109. package/dist/src/state/corrective-yield-transaction.js.map +1 -1
  110. package/dist/src/state/lock.d.ts +4 -0
  111. package/dist/src/state/lock.js +21 -0
  112. package/dist/src/state/lock.js.map +1 -1
  113. package/dist/src/version.d.ts +1 -1
  114. package/dist/src/version.js +1 -1
  115. package/dist/src/version.js.map +1 -1
  116. package/dist/src/workflow-blocker-route.d.ts +8 -0
  117. package/dist/src/workflow-blocker-route.js +106 -0
  118. package/dist/src/workflow-blocker-route.js.map +1 -0
  119. package/dist/src/workflow.d.ts +48 -63
  120. package/dist/src/workflow.js +666 -1460
  121. package/dist/src/workflow.js.map +1 -1
  122. package/docs/autonomy-guardrails.md +26 -304
  123. package/docs/decisions.md +14 -112
  124. package/docs/development-flow.md +35 -238
  125. package/docs/project-memory.md +31 -50
  126. package/docs/release-app-evidence.md +190 -0
  127. package/docs/release.md +95 -388
  128. package/docs/updating-existing-project.md +19 -717
  129. package/package.json +11 -13
  130. package/plugins/codex-workflow-gateway/.codex-plugin/plugin.json +2 -2
  131. package/plugins/codex-workflow-gateway/references/chat-dispatch.md +73 -197
  132. package/plugins/codex-workflow-gateway/references/codebase-memory-routing.md +57 -0
  133. package/plugins/codex-workflow-gateway/references/protocol.md +42 -445
  134. package/plugins/codex-workflow-gateway/scripts/chat-dispatch.mjs +7 -1
  135. package/plugins/codex-workflow-gateway/scripts/chat-model-policy.mjs +10 -8
  136. package/plugins/codex-workflow-gateway/scripts/chat-registry.mjs +23 -5
  137. package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +66 -757
  138. package/references/state-machine.md +4 -4
  139. package/roles/technical-planner.md +1 -1
  140. package/schemas/corrective-decision-event.schema.json +3 -1
  141. package/schemas/project-knowledge-map.schema.json +3 -1
  142. package/schemas/remediation-event.schema.json +10 -1
  143. package/schemas/task.schema.json +46 -2
  144. package/schemas/transition-payloads.schema.json +16 -1
  145. package/src/alpha6/adoption.ts +1123 -0
  146. package/src/alpha6/captured-check-evidence.ts +132 -0
  147. package/src/alpha6/check-support-anchor.ts +128 -0
  148. package/src/alpha6/component-owner.ts +168 -0
  149. package/src/alpha6/corrective-decision-boundary.ts +39 -0
  150. package/src/alpha6/downstream-proof.ts +520 -0
  151. package/src/alpha6/failed-step-planning-recovery.ts +88 -0
  152. package/src/alpha6/handoff.ts +1443 -0
  153. package/src/alpha6/journal.ts +473 -0
  154. package/src/alpha6/literal-test-invocation.ts +84 -0
  155. package/src/alpha6/mechanical-feasibility.ts +488 -0
  156. package/src/alpha6/milestone.ts +2192 -0
  157. package/src/alpha6/navigation-dirty-carryover.ts +154 -0
  158. package/src/alpha6/npm-check-contract.ts +47 -0
  159. package/src/alpha6/plan-integrity.ts +298 -0
  160. package/src/alpha6/plan-risk.ts +1480 -0
  161. package/src/alpha6/preexecution-replan.ts +187 -0
  162. package/src/alpha6/remediation-cause.ts +98 -0
  163. package/src/alpha6/remediation.ts +2438 -0
  164. package/src/alpha6/review.ts +1198 -0
  165. package/src/alpha6/root-cause-replan-carryover.ts +491 -0
  166. package/src/alpha6/store-sidecars.ts +335 -0
  167. package/src/alpha7/autonomy.ts +411 -0
  168. package/src/alpha7/corrective-recovery.ts +1332 -0
  169. package/src/artifacts.ts +130 -0
  170. package/src/beta1/project-transaction.ts +355 -0
  171. package/src/change-explanation.ts +153 -0
  172. package/src/checks/runner.ts +245 -0
  173. package/src/checks/task-sync.ts +41 -0
  174. package/src/checks/worker.ts +149 -0
  175. package/src/cli-actions.ts +107 -0
  176. package/src/cli.ts +1457 -0
  177. package/src/contracts.ts +1494 -0
  178. package/src/credential-output.ts +89 -0
  179. package/src/credential-transport.ts +215 -0
  180. package/src/delegation.ts +190 -0
  181. package/src/dependency-provenance.ts +472 -0
  182. package/src/diagnostics.ts +93 -0
  183. package/src/domain/base-sync-conflict.ts +35 -0
  184. package/src/domain/completed-step-carryover.ts +76 -0
  185. package/src/domain/discovery.ts +27 -0
  186. package/src/domain/plan-semantics.ts +58 -0
  187. package/src/domain/step-start-admission.ts +10 -0
  188. package/src/domain/validation.ts +177 -0
  189. package/src/domain/virgin-registration.ts +46 -0
  190. package/src/errors.ts +24 -0
  191. package/src/fs-utils.ts +61 -0
  192. package/src/gateway-handshake.ts +95 -0
  193. package/src/git.ts +183 -0
  194. package/src/graph.ts +342 -0
  195. package/src/historical-step-provenance.ts +136 -0
  196. package/src/index.ts +23 -0
  197. package/src/lifecycle/canonical-hash.ts +28 -0
  198. package/src/lifecycle/catalog.ts +202 -0
  199. package/src/lifecycle/compiler-inspection.ts +29 -0
  200. package/src/lifecycle/core-static-readiness.ts +132 -0
  201. package/src/lifecycle/corrective-replan-authority.ts +136 -0
  202. package/src/lifecycle/corrective-replan-binding-manifest.ts +51 -0
  203. package/src/lifecycle/corrective-replan-credential-core.ts +408 -0
  204. package/src/lifecycle/corrective-replan-credential-schema.ts +54 -0
  205. package/src/lifecycle/corrective-replan-credentials.ts +48 -0
  206. package/src/lifecycle/corrective-replan.ts +843 -0
  207. package/src/lifecycle/evaluator.ts +48 -0
  208. package/src/lifecycle/fingerprint.ts +488 -0
  209. package/src/lifecycle/immutable.ts +8 -0
  210. package/src/lifecycle/implementation-table.ts +118 -0
  211. package/src/lifecycle/index.ts +8 -0
  212. package/src/lifecycle/schema-artifact.ts +263 -0
  213. package/src/lifecycle/semantic-registry.ts +572 -0
  214. package/src/lifecycle/types.ts +838 -0
  215. package/src/memory.ts +273 -0
  216. package/src/migration.ts +161 -0
  217. package/src/navigation-actions.ts +70 -0
  218. package/src/navigation-update-artifact.ts +198 -0
  219. package/src/navigation-update.ts +121 -0
  220. package/src/observation.ts +225 -0
  221. package/src/observed-routes.ts +660 -0
  222. package/src/operational-contract.ts +125 -0
  223. package/src/pending-review-update.ts +175 -0
  224. package/src/repository.ts +99 -0
  225. package/src/reviewer.ts +1879 -0
  226. package/src/runtime.ts +6 -0
  227. package/src/state/corrective-replan-executor.ts +818 -0
  228. package/src/state/corrective-replan-public-schema.ts +83 -0
  229. package/src/state/corrective-replan-public.ts +908 -0
  230. package/src/state/corrective-replan-transaction.ts +949 -0
  231. package/src/state/corrective-yield-executor.ts +327 -0
  232. package/src/state/corrective-yield-transaction.ts +730 -0
  233. package/src/state/lock.ts +902 -0
  234. package/src/state/store.ts +567 -0
  235. package/src/transition-core.ts +330 -0
  236. package/src/ulid.ts +24 -0
  237. package/src/version.ts +2 -0
  238. package/src/workflow-blocker-route.ts +109 -0
  239. package/src/workflow.ts +10172 -0
  240. package/docs/alpha7.1-implementation-brief.md +0 -268
  241. package/docs/alpha7.2-corrective-context-refresh-brief.md +0 -484
  242. package/docs/alpha7.2.1-remediation-recovery-brief.md +0 -86
  243. package/docs/beta1-stabilization-brief.md +0 -165
  244. package/docs/beta11-plan-integrity-recovery-brief.md +0 -38
  245. package/docs/beta13.2-signal-review-recovery.md +0 -38
  246. package/docs/beta2-initial-assembly-navigation-brief.md +0 -616
  247. package/docs/change-model.md +0 -118
  248. package/docs/delegated-approval.md +0 -254
  249. package/docs/lifecycle/state-machine-stabilization.md +0 -641
  250. package/docs/pdf/README.md +0 -24
  251. package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +0 -0
  252. package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +0 -0
  253. package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +0 -0
  254. package/docs/pdf/requirements.txt +0 -1
  255. package/docs/pdf/sources/codex-workflow-v2-architecture-ru.md +0 -478
  256. package/docs/pdf/sources/codex-workflow-v2-chat-only-guide-ru.md +0 -506
  257. package/docs/pdf/sources/codex-workflow-v2-technical-reference-ru.md +0 -778
  258. package/docs/pending-review-update.md +0 -15
  259. package/docs/problem-briefs/01-pre-implementation-integrity.md +0 -482
  260. package/docs/problem-briefs/02-minimal-step-integrity.md +0 -411
  261. package/docs/problem-briefs/03-minimal-agent-context-integrity.md +0 -358
  262. package/docs/problem-briefs/04-task-dependency-and-structural-replacement-integrity.md +0 -573
  263. package/docs/problem-briefs/BRIEF-TEMPLATE.md +0 -56
  264. package/docs/problem-briefs/README.md +0 -120
  265. package/docs/problem-briefs/evidence/p01-mechanical-feasibility-corpus.md +0 -90
  266. package/docs/problem-briefs/evidence/signal-v4-pre-m3-replay.md +0 -246
  267. package/docs/split-required-recovery.md +0 -47
  268. package/docs/stable-release-defect-register.md +0 -730
  269. package/docs/validation-report.md +0 -182
  270. package/scripts/generate-pdf-docs.py +0 -524
  271. 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
+ }