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,198 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { pendingReviewRunnerFingerprint } from './pending-review-update.js';
7
+ import { PACKAGE_NAME, PACKAGE_VERSION } from './version.js';
8
+
9
+ const MAX_COMPRESSED = 32 * 1024 * 1024;
10
+ const MAX_EXPANDED = 128 * 1024 * 1024;
11
+ const MAX_FILE = 32 * 1024 * 1024;
12
+ const MAX_ENTRIES = 10_000;
13
+ const BLOCK = 512;
14
+ const decoder = new TextDecoder('utf-8', { fatal: true });
15
+
16
+ function reject(message: string): never {
17
+ throw new Error(`NAVIGATION_UPDATE_ARTIFACT_INVALID: ${message}`);
18
+ }
19
+
20
+ function digest(bytes: Buffer): string {
21
+ return createHash('sha256').update(bytes).digest('hex');
22
+ }
23
+
24
+ function readRegular(file: string, maximum: number): Buffer {
25
+ let descriptor: number | undefined;
26
+ try {
27
+ descriptor = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
28
+ const before = fstatSync(descriptor);
29
+ if (!before.isFile() || before.size > maximum) reject('A required file is nonregular or exceeds its size limit.');
30
+ const bounded = Buffer.alloc(before.size + 1);
31
+ let length = 0;
32
+ while (length < bounded.length) {
33
+ const read = readSync(descriptor, bounded, length, bounded.length - length, null);
34
+ if (read === 0) break;
35
+ length += read;
36
+ }
37
+ const bytes = bounded.subarray(0, length);
38
+ const after = fstatSync(descriptor);
39
+ if (bytes.length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs
40
+ || after.ctimeMs !== before.ctimeMs) reject('A required file changed while being inspected.');
41
+ return bytes;
42
+ } finally {
43
+ if (descriptor !== undefined) closeSync(descriptor);
44
+ }
45
+ }
46
+
47
+ function textField(field: Buffer): string {
48
+ const nul = field.indexOf(0);
49
+ if (nul >= 0 && field.subarray(nul).some(byte => byte !== 0)) reject('Noncanonical tar string field.');
50
+ return decoder.decode(nul < 0 ? field : field.subarray(0, nul));
51
+ }
52
+
53
+ function octal(field: Buffer): number {
54
+ const value = field.toString('ascii').replace(/[\0 ]+$/g, '').trim();
55
+ if (value && !/^[0-7]+$/.test(value)) reject('Unsupported or invalid tar numeric field.');
56
+ const number = value ? Number.parseInt(value, 8) : 0;
57
+ if (!Number.isSafeInteger(number) || number < 0) reject('Tar numeric field exceeds the supported range.');
58
+ return number;
59
+ }
60
+
61
+ function safeName(name: string, directory: boolean): string {
62
+ const normalized = directory && name.endsWith('/') ? name.slice(0, -1) : name;
63
+ if (!normalized || normalized.length > 4096 || normalized.startsWith('/')
64
+ || /[\\\x00-\x1f\x7f]/.test(normalized)
65
+ || normalized.split('/').some(part => !part || part === '.' || part === '..')) {
66
+ reject('Tar entry has an unsafe or noncanonical path.');
67
+ }
68
+ return normalized;
69
+ }
70
+
71
+ function paxFields(bytes: Buffer): Record<string, string> {
72
+ if (bytes.length > 64 * 1024) reject('PAX metadata exceeds its size limit.');
73
+ const result: Record<string, string> = Object.create(null) as Record<string, string>;
74
+ let offset = 0;
75
+ while (offset < bytes.length) {
76
+ const space = bytes.indexOf(32, offset);
77
+ if (space < 0 || space - offset > 8) reject('Malformed PAX record length.');
78
+ const digits = bytes.subarray(offset, space).toString('ascii');
79
+ if (!/^[1-9][0-9]*$/.test(digits)) reject('Malformed PAX record length.');
80
+ const length = Number(digits);
81
+ const end = offset + length;
82
+ if (end > bytes.length || end <= space + 2 || bytes[end - 1] !== 10) reject('Truncated PAX record.');
83
+ const record = decoder.decode(bytes.subarray(space + 1, end - 1));
84
+ const separator = record.indexOf('=');
85
+ const key = record.slice(0, separator);
86
+ if (separator < 1 || !['path', 'size', 'mtime', 'atime', 'ctime', 'uid', 'gid', 'uname', 'gname', 'charset'].includes(key)
87
+ || Object.hasOwn(result, key)) reject('Unsupported or duplicate PAX metadata.');
88
+ result[key] = record.slice(separator + 1);
89
+ offset = end;
90
+ }
91
+ return result;
92
+ }
93
+
94
+ function unpack(bytes: Buffer): Map<string, Buffer> {
95
+ const files = new Map<string, Buffer>();
96
+ const names = new Set<string>();
97
+ let offset = 0;
98
+ let count = 0;
99
+ let pendingPax: Record<string, string> | null = null;
100
+ while (offset + BLOCK <= bytes.length) {
101
+ const header = bytes.subarray(offset, offset + BLOCK);
102
+ if (header.every(byte => byte === 0)) {
103
+ if (pendingPax || offset + 2 * BLOCK > bytes.length || bytes.subarray(offset).some(byte => byte !== 0)) {
104
+ reject('Tar end marker is incomplete or followed by unexpected data.');
105
+ }
106
+ return files;
107
+ }
108
+ if (++count > MAX_ENTRIES) reject('Tar entry count exceeds its limit.');
109
+ const expectedChecksum = octal(header.subarray(148, 156));
110
+ const checksum = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
111
+ if (checksum !== expectedChecksum) reject('Tar header checksum does not match.');
112
+ if (textField(header.subarray(257, 263)).trim() !== 'ustar') reject('Only USTAR/PAX package archives are supported.');
113
+ const type = header[156];
114
+ if (![0, 48, 53, 120].includes(type!)) reject('Tar links, special files and unsupported metadata are forbidden.');
115
+ if (textField(header.subarray(157, 257))) reject('Tar link targets are forbidden.');
116
+ const prefix = textField(header.subarray(345, 500));
117
+ const baseName = textField(header.subarray(0, 100));
118
+ const headerName = safeName(prefix ? `${prefix}/${baseName}` : baseName, type === 53);
119
+ const size = octal(header.subarray(124, 136));
120
+ if (size > MAX_FILE) reject('Tar entry exceeds its size limit.');
121
+ const start = offset + BLOCK;
122
+ const end = start + size;
123
+ const paddedEnd = start + Math.ceil(size / BLOCK) * BLOCK;
124
+ if (paddedEnd > bytes.length) reject('Tar entry data is truncated.');
125
+ if (bytes.subarray(end, paddedEnd).some(byte => byte !== 0)) reject('Tar entry padding is not zero.');
126
+ offset = paddedEnd;
127
+ if (type === 120) {
128
+ if (pendingPax) reject('Consecutive PAX headers are ambiguous.');
129
+ pendingPax = paxFields(bytes.subarray(start, end));
130
+ continue;
131
+ }
132
+ const name = safeName(pendingPax?.path ?? headerName, type === 53);
133
+ if (pendingPax?.size !== undefined && (!/^(0|[1-9][0-9]*)$/.test(pendingPax.size)
134
+ || Number(pendingPax.size) !== size)) reject('PAX size does not match the bounded tar entry.');
135
+ pendingPax = null;
136
+ if (name !== 'package' && !name.startsWith('package/')) reject('Tar entries must stay under package/.');
137
+ if (names.has(name)) reject('Duplicate tar paths are forbidden.');
138
+ names.add(name);
139
+ if (type === 53) {
140
+ if (size !== 0) reject('Tar directory entries cannot contain data.');
141
+ } else {
142
+ if (name === 'package') reject('The package root must be a directory.');
143
+ files.set(name.slice('package/'.length), bytes.subarray(start, end));
144
+ }
145
+ }
146
+ reject('Tar archive is missing its complete end marker.');
147
+ }
148
+
149
+ /** Inspect an exact local package; no install, extraction, network or authority mutation. */
150
+ export function verifyNavigationUpdateArtifact(tarball: string, installedPackageRoot?: string): { sha256: string; runtimeHash: string } {
151
+ try {
152
+ if (!tarball.trim()) reject('An explicit local tarball path is required.');
153
+ const compressed = readRegular(path.resolve(tarball), MAX_COMPRESSED);
154
+ if (compressed[0] !== 0x1f || compressed[1] !== 0x8b) reject('The artifact must be a gzip tarball.');
155
+ // Isolate synchronous decompression so a corrupt archive has an enforced deadline.
156
+ const expanded = execFileSync(process.execPath, ['--input-type=module', '--eval',
157
+ `import{readFileSync}from'node:fs';import{gunzipSync}from'node:zlib';process.stdout.write(gunzipSync(readFileSync(0),{maxOutputLength:${MAX_EXPANDED}}));`], {
158
+ input: compressed, timeout: 10_000, killSignal: 'SIGKILL', maxBuffer: MAX_EXPANDED + 1024,
159
+ stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, NODE_OPTIONS: '' },
160
+ });
161
+ if (expanded.length > MAX_EXPANDED) reject('Expanded tarball exceeds its size limit.');
162
+ const files = unpack(expanded);
163
+ const manifestBytes = files.get('package.json');
164
+ const buildBytes = files.get('dist/reviewer-runtime-build.json');
165
+ if (!manifestBytes || !buildBytes) reject('Package identity or runtime build evidence is missing.');
166
+ const pkg = JSON.parse(manifestBytes.toString('utf8')) as { name?: unknown; version?: unknown };
167
+ if (pkg.name !== PACKAGE_NAME || pkg.version !== PACKAGE_VERSION) reject('Tarball package/version differs from the running Workflow.');
168
+ const build = JSON.parse(buildBytes.toString('utf8')) as { files?: Array<{ path?: unknown }> };
169
+ if (!Array.isArray(build.files) || build.files.length === 0
170
+ || build.files.some(entry => typeof entry.path !== 'string' || !files.has(entry.path))
171
+ || !files.has('dist/src/navigation-update-artifact.js')) reject('The tarball omits required runtime files.');
172
+ const packageRoot = realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'));
173
+ if (installedPackageRoot !== undefined && !installedPackageRoot.trim()) reject('Installed package root must be explicit and nonempty.');
174
+ const roots = [
175
+ { root: packageRoot, label: 'running' },
176
+ ...(installedPackageRoot === undefined ? [] : [{ root: realpathSync(installedPackageRoot), label: 'installed' }]),
177
+ ];
178
+ for (const { root, label } of roots) {
179
+ for (const [relative, content] of files) {
180
+ let current = root;
181
+ const parts = relative.split('/');
182
+ for (let index = 0; index < parts.length; index++) {
183
+ current = path.join(current, parts[index]!);
184
+ const stat = lstatSync(current);
185
+ if (stat.isSymbolicLink() || (index < parts.length - 1 && !stat.isDirectory())) {
186
+ reject(`The ${label} package path is not a regular confined path: ${relative}`);
187
+ }
188
+ }
189
+ if (!readRegular(current, MAX_FILE).equals(content)) reject(`Tarball bytes differ from the ${label} package: ${relative}`);
190
+ }
191
+ }
192
+ const runtimeHash = pendingReviewRunnerFingerprint();
193
+ return { sha256: digest(compressed), runtimeHash };
194
+ } catch (error) {
195
+ if (error instanceof Error && error.message.startsWith('NAVIGATION_UPDATE_ARTIFACT_INVALID:')) throw error;
196
+ reject(error instanceof Error ? error.message : 'Artifact inspection failed.');
197
+ }
198
+ }
@@ -0,0 +1,121 @@
1
+ import { lstatSync, readFileSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { TaskState } from './contracts.js';
4
+ import type { FileStateStore } from './state/store.js';
5
+ import { assessNavigationDirtyCarryover } from './alpha6/navigation-dirty-carryover.js';
6
+ import { canonicalJsonStringify, sha256Hex } from './alpha6/store-sidecars.js';
7
+ import { inspectBoundedWorkflowDependencyCommit } from './dependency-provenance.js';
8
+ import { inspectWorkflowVersions } from './diagnostics.js';
9
+ import { currentBranch, headCommit, runGit } from './repository.js';
10
+ import { PACKAGE_VERSION } from './version.js';
11
+
12
+ const SOURCE_VERSION: string = '2.0.0-beta.13.16';
13
+ export interface NavigationUpdateReceipt {
14
+ protocol: 'navigation-update-source-v1';
15
+ issuedAt: string;
16
+ projectId: string;
17
+ taskId: string;
18
+ revision: number;
19
+ taskHash: string;
20
+ artifactsHash: string;
21
+ knowledgeHash: string;
22
+ milestoneHash: string | null;
23
+ taskBranch: string;
24
+ baseBranch: string;
25
+ sourceHead: string;
26
+ sourceBaseHead: string;
27
+ sourceVersion: string;
28
+ targetVersion: string;
29
+ artifact: { sha256: string; runtimeHash: string };
30
+ actor: string;
31
+ dirtyHash: string;
32
+ }
33
+ function require(condition: unknown, message: string): asserts condition {
34
+ if (!condition) throw new Error(`NAVIGATION_UPDATE_BLOCKED: ${message}`);
35
+ }
36
+ const hash = (value: unknown) => sha256Hex(canonicalJsonStringify(value));
37
+ function hashFiles(root: string): string {
38
+ const visit = (directory: string): Array<[string, string]> => readdirSync(directory).sort().flatMap(name => {
39
+ const file = path.join(directory, name); const info = lstatSync(file);
40
+ require(!info.isSymbolicLink(), 'Task artifacts must not contain symlinks.');
41
+ require(info.isDirectory() || info.isFile(), 'Task artifacts must be regular files or directories.');
42
+ return info.isDirectory() ? visit(file) : [[path.relative(root, file), sha256Hex(readFileSync(file))]];
43
+ });
44
+ return hash(visit(root));
45
+ }
46
+ function sourcePin(repository: string, commit: string): void {
47
+ const manifest = JSON.parse(runGit(repository, ['show', `${commit}:package.json`]));
48
+ const lock = JSON.parse(runGit(repository, ['show', `${commit}:package-lock.json`]));
49
+ require(manifest.devDependencies?.['codex-workflow-v2'] === SOURCE_VERSION
50
+ && !manifest.dependencies?.['codex-workflow-v2']
51
+ && lock.packages?.['']?.devDependencies?.['codex-workflow-v2'] === SOURCE_VERSION
52
+ && !lock.packages?.['']?.dependencies?.['codex-workflow-v2']
53
+ && lock.packages?.['node_modules/codex-workflow-v2']?.version === SOURCE_VERSION,
54
+ 'Source manifest and lock must bind the exact supported source version.');
55
+ }
56
+
57
+ /** Recompute source proof even after transport. The virtual record classifies Git only;
58
+ * it neither writes state nor manufactures execution authority. */
59
+ export function navigationUpdateSource(
60
+ repository: string, store: FileStateStore, task: TaskState,
61
+ artifact: NavigationUpdateReceipt['artifact'], issuedAt: string,
62
+ sourceHead = headCommit(repository), sourceBaseHead = runGit(repository, ['rev-parse', task.baseBranch]),
63
+ ): NavigationUpdateReceipt {
64
+ require(PACKAGE_VERSION !== SOURCE_VERSION, 'Target must differ from source.');
65
+ require(issuedAt === task.updatedAt && Number.isFinite(Date.parse(issuedAt)), 'Receipt timestamp must bind the stored Task update.');
66
+ require(task.taskBranch && currentBranch(repository) === task.taskBranch, 'Expected owned Task branch.');
67
+ sourcePin(repository, sourceHead); sourcePin(repository, sourceBaseHead);
68
+ const dependency = inspectBoundedWorkflowDependencyCommit(repository, sourceHead);
69
+ require(dependency?.packageVersion === SOURCE_VERSION && !task.systemCommits.includes(sourceHead),
70
+ 'Source HEAD must be the one unregistered source dependency commit.');
71
+ require(runGit(repository, ['rev-list', '--parents', '-n', '1', sourceHead]).split(' ').length === 2,
72
+ 'Source dependency commit must have one parent.');
73
+ const virtual: TaskState = { ...task, systemCommits: [...task.systemCommits, sourceHead],
74
+ dependencyProvenanceRecoveries: [...(task.dependencyProvenanceRecoveries ?? []), {
75
+ ...dependency, packageName: 'codex-workflow-v2', files: ['package-lock.json', 'package.json'],
76
+ actor: 'source-proof', reason: 'Read-only Git classification.', recordedAt: issuedAt,
77
+ }] };
78
+ const proof = assessNavigationDirtyCarryover(store, repository, virtual, undefined, sourceHead, sourceHead);
79
+ require(proof.eligible && proof.compatibility && proof.requiredActor, proof.blockers.join(' '));
80
+ return { protocol: 'navigation-update-source-v1', issuedAt, projectId: task.projectId, taskId: task.id,
81
+ revision: task.revision, taskHash: hash(task), artifactsHash: hashFiles(store.taskRoot(task.projectId, task.id)),
82
+ knowledgeHash: hash(store.readKnowledgeMap(task.projectId)),
83
+ milestoneHash: task.milestoneId ? hash(store.readMilestone(task.projectId, task.milestoneId)) : null,
84
+ taskBranch: task.taskBranch, baseBranch: task.baseBranch, sourceHead, sourceBaseHead,
85
+ sourceVersion: SOURCE_VERSION, targetVersion: PACKAGE_VERSION, artifact,
86
+ actor: proof.requiredActor, dirtyHash: proof.compatibility.dirtyWorktreeHash };
87
+ }
88
+
89
+ export function assertNavigationSourceInstalled(repository: string, task: TaskState): void {
90
+ const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
91
+ require([versions.declared, versions.locked, versions.installed, versions.currentBranch,
92
+ ...versions.milestoneBases.map(base => base.version)].every(version => version === SOURCE_VERSION),
93
+ 'All source version surfaces must agree before transport.');
94
+ }
95
+
96
+ export function inspectNavigationTransport(
97
+ repository: string, store: FileStateStore, task: TaskState, receipt: NavigationUpdateReceipt,
98
+ artifact: NavigationUpdateReceipt['artifact'], _now: Date,
99
+ ) {
100
+ require(receipt?.protocol === 'navigation-update-source-v1', 'Missing or foreign source receipt.');
101
+ const head = headCommit(repository);
102
+ const base = runGit(repository, ['rev-parse', task.baseBranch]);
103
+ const target = inspectBoundedWorkflowDependencyCommit(repository, head);
104
+ const baseTransport = inspectBoundedWorkflowDependencyCommit(repository, base);
105
+ require([head, base].every(commit => runGit(repository, ['rev-list', '--parents', '-n', '1', commit]).split(' ').length === 2),
106
+ 'Transport commits must have one parent.');
107
+ require(target?.parentCommitSha === receipt.sourceHead && target?.packageVersion === PACKAGE_VERSION,
108
+ 'Expected exactly one target dependency transport on the source HEAD.');
109
+ require(baseTransport?.parentCommitSha === receipt.sourceBaseHead && baseTransport?.packageVersion === PACKAGE_VERSION,
110
+ 'Expected exactly one target dependency transport on the source base HEAD.');
111
+ const recomputed = navigationUpdateSource(repository, store, task, artifact, receipt.issuedAt,
112
+ target.parentCommitSha, baseTransport.parentCommitSha);
113
+ require(canonicalJsonStringify(receipt) === canonicalJsonStringify(recomputed),
114
+ 'Receipt does not match independently recomputed source evidence, state or target artifact.');
115
+ const versions = inspectWorkflowVersions(repository, [task.baseBranch]);
116
+ require([versions.declared, versions.locked, versions.installed, versions.currentBranch,
117
+ ...versions.milestoneBases.map(item => item.version)].every(version => version === PACKAGE_VERSION),
118
+ 'All installed target version surfaces must agree.');
119
+ const source = inspectBoundedWorkflowDependencyCommit(repository, receipt.sourceHead)!;
120
+ return { receipt: recomputed, commits: [source, target], baseTransport: base, targetHead: head };
121
+ }
@@ -0,0 +1,225 @@
1
+ import { existsSync, readdirSync, realpathSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type {
4
+ AdoptionPostureEvent,
5
+ ObservationReasonCode,
6
+ ObservedMilestoneTransaction,
7
+ ObservedProjectTransaction,
8
+ PublicWriterLease,
9
+ ProjectRecord,
10
+ WorkflowObservation,
11
+ WriterLease,
12
+ } from './contracts.js';
13
+ import { readCurrentAdoptionPosture } from './alpha6/adoption.js';
14
+ import { listMilestoneTransactionJournals } from './alpha6/journal.js';
15
+ import { assertNoSymlinkPathComponents } from './alpha6/store-sidecars.js';
16
+ import { listProjectTransactions } from './beta1/project-transaction.js';
17
+ import type { RepositoryIdentity } from './repository.js';
18
+ import {
19
+ listCoreTaskOperationMutexes,
20
+ listWriterLeases,
21
+ toPublicCoreTaskOperation,
22
+ toPublicWriterLease,
23
+ } from './state/lock.js';
24
+ import { inspectCorrectiveReplanTaskTransactions } from './state/corrective-replan-transaction.js';
25
+ import { inspectCorrectiveYieldTaskTransactions } from './state/corrective-yield-transaction.js';
26
+ import type { FileStateStore } from './state/store.js';
27
+
28
+ export interface WorkflowObservationBoundary {
29
+ identity: RepositoryIdentity;
30
+ project: ProjectRecord | null;
31
+ currentAdoptionPosture: AdoptionPostureEvent | null;
32
+ leases: WriterLease[];
33
+ observation: WorkflowObservation;
34
+ }
35
+
36
+ /**
37
+ * Reads only the persistence boundary needed to decide whether a coherent lifecycle snapshot can
38
+ * be loaded. It deliberately does not register a project, recover a journal, initialize adoption,
39
+ * or construct a WriterLockManager.
40
+ */
41
+ export function inspectWorkflowObservationBoundary(options: {
42
+ store: FileStateStore;
43
+ identity: RepositoryIdentity;
44
+ now: Date;
45
+ }): WorkflowObservationBoundary {
46
+ const { store, identity, now } = options;
47
+ const projectRoot = store.projectRoot(identity.projectId);
48
+ const projectFile = path.join(projectRoot, 'project.json');
49
+ const lockRoot = store.lockRoot(identity.projectId);
50
+ const stateRootPresent = existsSync(store.root);
51
+ const projectRootPresent = existsSync(projectRoot);
52
+ const lockRootPresent = existsSync(lockRoot);
53
+ const missingRegistration = !existsSync(projectFile);
54
+ const pendingProjectTransactions = observeProjectTransactions(projectRoot, identity.projectId);
55
+ const pendingMilestoneTransactions = observeMilestoneTransactions(store, identity.projectId);
56
+ const replanTransactions = inspectCorrectiveReplanTaskTransactions(projectRoot, store.root);
57
+ const yieldTransactions = inspectCorrectiveYieldTaskTransactions(projectRoot, store.root);
58
+ const leases = listWriterLeases(lockRoot);
59
+ const coreTaskOperations = listCoreTaskOperationMutexes(lockRoot, store.root)
60
+ .map((mutex) => toPublicCoreTaskOperation(mutex, now));
61
+ const completedTaskTransactions = [...replanTransactions.completed, ...yieldTransactions.completed];
62
+ const retainedCompletedWithMutex = completedTaskTransactions
63
+ .filter(transaction => coreTaskOperations.some(operation =>
64
+ operation.entityId === transaction.taskId && operation.operationId === transaction.transactionId
65
+ && operation.transitionId === transaction.transitionId));
66
+ const taskTransactions = {
67
+ pending: [...replanTransactions.pending, ...yieldTransactions.pending, ...retainedCompletedWithMutex]
68
+ .sort((left, right) => left.taskId.localeCompare(right.taskId) || left.transactionId.localeCompare(right.transactionId)),
69
+ corrupt: [...replanTransactions.corrupt, ...yieldTransactions.corrupt]
70
+ .sort((left, right) => left.taskId.localeCompare(right.taskId) || left.transactionId.localeCompare(right.transactionId)),
71
+ };
72
+ // Recover one exact journal at a time; independent Tasks can interrupt different families.
73
+ const pendingTaskTransaction = taskTransactions.pending[0];
74
+ const orphanCoreTaskOperations = coreTaskOperations.filter((operation) => !taskTransactions.pending.some(
75
+ (transaction) => transaction.taskId === operation.entityId && transaction.transactionId === operation.operationId
76
+ && transaction.transitionId === operation.transitionId,
77
+ ));
78
+
79
+ const activeTaskRecoveryOwner = coreTaskOperations.some(operation => !operation.stale)
80
+ || taskTransactions.pending.some(transaction => leases.some(lease =>
81
+ lease.entityId === transaction.taskId && Date.parse(lease.expiresAt) > now.getTime()));
82
+ const staleOrphanCoreOperations = orphanCoreTaskOperations.filter(operation => operation.stale);
83
+ const conflictsWithRetainedJournal = (operation: typeof coreTaskOperations[number]) =>
84
+ taskTransactions.pending.some(transaction => transaction.taskId === operation.entityId)
85
+ || completedTaskTransactions.some(transaction => transaction.taskId === operation.entityId
86
+ && transaction.transactionId === operation.operationId);
87
+ const conflictingOrphanCoreOperations = staleOrphanCoreOperations.filter(conflictsWithRetainedJournal);
88
+ const repairableOrphanCoreOperation = staleOrphanCoreOperations.find(operation => !conflictsWithRetainedJournal(operation));
89
+
90
+ const project = !missingRegistration && pendingProjectTransactions.length === 0
91
+ ? store.readProject(identity.projectId)
92
+ : null;
93
+ const repositoryRootBound = project?.repositoryRoots.includes(identity.repositoryRoot) ?? false;
94
+ const adoptionAssessed = pendingProjectTransactions.length === 0
95
+ && pendingMilestoneTransactions.length === 0
96
+ && taskTransactions.pending.length === 0
97
+ && taskTransactions.corrupt.length === 0
98
+ && orphanCoreTaskOperations.length === 0;
99
+ const currentAdoptionPosture = adoptionAssessed && stateRootPresent
100
+ ? readCurrentAdoptionPosture(store, identity.projectId)
101
+ : null;
102
+ const missingAdoption = adoptionAssessed && currentAdoptionPosture === null;
103
+ const observedLeases = leases.map((lease) => toPublicWriterLease(lease, now));
104
+ const activeLeases = observedLeases.filter((lease) => !lease.stale);
105
+ const staleLeases = observedLeases.filter((lease) => lease.stale);
106
+
107
+ const reasonCodes: ObservationReasonCode[] = [];
108
+ if (missingRegistration) reasonCodes.push('PROJECT_REGISTRATION_MISSING');
109
+ if (project && !repositoryRootBound) reasonCodes.push('PROJECT_REPOSITORY_ROOT_UNBOUND');
110
+ if (pendingProjectTransactions.length > 0) reasonCodes.push('PROJECT_RECOVERY_REQUIRED');
111
+ if (pendingMilestoneTransactions.length > 0) reasonCodes.push('MILESTONE_RECOVERY_REQUIRED');
112
+ if (taskTransactions.pending.length > 0) reasonCodes.push('TASK_RECOVERY_REQUIRED');
113
+ if (taskTransactions.corrupt.length > 0) reasonCodes.push('TASK_TRANSACTION_INTEGRITY_FAILED');
114
+ if (activeTaskRecoveryOwner) reasonCodes.push('TASK_OPERATION_IN_PROGRESS');
115
+ if (orphanCoreTaskOperations.some((operation) => operation.stale)) reasonCodes.push('STALE_TASK_OPERATION');
116
+ if (activeLeases.length > 0) reasonCodes.push('ACTIVE_WRITER_LEASE');
117
+ if (staleLeases.length > 0) reasonCodes.push('STALE_WRITER_LEASE');
118
+ if (missingAdoption) reasonCodes.push('ADOPTION_MISSING');
119
+
120
+ const blocked = missingRegistration
121
+ || (project !== null && !repositoryRootBound)
122
+ || pendingProjectTransactions.some(transaction => transaction.ownerActive)
123
+ || taskTransactions.corrupt.length > 0
124
+ || activeTaskRecoveryOwner
125
+ || conflictingOrphanCoreOperations.length > 0;
126
+ const repairable = repairableOrphanCoreOperation !== undefined
127
+ || pendingProjectTransactions.length > 0
128
+ || taskTransactions.pending.length > 0
129
+ || pendingMilestoneTransactions.length > 0;
130
+ const kind: WorkflowObservation['kind'] = blocked ? 'blocked' : repairable ? 'repair' : 'assessment';
131
+ const repair = kind !== 'repair'
132
+ ? null
133
+ : repairableOrphanCoreOperation
134
+ ? {
135
+ command: 'locks repair' as const,
136
+ cataloged: false as const,
137
+ projectId: identity.projectId,
138
+ entityIds: [repairableOrphanCoreOperation.entityId],
139
+ }
140
+ : taskTransactions.pending.length > 0
141
+ ? {
142
+ command: pendingTaskTransaction!.transitionId === 'task.coordination.corrective-yield'
143
+ ? 'task corrective-yield-recover' as const
144
+ : 'task corrective-replan-recover' as const,
145
+ cataloged: false as const,
146
+ available: true as const,
147
+ projectId: identity.projectId,
148
+ taskIds: [pendingTaskTransaction!.taskId],
149
+ transactionIds: [pendingTaskTransaction!.transactionId],
150
+ }
151
+ : pendingMilestoneTransactions.length > 0 || pendingProjectTransactions.length > 0
152
+ ? {
153
+ command: 'milestone recover' as const,
154
+ cataloged: false as const,
155
+ projectId: identity.projectId,
156
+ milestoneIds: [...new Set(pendingMilestoneTransactions.map((transaction) => transaction.milestoneId))].sort(),
157
+ transactionIds: [...pendingProjectTransactions, ...pendingMilestoneTransactions]
158
+ .map((transaction) => transaction.transactionId).sort(),
159
+ }
160
+ : {
161
+ command: 'locks repair' as const,
162
+ cataloged: false as const,
163
+ projectId: identity.projectId,
164
+ entityIds: staleLeases.map((lease) => lease.entityId).sort(),
165
+ };
166
+
167
+ return {
168
+ identity,
169
+ project,
170
+ currentAdoptionPosture,
171
+ leases,
172
+ observation: {
173
+ kind,
174
+ reasonCodes,
175
+ stateRootPresent,
176
+ projectRootPresent,
177
+ lockRootPresent,
178
+ missingRegistration,
179
+ repositoryRootBound,
180
+ adoptionAssessed,
181
+ missingAdoption,
182
+ pendingProjectTransactions,
183
+ pendingMilestoneTransactions,
184
+ pendingTaskTransactions: taskTransactions.pending,
185
+ corruptTaskTransactions: taskTransactions.corrupt,
186
+ coreTaskOperations,
187
+ leases: observedLeases,
188
+ availableRepairTransitionId: null,
189
+ repair,
190
+ },
191
+ };
192
+ }
193
+
194
+ function observeProjectTransactions(projectRoot: string, projectId: string): ObservedProjectTransaction[] {
195
+ return listProjectTransactions(projectRoot, projectId)
196
+ .map((transaction) => ({ ...transaction }))
197
+ .sort((left, right) => left.preparedAt.localeCompare(right.preparedAt)
198
+ || left.transactionId.localeCompare(right.transactionId));
199
+ }
200
+
201
+ function observeMilestoneTransactions(
202
+ store: FileStateStore,
203
+ projectId: string,
204
+ ): ObservedMilestoneTransaction[] {
205
+ const milestonesRoot = path.join(store.projectRoot(projectId), 'milestones');
206
+ if (!existsSync(milestonesRoot)) return [];
207
+ assertNoSymlinkPathComponents(milestonesRoot, {
208
+ label: 'Milestones observation root',
209
+ requireLeaf: true,
210
+ trustedRoot: store.root,
211
+ });
212
+ return readdirSync(milestonesRoot, { withFileTypes: true })
213
+ .filter((entry) => entry.isDirectory())
214
+ .flatMap((entry) => listMilestoneTransactionJournals(realpathSync(path.join(milestonesRoot, entry.name)), {
215
+ expectedMilestoneId: entry.name,
216
+ }).map((journal) => ({
217
+ milestoneId: journal.milestoneId,
218
+ transactionId: journal.transactionId,
219
+ phase: journal.phase,
220
+ preparedAt: journal.preparedAt,
221
+ updatedAt: journal.updatedAt,
222
+ })))
223
+ .sort((left, right) => left.preparedAt.localeCompare(right.preparedAt)
224
+ || left.transactionId.localeCompare(right.transactionId));
225
+ }