@open-agent-toolkit/cli 0.3.10 → 0.3.12

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 (163) hide show
  1. package/README.md +2 -1
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/cli-utilities/meta.json +13 -0
  4. package/assets/docs/cli-utilities/tool-packs.md +8 -2
  5. package/assets/docs/cli-utilities/workflow-gates.md +13 -0
  6. package/assets/docs/contributing/code.md +8 -2
  7. package/assets/docs/contributing/documentation.md +7 -1
  8. package/assets/docs/contributing/meta.json +15 -0
  9. package/assets/docs/docs-tooling/add-docs-to-a-repo.md +14 -5
  10. package/assets/docs/docs-tooling/commands.md +26 -8
  11. package/assets/docs/docs-tooling/meta.json +4 -0
  12. package/assets/docs/docs-tooling/workflows.md +2 -2
  13. package/assets/docs/guide/meta.json +4 -0
  14. package/assets/docs/meta.json +14 -0
  15. package/assets/docs/provider-sync/instruction-sync.md +8 -0
  16. package/assets/docs/provider-sync/meta.json +11 -0
  17. package/assets/docs/provider-sync/scope-and-surface.md +3 -4
  18. package/assets/docs/reference/cli-reference.md +5 -2
  19. package/assets/docs/reference/docs-index-contract.md +23 -6
  20. package/assets/docs/reference/file-locations.md +7 -2
  21. package/assets/docs/reference/index.md +1 -1
  22. package/assets/docs/reference/meta.json +10 -0
  23. package/assets/docs/reference/troubleshooting.md +41 -6
  24. package/assets/docs/workflows/ideas/meta.json +4 -0
  25. package/assets/docs/workflows/meta.json +4 -0
  26. package/assets/docs/workflows/projects/cursor-cloud.md +8 -3
  27. package/assets/docs/workflows/projects/evidence-layers.md +6 -8
  28. package/assets/docs/workflows/projects/implementation-execution.md +30 -4
  29. package/assets/docs/workflows/projects/lifecycle.md +7 -1
  30. package/assets/docs/workflows/projects/meta.json +25 -0
  31. package/assets/docs/workflows/projects/orchestration-model.md +5 -6
  32. package/assets/docs/workflows/skills/meta.json +10 -0
  33. package/assets/docs/workflows/skills/recon.md +31 -3
  34. package/assets/public-package-versions.json +4 -4
  35. package/assets/skills/oat-cursor-cloud-projects/SKILL.md +30 -16
  36. package/assets/skills/oat-dispatch-subagents/SKILL.md +4 -12
  37. package/assets/skills/oat-dispatch-subagents/references/managed-claude-example.json +1 -1
  38. package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +51 -0
  39. package/assets/skills/oat-dispatch-subagents/references/record-schema.md +8 -7
  40. package/assets/skills/oat-docs-analyze/SKILL.md +28 -3
  41. package/assets/skills/oat-docs-apply/SKILL.md +3 -3
  42. package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
  43. package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +5 -2
  44. package/assets/skills/oat-docs-bootstrap/SKILL.md +4 -3
  45. package/assets/skills/oat-docs-bootstrap/assets/AGENTS.md.template +3 -3
  46. package/assets/skills/oat-pjm-decision/SKILL.md +3 -2
  47. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +4 -4
  48. package/assets/skills/oat-project-complete/SKILL.md +47 -2
  49. package/assets/skills/oat-project-design/SKILL.md +10 -6
  50. package/assets/skills/oat-project-discover/SKILL.md +3 -3
  51. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +3 -10
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +4 -4
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +135 -3
  55. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +7 -9
  56. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +4 -4
  57. package/assets/skills/oat-project-implement/references/plan-and-resume.md +1 -1
  58. package/assets/skills/oat-project-import-plan/SKILL.md +3 -3
  59. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +4 -4
  60. package/assets/skills/oat-project-new/SKILL.md +3 -3
  61. package/assets/skills/oat-project-next/SKILL.md +50 -7
  62. package/assets/skills/oat-project-plan/SKILL.md +4 -4
  63. package/assets/skills/oat-project-plan-writing/SKILL.md +2 -2
  64. package/assets/skills/oat-project-pr-final/SKILL.md +11 -1
  65. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +4 -4
  66. package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
  67. package/assets/skills/oat-project-quick-start/SKILL.md +12 -8
  68. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +4 -4
  69. package/assets/skills/oat-project-retro/SKILL.md +6 -3
  70. package/assets/skills/oat-project-review-provide/SKILL.md +2 -2
  71. package/assets/skills/oat-project-review-provide-remote/SKILL.md +2 -2
  72. package/assets/skills/oat-project-spec/SKILL.md +3 -3
  73. package/assets/skills/oat-project-summary/SKILL.md +22 -3
  74. package/assets/skills/oat-repo-improve/SKILL.md +2 -2
  75. package/assets/skills/recon/SKILL.md +24 -3
  76. package/assets/skills/recon/references/packet-contract.md +36 -6
  77. package/assets/skills/recon/references/profiles.md +3 -0
  78. package/assets/skills/recon/references/worker-contract.md +16 -3
  79. package/assets/skills/recon/scripts/create-review-brief.mjs +17 -86
  80. package/assets/skills/recon/scripts/lib/contracts.mjs +158 -41
  81. package/assets/skills/recon/scripts/lib/review-binding.mjs +123 -0
  82. package/assets/skills/recon/scripts/lib/routing.mjs +5 -2
  83. package/assets/skills/recon/scripts/reconcile-ledger.mjs +18 -7
  84. package/assets/skills/recon/scripts/render-packet.mjs +120 -1
  85. package/assets/skills/recon/scripts/validate-packet.mjs +119 -88
  86. package/assets/templates/state.md +1 -0
  87. package/dist/commands/backlog/new.d.ts +2 -2
  88. package/dist/commands/backlog/new.d.ts.map +1 -1
  89. package/dist/commands/backlog/new.js +2 -2
  90. package/dist/commands/decision/new.d.ts +2 -2
  91. package/dist/commands/decision/new.d.ts.map +1 -1
  92. package/dist/commands/decision/new.js +2 -2
  93. package/dist/commands/docs/nav/contents.d.ts +9 -0
  94. package/dist/commands/docs/nav/contents.d.ts.map +1 -1
  95. package/dist/commands/docs/nav/contents.js +7 -2
  96. package/dist/commands/docs/nav/fumadocs.d.ts +58 -0
  97. package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -0
  98. package/dist/commands/docs/nav/fumadocs.js +326 -0
  99. package/dist/commands/docs/nav/sync.d.ts +31 -1
  100. package/dist/commands/docs/nav/sync.d.ts.map +1 -1
  101. package/dist/commands/docs/nav/sync.js +163 -6
  102. package/dist/commands/index.d.ts.map +1 -1
  103. package/dist/commands/index.js +2 -0
  104. package/dist/commands/instructions/instructions.utils.d.ts +19 -0
  105. package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
  106. package/dist/commands/instructions/instructions.utils.js +89 -17
  107. package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
  108. package/dist/commands/instructions/sync/sync.js +49 -2
  109. package/dist/commands/pjm/init.d.ts.map +1 -1
  110. package/dist/commands/pjm/init.js +2 -2
  111. package/dist/commands/project/closeout-check/closeout-invariant.d.ts +97 -0
  112. package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -0
  113. package/dist/commands/project/closeout-check/closeout-invariant.js +301 -0
  114. package/dist/commands/project/closeout-check/index.d.ts +38 -0
  115. package/dist/commands/project/closeout-check/index.d.ts.map +1 -0
  116. package/dist/commands/project/closeout-check/index.js +117 -0
  117. package/dist/commands/project/complete-state/index.d.ts +4 -0
  118. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  119. package/dist/commands/project/complete-state/index.js +11 -13
  120. package/dist/commands/project/dispatch/index.d.ts.map +1 -1
  121. package/dist/commands/project/dispatch/index.js +6 -30
  122. package/dist/commands/project/dispatch/record.d.ts +9 -18
  123. package/dist/commands/project/dispatch/record.d.ts.map +1 -1
  124. package/dist/commands/project/dispatch/record.js +15 -212
  125. package/dist/commands/project/index.d.ts.map +1 -1
  126. package/dist/commands/project/index.js +2 -0
  127. package/dist/commands/project/new/scaffold.d.ts +0 -1
  128. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  129. package/dist/commands/project/new/scaffold.js +19 -20
  130. package/dist/commands/project/promote/promote.d.ts +2 -2
  131. package/dist/commands/project/promote/promote.d.ts.map +1 -1
  132. package/dist/commands/project/promote/promote.js +13 -11
  133. package/dist/commands/shared/frontmatter.d.ts +21 -0
  134. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  135. package/dist/commands/shared/frontmatter.js +70 -4
  136. package/dist/commands/shared/template-source.d.ts +29 -0
  137. package/dist/commands/shared/template-source.d.ts.map +1 -0
  138. package/dist/commands/{pjm → shared}/template-source.js +24 -7
  139. package/dist/commands/state/generate.js +2 -2
  140. package/dist/commands/template/index.d.ts +10 -0
  141. package/dist/commands/template/index.d.ts.map +1 -0
  142. package/dist/commands/template/index.js +87 -0
  143. package/dist/commands/template/resolve.d.ts +31 -0
  144. package/dist/commands/template/resolve.d.ts.map +1 -0
  145. package/dist/commands/template/resolve.js +86 -0
  146. package/dist/fs/io.d.ts +0 -41
  147. package/dist/fs/io.d.ts.map +1 -1
  148. package/dist/fs/io.js +2 -414
  149. package/dist/providers/identity/absolute-paths.d.ts +4 -13
  150. package/dist/providers/identity/absolute-paths.d.ts.map +1 -1
  151. package/dist/providers/identity/absolute-paths.js +4 -17
  152. package/dist/providers/identity/generic-dispatch-record.d.ts +1 -1
  153. package/dist/providers/identity/generic-dispatch-record.js +2 -2
  154. package/dist/providers/identity/oat-dispatch-record.d.ts +4 -357
  155. package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -1
  156. package/dist/providers/identity/oat-dispatch-record.js +4 -236
  157. package/dist/release/public-package-contract.d.ts.map +1 -1
  158. package/dist/release/public-package-contract.js +12 -4
  159. package/dist/validation/skills.d.ts.map +1 -1
  160. package/dist/validation/skills.js +32 -6
  161. package/package.json +2 -2
  162. package/dist/commands/pjm/template-source.d.ts +0 -14
  163. package/dist/commands/pjm/template-source.d.ts.map +0 -1
@@ -0,0 +1,301 @@
1
+ import YAML, { isMap } from 'yaml';
2
+ /**
3
+ * The closeout invariant shared by `oat project closeout-check` and
4
+ * `oat project complete-state` (BL-260806-fail-closed-when-configured).
5
+ *
6
+ * A closeout that the implement skill must snapshot (configured
7
+ * `workflow.postImplementSequence`, autonomous, or lite) cannot reach terminal
8
+ * completion until its durable `oat_post_implement_sequence` snapshot exists,
9
+ * every stored step is recorded complete, approval is recorded, and the
10
+ * snapshot reached `status: complete`. The snapshot schema is the one
11
+ * `oat-project-implement/references/completion-and-closeout.md` Step 15
12
+ * persists. Once a snapshot exists it is authoritative: current configuration
13
+ * is never consulted, so a later config change cannot invalidate a persisted
14
+ * run.
15
+ */
16
+ export const SEQUENCE_FIELD = 'oat_post_implement_sequence';
17
+ export const RECOVERY_SKILL = 'oat-project-implement';
18
+ const APPROVAL_WRITES = [
19
+ 'approval: approved',
20
+ 'approval: not_required',
21
+ ];
22
+ const PRE_APPROVAL_STEPS = ['summary', 'document', 'pr'];
23
+ const POST_APPROVAL_STEPS = ['summary', 'document', 'pr', 'retro'];
24
+ const SEQUENCE_STATUSES = [
25
+ 'pre_approval',
26
+ 'awaiting_approval',
27
+ 'post_approval',
28
+ 'failed',
29
+ 'complete',
30
+ ];
31
+ const APPROVALS = ['pending', 'approved', 'not_required'];
32
+ const SOURCES = ['configured', 'autonomous-default'];
33
+ const APPROVAL_SOURCES = ['user', 'oat-autonomous'];
34
+ export const STEP_SKILLS = {
35
+ summary: 'oat-project-summary',
36
+ document: 'oat-project-document',
37
+ pr: 'oat-project-pr-final',
38
+ retro: 'oat-project-retro',
39
+ };
40
+ class MalformedSnapshotError extends Error {
41
+ }
42
+ function malformed(detail) {
43
+ throw new MalformedSnapshotError(detail);
44
+ }
45
+ function readFrontmatter(content) {
46
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
47
+ if (!match) {
48
+ throw new Error('state.md is missing frontmatter');
49
+ }
50
+ const document = YAML.parseDocument(match[1], { uniqueKeys: true });
51
+ if (document.errors.length > 0) {
52
+ throw new Error(`state.md frontmatter is malformed (${document.errors[0]?.message ?? 'YAML parse error'})`);
53
+ }
54
+ if (!isMap(document.contents)) {
55
+ throw new Error('state.md frontmatter root is not a YAML map');
56
+ }
57
+ const value = document.toJS();
58
+ return value;
59
+ }
60
+ function readEnum(record, key, allowed, options = {}) {
61
+ if (!(key in record)) {
62
+ if (options.optional)
63
+ return null;
64
+ malformed(`\`${key}\` is missing`);
65
+ }
66
+ const value = record[key];
67
+ if (value === null && options.nullable)
68
+ return null;
69
+ if (typeof value !== 'string' || !allowed.includes(value)) {
70
+ malformed(`\`${key}\` must be one of ${allowed.join(', ')}${options.nullable ? ', or null' : ''}`);
71
+ }
72
+ return value;
73
+ }
74
+ function readSteps(record, key, allowed) {
75
+ const value = record[key];
76
+ if (!Array.isArray(value)) {
77
+ malformed(`\`${key}\` must be an array`);
78
+ }
79
+ const steps = [];
80
+ for (const entry of value) {
81
+ if (typeof entry !== 'string' || !allowed.includes(entry)) {
82
+ malformed(`\`${key}\` contains \`${String(entry)}\`; allowed steps are ${allowed.join(', ')}`);
83
+ }
84
+ if (steps.includes(entry)) {
85
+ malformed(`\`${key}\` lists \`${entry}\` more than once`);
86
+ }
87
+ steps.push(entry);
88
+ }
89
+ return steps;
90
+ }
91
+ /**
92
+ * Completed steps must be exactly the first N stored steps: the implement
93
+ * skill dispatches in stored order, so a gap or a foreign step is a
94
+ * contradiction, not progress.
95
+ */
96
+ function assertCompletedPrefix(stored, completed, storedKey, completedKey) {
97
+ for (const step of completed) {
98
+ if (!stored.includes(step)) {
99
+ malformed(`\`${completedKey}\` lists \`${step}\`, which \`${storedKey}\` does not`);
100
+ }
101
+ }
102
+ const prefix = stored.slice(0, completed.length);
103
+ if (!prefix.every((step) => completed.includes(step))) {
104
+ malformed(`\`${completedKey}\` is not a prefix of the stored \`${storedKey}\` order`);
105
+ }
106
+ }
107
+ export function parseCloseoutSnapshot(value) {
108
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
109
+ malformed('the snapshot must be a map');
110
+ }
111
+ const record = value;
112
+ const status = readEnum(record, 'status', SEQUENCE_STATUSES);
113
+ // `source` and `approval_source` are additive provenance; an older snapshot
114
+ // without them stays valid.
115
+ const source = readEnum(record, 'source', SOURCES, {
116
+ optional: true,
117
+ nullable: true,
118
+ });
119
+ readEnum(record, 'approval_source', APPROVAL_SOURCES, {
120
+ optional: true,
121
+ nullable: true,
122
+ });
123
+ if ('final_phase' in record &&
124
+ record.final_phase !== null &&
125
+ typeof record.final_phase !== 'string') {
126
+ malformed('`final_phase` must be a phase ID string');
127
+ }
128
+ const preApproval = readSteps(record, 'pre_approval', PRE_APPROVAL_STEPS);
129
+ const postApproval = readSteps(record, 'post_approval', POST_APPROVAL_STEPS);
130
+ for (const step of postApproval) {
131
+ if (preApproval.includes(step)) {
132
+ malformed(`\`${step}\` appears in both pre_approval and post_approval`);
133
+ }
134
+ }
135
+ const preApprovalCompleted = readSteps(record, 'pre_approval_completed', PRE_APPROVAL_STEPS);
136
+ const postApprovalCompleted = readSteps(record, 'post_approval_completed', POST_APPROVAL_STEPS);
137
+ assertCompletedPrefix(preApproval, preApprovalCompleted, 'pre_approval', 'pre_approval_completed');
138
+ assertCompletedPrefix(postApproval, postApprovalCompleted, 'post_approval', 'post_approval_completed');
139
+ const approval = readEnum(record, 'approval', APPROVALS);
140
+ if (postApprovalCompleted.length > 0 && approval === 'pending') {
141
+ malformed('`post_approval_completed` records work while `approval` is still pending');
142
+ }
143
+ const snapshot = {
144
+ status,
145
+ source,
146
+ preApproval,
147
+ preApprovalCompleted,
148
+ approval,
149
+ postApproval,
150
+ postApprovalCompleted,
151
+ failure: record.failure ?? null,
152
+ };
153
+ if (status === 'complete') {
154
+ const pending = firstPending(preApproval, preApprovalCompleted) ??
155
+ firstPending(postApproval, postApprovalCompleted);
156
+ if (pending || approval === 'pending') {
157
+ malformed('`status: complete` contradicts pending steps or a pending approval');
158
+ }
159
+ }
160
+ return snapshot;
161
+ }
162
+ function firstPending(stored, completed) {
163
+ return stored.find((step) => !completed.includes(step));
164
+ }
165
+ function stepOwner(phase, step) {
166
+ return { kind: 'step', phase, step, skill: STEP_SKILLS[step] };
167
+ }
168
+ function approvalOwner() {
169
+ return { kind: 'approval', skill: RECOVERY_SKILL, writes: APPROVAL_WRITES };
170
+ }
171
+ function evaluateSnapshot(snapshot, inputs) {
172
+ const prePending = firstPending(snapshot.preApproval, snapshot.preApprovalCompleted);
173
+ const postPending = firstPending(snapshot.postApproval, snapshot.postApprovalCompleted);
174
+ const incomplete = (invariant, detail, nextOwner) => ({
175
+ status: 'incomplete',
176
+ invariant,
177
+ detail,
178
+ route: RECOVERY_SKILL,
179
+ nextOwner,
180
+ inputs,
181
+ });
182
+ if (snapshot.status === 'failed') {
183
+ // Approval-aware order: pending pre-approval work, then the pending
184
+ // approval boundary, then post-approval work, then status repair. A
185
+ // post-approval step is never named while approval is pending.
186
+ const approvalPending = snapshot.approval === 'pending';
187
+ const owner = prePending
188
+ ? stepOwner('pre_approval', prePending)
189
+ : approvalPending
190
+ ? approvalOwner()
191
+ : postPending
192
+ ? stepOwner('post_approval', postPending)
193
+ : { kind: 'sequence-status', skill: RECOVERY_SKILL };
194
+ const at = owner.kind === 'step'
195
+ ? ` at \`${owner.step}\``
196
+ : owner.kind === 'approval'
197
+ ? ' at the approval boundary'
198
+ : '';
199
+ return incomplete('sequence_failed', `the post-implementation sequence recorded a failure${at}`, owner);
200
+ }
201
+ if (prePending) {
202
+ return incomplete('pre_approval_step_pending', `pre-approval step \`${prePending}\` is not recorded complete (stored order: ${snapshot.preApproval.join(', ')})`, stepOwner('pre_approval', prePending));
203
+ }
204
+ if (snapshot.approval === 'pending') {
205
+ return incomplete('approval_pending', 'final approval is not recorded', approvalOwner());
206
+ }
207
+ if (postPending) {
208
+ return incomplete('post_approval_step_pending', `post-approval step \`${postPending}\` is not recorded complete (stored order: ${snapshot.postApproval.join(', ')})`, stepOwner('post_approval', postPending));
209
+ }
210
+ if (snapshot.status !== 'complete') {
211
+ return incomplete('sequence_not_complete', `every step and approval is recorded but the snapshot status is \`${snapshot.status}\`, not \`complete\``, { kind: 'sequence-status', skill: RECOVERY_SKILL });
212
+ }
213
+ return { status: 'complete', inputs };
214
+ }
215
+ export async function evaluateCloseout(options) {
216
+ const frontmatter = readFrontmatter(options.stateContent);
217
+ const workflowMode = typeof frontmatter.oat_workflow_mode === 'string'
218
+ ? frontmatter.oat_workflow_mode
219
+ : 'spec-driven';
220
+ const autonomousSource = options.autonomousFlag
221
+ ? 'flag'
222
+ : options.env.OAT_AUTONOMOUS === '1'
223
+ ? 'env'
224
+ : null;
225
+ const rawSnapshot = frontmatter[SEQUENCE_FIELD];
226
+ // An explicit null carries no snapshot, exactly like an absent key.
227
+ const snapshotPresent = SEQUENCE_FIELD in frontmatter && rawSnapshot !== null;
228
+ const inputs = {
229
+ workflowMode,
230
+ lite: workflowMode === 'lite',
231
+ autonomous: autonomousSource !== null,
232
+ autonomousSource,
233
+ configured: null,
234
+ snapshot: snapshotPresent ? 'present' : 'absent',
235
+ snapshotSource: null,
236
+ };
237
+ if (snapshotPresent) {
238
+ let snapshot;
239
+ try {
240
+ snapshot = parseCloseoutSnapshot(rawSnapshot);
241
+ }
242
+ catch (error) {
243
+ if (!(error instanceof MalformedSnapshotError))
244
+ throw error;
245
+ return {
246
+ status: 'incomplete',
247
+ invariant: 'snapshot_malformed',
248
+ detail: `\`${SEQUENCE_FIELD}\` is malformed: ${error.message}`,
249
+ route: RECOVERY_SKILL,
250
+ nextOwner: { kind: 'snapshot', skill: RECOVERY_SKILL },
251
+ inputs,
252
+ };
253
+ }
254
+ inputs.snapshotSource = snapshot.source;
255
+ return evaluateSnapshot(snapshot, inputs);
256
+ }
257
+ let reason = null;
258
+ if (inputs.lite) {
259
+ reason = 'lite';
260
+ }
261
+ else if (inputs.autonomous) {
262
+ reason = 'autonomous';
263
+ }
264
+ else {
265
+ inputs.configured = await options.resolveConfigured();
266
+ if (inputs.configured)
267
+ reason = 'configured';
268
+ }
269
+ if (!reason) {
270
+ return { status: 'not_required', inputs };
271
+ }
272
+ const why = {
273
+ lite: '`oat_workflow_mode: lite` always snapshots its closeout',
274
+ autonomous: 'an autonomous closeout (`--autonomous` or `OAT_AUTONOMOUS=1`) always snapshots its closeout',
275
+ configured: 'the effective `workflow.postImplementSequence` is configured',
276
+ }[reason];
277
+ return {
278
+ status: 'incomplete',
279
+ invariant: 'snapshot_missing',
280
+ detail: `\`${SEQUENCE_FIELD}\` is absent, but ${why}`,
281
+ route: RECOVERY_SKILL,
282
+ nextOwner: { kind: 'snapshot', skill: RECOVERY_SKILL },
283
+ inputs,
284
+ };
285
+ }
286
+ /**
287
+ * The one refusal message both commands print, so a skill or operator sees the
288
+ * same invariant and recovery route from either.
289
+ */
290
+ export function formatCloseoutRefusal(projectPath, result) {
291
+ const owner = result.nextOwner;
292
+ const next = owner.kind === 'step'
293
+ ? ` Next stored step: \`${owner.step}\` (${owner.skill}).`
294
+ : owner.kind === 'approval'
295
+ ? ' Next: record the final approval decision: `approval: approved` after final HiLL sign-off, or `approval: not_required` when no final checkpoint exists.'
296
+ : '';
297
+ return [
298
+ `Closeout invariant not satisfied for ${projectPath} (${result.invariant}): ${result.detail}.${next}`,
299
+ `Resume with ${RECOVERY_SKILL}; it persists the ${SEQUENCE_FIELD} snapshot (completion-and-closeout.md Step 15) and completes every stored step before completion.`,
300
+ ].join(' ');
301
+ }
@@ -0,0 +1,38 @@
1
+ import { readFile as defaultReadFile } from 'node:fs/promises';
2
+ import { buildCommandContext, type CommandContext } from '../../../app/command-context.js';
3
+ import { resolveEffectiveConfig } from '../../../config/resolve.js';
4
+ import { dirExists, fileExists } from '../../../fs/io.js';
5
+ import { Command } from 'commander';
6
+ import { type CloseoutResult } from './closeout-invariant.js';
7
+ export { evaluateCloseout, formatCloseoutRefusal, type CloseoutResult, } from './closeout-invariant.js';
8
+ export interface CloseoutCheckDependencies {
9
+ buildCommandContext: (options: Parameters<typeof buildCommandContext>[0]) => CommandContext;
10
+ resolveProjectRoot: (cwd: string) => Promise<string>;
11
+ resolveEffectiveConfig: typeof resolveEffectiveConfig;
12
+ readFile: typeof defaultReadFile;
13
+ dirExists: typeof dirExists;
14
+ fileExists: typeof fileExists;
15
+ /**
16
+ * Environment read for `OAT_AUTONOMOUS`. Injectable so a caller that forgets
17
+ * `--autonomous` under `OAT_AUTONOMOUS=1` still cannot fail open.
18
+ */
19
+ env: NodeJS.ProcessEnv;
20
+ }
21
+ export declare const DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES: CloseoutCheckDependencies;
22
+ export interface ProjectCloseoutCheck {
23
+ repoRoot: string;
24
+ targetProjectPath: string;
25
+ statePath: string;
26
+ stateContent: string;
27
+ result: CloseoutResult;
28
+ }
29
+ /**
30
+ * Resolve a project, read its `state.md` from disk, and evaluate the closeout
31
+ * invariant. Shared by `closeout-check` and `complete-state` so both read the
32
+ * same inputs and print the same refusal.
33
+ */
34
+ export declare function checkProjectCloseout(projectPath: string, options: {
35
+ autonomous: boolean;
36
+ }, context: CommandContext, dependencies: Pick<CloseoutCheckDependencies, 'resolveProjectRoot' | 'resolveEffectiveConfig' | 'readFile' | 'dirExists' | 'fileExists' | 'env'>): Promise<ProjectCloseoutCheck>;
37
+ export declare function createProjectCloseoutCheckCommand(overrides?: Partial<CloseoutCheckDependencies>): Command;
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/closeout-check/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAG/D,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,yBAAyB;IACxC,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,QAAQ,EAAE,OAAO,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAED,eAAO,MAAM,mCAAmC,EAAE,yBAQjD,CAAC;AAEF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,EAChC,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,IAAI,CAChB,yBAAyB,EACvB,oBAAoB,GACpB,wBAAwB,GACxB,UAAU,GACV,WAAW,GACX,YAAY,GACZ,KAAK,CACR,GACA,OAAO,CAAC,oBAAoB,CAAC,CA+B/B;AAmED,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,yBAAyB,CAAM,GACjD,OAAO,CA2BT"}
@@ -0,0 +1,117 @@
1
+ import { readFile as defaultReadFile } from 'node:fs/promises';
2
+ import { isAbsolute, join } from 'node:path';
3
+ import { buildCommandContext } from '../../../app/command-context.js';
4
+ import { readGlobalOptions } from '../../shared/shared.utils.js';
5
+ import { resolveEffectiveConfig } from '../../../config/resolve.js';
6
+ import { CliError } from '../../../errors/cli-error.js';
7
+ import { dirExists, fileExists } from '../../../fs/io.js';
8
+ import { resolveProjectRoot } from '../../../fs/paths.js';
9
+ import { Command } from 'commander';
10
+ import { evaluateCloseout, formatCloseoutRefusal, } from './closeout-invariant.js';
11
+ export { evaluateCloseout, formatCloseoutRefusal, } from './closeout-invariant.js';
12
+ export const DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES = {
13
+ buildCommandContext,
14
+ resolveProjectRoot,
15
+ resolveEffectiveConfig,
16
+ readFile: defaultReadFile,
17
+ dirExists,
18
+ fileExists,
19
+ env: process.env,
20
+ };
21
+ /**
22
+ * Resolve a project, read its `state.md` from disk, and evaluate the closeout
23
+ * invariant. Shared by `closeout-check` and `complete-state` so both read the
24
+ * same inputs and print the same refusal.
25
+ */
26
+ export async function checkProjectCloseout(projectPath, options, context, dependencies) {
27
+ const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
28
+ const targetProjectPath = isAbsolute(projectPath)
29
+ ? projectPath
30
+ : join(repoRoot, projectPath);
31
+ if (!(await dependencies.dirExists(targetProjectPath))) {
32
+ throw new CliError(`Project not found: ${projectPath}`, 1);
33
+ }
34
+ const statePath = join(targetProjectPath, 'state.md');
35
+ if (!(await dependencies.fileExists(statePath))) {
36
+ throw new CliError(`Project state.md not found: ${statePath}`, 1);
37
+ }
38
+ const stateContent = await dependencies.readFile(statePath, 'utf8');
39
+ const result = await evaluateCloseout({
40
+ stateContent,
41
+ autonomousFlag: options.autonomous,
42
+ env: dependencies.env,
43
+ resolveConfigured: async () => {
44
+ const effective = await dependencies.resolveEffectiveConfig(repoRoot, join(context.home, '.oat'), dependencies.env);
45
+ const value = effective.resolved['workflow.postImplementSequence']?.value;
46
+ return value !== null && value !== undefined;
47
+ },
48
+ });
49
+ return { repoRoot, targetProjectPath, statePath, stateContent, result };
50
+ }
51
+ async function runCloseoutCheck(projectPath, options, context, dependencies) {
52
+ try {
53
+ const { result } = await checkProjectCloseout(projectPath, { autonomous: options.autonomous ?? false }, context, dependencies);
54
+ if (result.status === 'incomplete') {
55
+ const message = formatCloseoutRefusal(projectPath, result);
56
+ if (context.json) {
57
+ context.logger.json({
58
+ status: 'incomplete',
59
+ projectPath,
60
+ invariant: result.invariant,
61
+ detail: result.detail,
62
+ route: result.route,
63
+ nextOwner: result.nextOwner,
64
+ inputs: result.inputs,
65
+ message,
66
+ });
67
+ }
68
+ else {
69
+ context.logger.info(message);
70
+ }
71
+ // Fail closed: a caller that ignores the JSON still stops.
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ if (context.json) {
76
+ context.logger.json({
77
+ status: result.status,
78
+ projectPath,
79
+ invariant: null,
80
+ route: null,
81
+ nextOwner: null,
82
+ inputs: result.inputs,
83
+ });
84
+ }
85
+ else if (result.status === 'complete') {
86
+ context.logger.info(`Closeout invariant satisfied for ${projectPath}: every stored step and the approval are recorded complete.`);
87
+ }
88
+ else {
89
+ context.logger.info(`Closeout invariant not required for ${projectPath}: no snapshot, and the closeout is not configured, autonomous, or lite.`);
90
+ }
91
+ process.exitCode = 0;
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ if (context.json) {
96
+ context.logger.json({ status: 'error', message });
97
+ }
98
+ else {
99
+ context.logger.error(message);
100
+ }
101
+ process.exitCode = error instanceof CliError ? error.exitCode : 1;
102
+ }
103
+ }
104
+ export function createProjectCloseoutCheckCommand(overrides = {}) {
105
+ const dependencies = {
106
+ ...DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES,
107
+ ...overrides,
108
+ };
109
+ return new Command('closeout-check')
110
+ .description('Report whether a project closeout snapshot permits terminal completion (read-only)')
111
+ .argument('<project-path>', 'Project path to check')
112
+ .option('--autonomous', 'Treat the closeout as autonomous (also implied by OAT_AUTONOMOUS=1)')
113
+ .action(async (projectPath, options, command) => {
114
+ const context = dependencies.buildCommandContext(readGlobalOptions(command));
115
+ await runCloseoutCheck(projectPath, options, context, dependencies);
116
+ });
117
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFile as defaultReadFile, writeFile as defaultWriteFile } from 'node:fs/promises';
2
2
  import { buildCommandContext, type CommandContext } from '../../../app/command-context.js';
3
+ import { resolveEffectiveConfig } from '../../../config/resolve.js';
3
4
  import { dirExists, fileExists } from '../../../fs/io.js';
4
5
  import { Command } from 'commander';
5
6
  interface ProjectCompleteStateDependencies {
@@ -9,6 +10,9 @@ interface ProjectCompleteStateDependencies {
9
10
  writeFile: typeof defaultWriteFile;
10
11
  dirExists: typeof dirExists;
11
12
  fileExists: typeof fileExists;
13
+ resolveEffectiveConfig: typeof resolveEffectiveConfig;
14
+ /** Environment read for `OAT_AUTONOMOUS`; injectable for tests. */
15
+ env: NodeJS.ProcessEnv;
12
16
  now: () => Date;
13
17
  }
14
18
  export declare function createProjectCompleteStateCommand(overrides?: Partial<ProjectCompleteStateDependencies>): Command;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/complete-state/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,IAAI,eAAe,EAC3B,SAAS,IAAI,gBAAgB,EAC9B,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGhF,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,UAAU,gCAAgC;IACxC,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,OAAO,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,gBAAgB,CAAC;IACnC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AA4ED,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,gCAAgC,CAAM,GACxD,OAAO,CA2BT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/complete-state/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,IAAI,eAAe,EAC3B,SAAS,IAAI,gBAAgB,EAC9B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAMhF,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAG/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,UAAU,gCAAgC;IACxC,mBAAmB,EAAE,CACnB,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAC/C,cAAc,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,OAAO,eAAe,CAAC;IACjC,SAAS,EAAE,OAAO,gBAAgB,CAAC;IACnC,SAAS,EAAE,OAAO,SAAS,CAAC;IAC5B,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;IACtD,mEAAmE;IACnE,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,IAAI,CAAC;CACjB;AA4ED,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,gCAAgC,CAAM,GACxD,OAAO,CA+BT"}
@@ -1,7 +1,8 @@
1
1
  import { readFile as defaultReadFile, writeFile as defaultWriteFile, } from 'node:fs/promises';
2
- import { isAbsolute, join } from 'node:path';
3
2
  import { buildCommandContext } from '../../../app/command-context.js';
3
+ import { checkProjectCloseout, formatCloseoutRefusal, } from '../../project/closeout-check/index.js';
4
4
  import { readGlobalOptions } from '../../shared/shared.utils.js';
5
+ import { resolveEffectiveConfig } from '../../../config/resolve.js';
5
6
  import { CliError } from '../../../errors/cli-error.js';
6
7
  import { dirExists, fileExists } from '../../../fs/io.js';
7
8
  import { resolveProjectRoot } from '../../../fs/paths.js';
@@ -15,24 +16,20 @@ const DEFAULT_DEPENDENCIES = {
15
16
  writeFile: defaultWriteFile,
16
17
  dirExists,
17
18
  fileExists,
19
+ resolveEffectiveConfig,
20
+ env: process.env,
18
21
  now: () => new Date(),
19
22
  };
20
- function resolveTargetProjectPath(repoRoot, projectPath) {
21
- return isAbsolute(projectPath) ? projectPath : join(repoRoot, projectPath);
22
- }
23
23
  async function runProjectCompleteState(projectPath, options, context, dependencies) {
24
24
  try {
25
- const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
26
- const targetProjectPath = resolveTargetProjectPath(repoRoot, projectPath);
27
- if (!(await dependencies.dirExists(targetProjectPath))) {
28
- throw new CliError(`Project not found: ${projectPath}`, 1);
29
- }
30
- const statePath = join(targetProjectPath, 'state.md');
31
- if (!(await dependencies.fileExists(statePath))) {
32
- throw new CliError(`Project state.md not found: ${statePath}`, 1);
25
+ // Refuse the same closeout invariant `oat project closeout-check` reports,
26
+ // before any write: a configured, autonomous, or lite closeout cannot
27
+ // reach terminal completion without its complete durable snapshot.
28
+ const { targetProjectPath, statePath, stateContent: content, result, } = await checkProjectCloseout(projectPath, { autonomous: options.autonomous ?? false }, context, dependencies);
29
+ if (result.status === 'incomplete') {
30
+ throw new CliError(formatCloseoutRefusal(projectPath, result), 1);
33
31
  }
34
32
  const now = dependencies.now();
35
- const content = await dependencies.readFile(statePath, 'utf8');
36
33
  const updatedContent = renderCompletedProjectState(content, {
37
34
  archived: options.archived ?? false,
38
35
  nowUtc: now.toISOString(),
@@ -78,6 +75,7 @@ export function createProjectCompleteStateCommand(overrides = {}) {
78
75
  .description('Update a project state.md to the completed lifecycle shape')
79
76
  .argument('<project-path>', 'Project path to update')
80
77
  .option('--archived', 'Mark the completed project as archived locally')
78
+ .option('--autonomous', 'Treat the closeout as autonomous (also implied by OAT_AUTONOMOUS=1)')
81
79
  .action(async (projectPath, options, command) => {
82
80
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
83
81
  await runProjectCompleteState(projectPath, options, context, dependencies);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAI9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,MAAM,WAAW,kCAAkC;IACjD,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AA0ID,wBAAgB,4BAA4B,CAC1C,SAAS,GAAE,OAAO,CAAC,kCAAkC,CAAM,GAC1D,OAAO,CA4BT"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/dispatch/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,MAAM,WAAW,kCAAkC;IACjD,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAClC;AAiHD,wBAAgB,4BAA4B,CAC1C,SAAS,GAAE,OAAO,CAAC,kCAAkC,CAAM,GAC1D,OAAO,CA0BT"}
@@ -1,9 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { isAbsolute, join } from 'node:path';
3
2
  import { buildCommandContext, } from '../../../app/command-context.js';
4
3
  import { readGlobalOptions } from '../../shared/shared.utils.js';
5
- import { fileExists } from '../../../fs/io.js';
6
- import { resolveProjectRoot, validateRealPathWithinScope } from '../../../fs/paths.js';
4
+ import { resolveProjectRoot } from '../../../fs/paths.js';
7
5
  import { Command } from 'commander';
8
6
  import { createCanonicalRoleCommand } from './canonical-role.js';
9
7
  import { recordProjectDispatch, redactDispatchMessage, } from './record.js';
@@ -19,16 +17,6 @@ const DEFAULT_DEPENDENCIES = {
19
17
  return Buffer.concat(chunks).toString('utf8');
20
18
  },
21
19
  };
22
- async function resolveProjectPath(repoRoot, project) {
23
- if (!project)
24
- return null;
25
- const candidate = isAbsolute(project) ? project : join(repoRoot, project);
26
- const validated = await validateRealPathWithinScope(candidate, repoRoot);
27
- if (!(await fileExists(join(validated.realPath, 'state.md')))) {
28
- throw new Error('Project path must contain a readable state.md.');
29
- }
30
- return validated.realPath;
31
- }
32
20
  function axes(values) {
33
21
  return values
34
22
  .map(([name, value]) => `${name}=${value ?? 'not-reported'}`)
@@ -65,19 +53,14 @@ function runtimeIdentityLines(identity) {
65
53
  }
66
54
  async function runRecordCommand(options, context, dependencies) {
67
55
  let repoRoot = null;
68
- let projectPath = null;
69
56
  try {
70
57
  repoRoot = await dependencies.resolveProjectRoot(context.cwd);
71
- projectPath = await resolveProjectPath(repoRoot, options.project);
72
58
  const content = options.eventFile === '-'
73
59
  ? await dependencies.readStdin()
74
60
  : await dependencies.readFile(options.eventFile);
75
61
  // Hand the raw event to the recorder; it owns the single authoritative
76
62
  // parse, and parsing twice would relabel the provenance of its own output.
77
- const raw = await recordProjectDispatch({
78
- projectPath,
79
- input: JSON.parse(content),
80
- });
63
+ const raw = await recordProjectDispatch({ input: JSON.parse(content) });
81
64
  // The degradation reason is caller-influenced text on the success path, so
82
65
  // it goes through the same single redaction boundary as every failure
83
66
  // message. Producing a message that skips this boundary is exactly the
@@ -89,7 +72,6 @@ async function runRecordCommand(options, context, dependencies) {
89
72
  reason: raw.runtimeIdentity.reason === null
90
73
  ? null
91
74
  : redactDispatchMessage(raw.runtimeIdentity.reason, {
92
- project: projectPath,
93
75
  repo: repoRoot,
94
76
  home: context.home,
95
77
  }),
@@ -99,12 +81,7 @@ async function runRecordCommand(options, context, dependencies) {
99
81
  context.logger.json(result);
100
82
  }
101
83
  else {
102
- if (result.status === 'persisted') {
103
- context.logger.success(`Recorded project dispatch: ${result.path}`);
104
- }
105
- else {
106
- context.logger.info('Dispatch evidence is valid; no project path was supplied, so nothing was persisted.');
107
- }
84
+ context.logger.info('Dispatch evidence is valid; nothing was written.');
108
85
  for (const line of runtimeIdentityLines(result.runtimeIdentity)) {
109
86
  context.logger.info(line);
110
87
  }
@@ -113,7 +90,7 @@ async function runRecordCommand(options, context, dependencies) {
113
90
  }
114
91
  catch (error) {
115
92
  // Single redaction boundary for every failure this command can surface.
116
- const message = redactDispatchMessage(error instanceof Error ? error.message : String(error), { project: projectPath, repo: repoRoot, home: context.home });
93
+ const message = redactDispatchMessage(error instanceof Error ? error.message : String(error), { repo: repoRoot, home: context.home });
117
94
  if (context.json) {
118
95
  context.logger.json({ status: 'error', message });
119
96
  }
@@ -126,10 +103,9 @@ async function runRecordCommand(options, context, dependencies) {
126
103
  export function createProjectDispatchCommand(overrides = {}) {
127
104
  const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
128
105
  return new Command('dispatch')
129
- .description('Validate and persist project dispatch provenance')
106
+ .description('Validate project dispatch provenance')
130
107
  .addCommand(new Command('record')
131
- .description('Record one generic dispatch plus namespaced OAT evidence')
132
- .option('--project <project-path>', 'Project path; omit to validate without persistence')
108
+ .description('Validate one generic dispatch plus namespaced OAT evidence (writes nothing)')
133
109
  .requiredOption('--event-file <json-file-or-dash>', 'Complete record and event JSON file, or - for standard input')
134
110
  .action(async (options, command) => {
135
111
  const context = dependencies.buildCommandContext(readGlobalOptions(command));