@open-agent-toolkit/cli 0.3.19 → 0.3.20

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 (119) hide show
  1. package/README.md +2 -1
  2. package/assets/agents/oat-phase-implementer.md +26 -3
  3. package/assets/agents/oat-reviewer.md +8 -3
  4. package/assets/bundle-metadata.json +1 -1
  5. package/assets/docs/reference/cli-reference.md +12 -7
  6. package/assets/docs/reference/oat-directory-structure.md +1 -0
  7. package/assets/docs/reference/project-artifacts.md +56 -1
  8. package/assets/docs/reference/project-state-machine.md +41 -7
  9. package/assets/docs/skills/brainstorm.md +3 -2
  10. package/assets/docs/skills/index.md +13 -10
  11. package/assets/docs/workflows/advanced/autonomy.md +3 -1
  12. package/assets/docs/workflows/advanced/dispatch-ceiling.md +2 -2
  13. package/assets/docs/workflows/advanced/workflow-gates.md +17 -4
  14. package/assets/docs/workflows/approvals-and-automation.md +21 -10
  15. package/assets/docs/workflows/choose-workflow.md +17 -9
  16. package/assets/docs/workflows/ideas/index.md +1 -1
  17. package/assets/docs/workflows/ideas/lifecycle.md +3 -2
  18. package/assets/docs/workflows/projects/closeout/closeout-skills.md +80 -7
  19. package/assets/docs/workflows/projects/closeout/pr-flow.md +7 -1
  20. package/assets/docs/workflows/projects/execution/execution-skills.md +16 -3
  21. package/assets/docs/workflows/projects/execution/implementation-execution.md +1 -1
  22. package/assets/docs/workflows/projects/execution/picking-up-projects.md +11 -1
  23. package/assets/docs/workflows/projects/lifecycle.md +108 -9
  24. package/assets/docs/workflows/projects/planning/hill-checkpoints.md +6 -6
  25. package/assets/docs/workflows/projects/planning/planning-skills.md +9 -7
  26. package/assets/docs/workflows/projects/planning/starting-projects.md +57 -11
  27. package/assets/docs/workflows/projects/reviews/index.md +22 -1
  28. package/assets/docs/workflows/projects/reviews/review-flavors.md +5 -3
  29. package/assets/public-package-versions.json +4 -4
  30. package/assets/skills/oat-brainstorm/SKILL.md +34 -9
  31. package/assets/skills/oat-brainstorm/references/destinations.md +6 -3
  32. package/assets/skills/oat-docs/SKILL.md +7 -3
  33. package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
  34. package/assets/skills/oat-project-autonomous/SKILL.md +9 -3
  35. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +151 -132
  36. package/assets/skills/oat-project-brief/SKILL.md +1 -1
  37. package/assets/skills/oat-project-brief/references/brief-contract.md +458 -9
  38. package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +151 -132
  39. package/assets/skills/oat-project-brief/references/docs/gate-approval-record.md +9 -0
  40. package/assets/skills/oat-project-capture/SKILL.md +2 -2
  41. package/assets/skills/oat-project-complete/SKILL.md +14 -5
  42. package/assets/skills/oat-project-discover/SKILL.md +3 -2
  43. package/assets/skills/oat-project-document/SKILL.md +12 -6
  44. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +151 -132
  45. package/assets/skills/oat-project-goal/SKILL.md +697 -0
  46. package/assets/skills/oat-project-goal/references/docs/autonomy-contract.md +412 -0
  47. package/assets/skills/oat-project-goal/references/docs/gate-approval-record.md +93 -0
  48. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  49. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +67 -0
  50. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +151 -132
  51. package/assets/skills/oat-project-implement/references/docs/gate-approval-record.md +9 -0
  52. package/assets/skills/oat-project-implement/references/phase-execution.md +10 -3
  53. package/assets/skills/oat-project-implement/references/plan-and-resume.md +28 -1
  54. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  55. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +151 -132
  56. package/assets/skills/oat-project-new/SKILL.md +3 -3
  57. package/assets/skills/oat-project-next/SKILL.md +45 -15
  58. package/assets/skills/oat-project-next/references/docs/gate-approval-record.md +9 -0
  59. package/assets/skills/oat-project-plan/SKILL.md +14 -3
  60. package/assets/skills/oat-project-plan-writing/SKILL.md +6 -4
  61. package/assets/skills/oat-project-pr-final/SKILL.md +51 -1
  62. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +151 -132
  63. package/assets/skills/oat-project-pr-progress/SKILL.md +97 -3
  64. package/assets/skills/oat-project-progress/SKILL.md +38 -5
  65. package/assets/skills/oat-project-progress/references/docs/gate-approval-record.md +9 -0
  66. package/assets/skills/oat-project-quick-start/SKILL.md +14 -9
  67. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +151 -132
  68. package/assets/skills/oat-project-quick-start/references/docs/gate-approval-record.md +9 -0
  69. package/assets/skills/oat-project-reconcile/SKILL.md +31 -2
  70. package/assets/skills/oat-project-review-provide/SKILL.md +79 -13
  71. package/assets/skills/oat-project-review-receive/SKILL.md +70 -1
  72. package/assets/skills/oat-project-revise/SKILL.md +25 -1
  73. package/assets/skills/oat-project-summary/SKILL.md +60 -5
  74. package/assets/skills/oat-wave-execute/SKILL.md +2 -2
  75. package/assets/templates/state.md +1 -1
  76. package/dist/commands/project/closeout-check/closeout-invariant.d.ts +119 -3
  77. package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -1
  78. package/dist/commands/project/closeout-check/closeout-invariant.js +454 -2
  79. package/dist/commands/project/closeout-check/index.d.ts +21 -3
  80. package/dist/commands/project/closeout-check/index.d.ts.map +1 -1
  81. package/dist/commands/project/closeout-check/index.js +190 -4
  82. package/dist/commands/project/complete-state/index.d.ts +2 -1
  83. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  84. package/dist/commands/project/complete-state/index.js +7 -1
  85. package/dist/commands/project/delivery/check.d.ts +66 -0
  86. package/dist/commands/project/delivery/check.d.ts.map +1 -0
  87. package/dist/commands/project/delivery/check.js +232 -0
  88. package/dist/commands/project/delivery/evidence.d.ts +171 -0
  89. package/dist/commands/project/delivery/evidence.d.ts.map +1 -0
  90. package/dist/commands/project/delivery/evidence.js +902 -0
  91. package/dist/commands/project/delivery/index.d.ts +7 -0
  92. package/dist/commands/project/delivery/index.d.ts.map +1 -0
  93. package/dist/commands/project/delivery/index.js +13 -0
  94. package/dist/commands/project/delivery/refresh.d.ts +110 -0
  95. package/dist/commands/project/delivery/refresh.d.ts.map +1 -0
  96. package/dist/commands/project/delivery/refresh.js +441 -0
  97. package/dist/commands/project/index.d.ts.map +1 -1
  98. package/dist/commands/project/index.js +2 -0
  99. package/dist/commands/project/log/append.d.ts +6 -0
  100. package/dist/commands/project/log/append.d.ts.map +1 -1
  101. package/dist/commands/project/log/append.js +6 -1
  102. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  103. package/dist/commands/project/new/scaffold.js +47 -1
  104. package/dist/commands/project/promote/promote.d.ts +15 -1
  105. package/dist/commands/project/promote/promote.d.ts.map +1 -1
  106. package/dist/commands/project/promote/promote.js +99 -15
  107. package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
  108. package/dist/commands/project/validate-plan/index.js +10 -0
  109. package/dist/commands/project/validate-plan/profiles.d.ts +19 -4
  110. package/dist/commands/project/validate-plan/profiles.d.ts.map +1 -1
  111. package/dist/commands/project/validate-plan/profiles.js +153 -2
  112. package/dist/commands/shared/frontmatter.d.ts +1 -1
  113. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  114. package/dist/commands/shared/frontmatter.js +1 -0
  115. package/dist/commands/state/generate.d.ts.map +1 -1
  116. package/dist/commands/state/generate.js +35 -4
  117. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  118. package/dist/commands/tools/shared/pack-manifest.js +1 -0
  119. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { parseBriefOutcomes, parsePhaseMarkers, parseTaskProgress, validateGoalOutcomeAudit, } from '@open-agent-toolkit/control-plane';
1
2
  import YAML, { isMap } from 'yaml';
2
3
  /**
3
4
  * The closeout invariant shared by `oat project closeout-check` and
@@ -12,6 +13,13 @@ import YAML, { isMap } from 'yaml';
12
13
  * persists. Once a snapshot exists it is authoritative: current configuration
13
14
  * is never consulted, so a later config change cannot invalidate a persisted
14
15
  * run.
16
+ *
17
+ * A goal project additionally passes the goal terminal boundary before either
18
+ * success return (`complete` or `not_required`): the terminal declaration, an
19
+ * exhaustive outcome audit validated by the control plane's single audit
20
+ * predicate, resolved extraction and delivery coverage over current
21
+ * observations. A complete snapshot cannot waive it, and every refusal names
22
+ * the actual next owner.
15
23
  */
16
24
  export const SEQUENCE_FIELD = 'oat_post_implement_sequence';
17
25
  export const RECOVERY_SKILL = 'oat-project-implement';
@@ -37,6 +45,392 @@ export const STEP_SKILLS = {
37
45
  pr: 'oat-project-pr-final',
38
46
  retro: 'oat-project-retro',
39
47
  };
48
+ const GOAL_SKILL = 'oat-project-goal';
49
+ const DELIVERY_SKILL = 'oat-project-pr-progress';
50
+ const TERMINAL_DELIVERY_SKILL = 'oat-project-pr-final';
51
+ const REVIEW_SKILL = 'oat-project-review-provide';
52
+ /** The next owner for each refusal reason (A3 diagnostics). */
53
+ const REASON_OWNERS = {
54
+ 'plan-unreadable': GOAL_SKILL,
55
+ 'journal-unreadable': GOAL_SKILL,
56
+ 'registry-unreadable': DELIVERY_SKILL,
57
+ 'not-declared': GOAL_SKILL,
58
+ 'audit-invalid': GOAL_SKILL,
59
+ 'extraction-target-mismatch': GOAL_SKILL,
60
+ 'child-missing': GOAL_SKILL,
61
+ 'child-backlink-mismatch': GOAL_SKILL,
62
+ 'child-incomplete': GOAL_SKILL,
63
+ 'child-deferral-under-met': GOAL_SKILL,
64
+ 'member-unobserved': DELIVERY_SKILL,
65
+ 'delivery-duplicate-id': DELIVERY_SKILL,
66
+ 'delivery-self-link': DELIVERY_SKILL,
67
+ 'delivery-missing-target': DELIVERY_SKILL,
68
+ 'delivery-link-mismatch': DELIVERY_SKILL,
69
+ 'delivery-conflicting-predecessor': DELIVERY_SKILL,
70
+ 'delivery-source-not-superseded': DELIVERY_SKILL,
71
+ 'delivery-missing-link': DELIVERY_SKILL,
72
+ 'delivery-cycle': DELIVERY_SKILL,
73
+ 'delivery-replaced-after-endpoint': DELIVERY_SKILL,
74
+ 'correction-claims-inherited-scope': DELIVERY_SKILL,
75
+ 'delivery-registering': DELIVERY_SKILL,
76
+ 'terminal-scopes-overlap': DELIVERY_SKILL,
77
+ 'endpoint-unmet': DELIVERY_SKILL,
78
+ 'evidence-unevaluated': DELIVERY_SKILL,
79
+ 'review-not-fresh': REVIEW_SKILL,
80
+ 'exit-carrier-not-fresh': RECOVERY_SKILL,
81
+ 'scope-claim-unknown': DELIVERY_SKILL,
82
+ 'scope-claim-unaccepted': DELIVERY_SKILL,
83
+ 'phase-uncovered': TERMINAL_DELIVERY_SKILL,
84
+ };
85
+ function goalReason(code, message, subject) {
86
+ return subject === undefined ? { code, message } : { code, message, subject };
87
+ }
88
+ /**
89
+ * Validate the supersession graph and resolve every replacement chain to its
90
+ * terminal delivery (A4). In v1.5 each replacement has at most one direct
91
+ * predecessor: `covers_scope_of` names it and the predecessor's
92
+ * `superseded_by` points back. Missing targets, self-links, cycles,
93
+ * conflicting predecessors, mismatched links, a non-superseded source and a
94
+ * replacement of a delivery that already reached its endpoint are refused.
95
+ * `corrects_delivery_id` is additive and inherits no coverage, so a
96
+ * correction may not claim its target's scope.
97
+ */
98
+ export function resolveDeliveryGraph(deliveries, phaseOrder = []) {
99
+ const errors = [];
100
+ const byId = new Map();
101
+ for (const delivery of deliveries) {
102
+ if (byId.has(delivery.delivery_id)) {
103
+ errors.push(goalReason('delivery-duplicate-id', `delivery id ${delivery.delivery_id} is registered more than once`, delivery.delivery_id));
104
+ continue;
105
+ }
106
+ byId.set(delivery.delivery_id, delivery);
107
+ }
108
+ const replacedBy = new Map();
109
+ for (const delivery of byId.values()) {
110
+ const id = delivery.delivery_id;
111
+ const predecessorId = delivery.covers_scope_of;
112
+ if (predecessorId !== null) {
113
+ const predecessor = byId.get(predecessorId);
114
+ if (predecessorId === id) {
115
+ errors.push(goalReason('delivery-self-link', `${id} covers its own scope`, id));
116
+ }
117
+ else if (predecessor === undefined) {
118
+ errors.push(goalReason('delivery-missing-target', `${id} covers the scope of ${predecessorId}, which is not registered`, id));
119
+ }
120
+ else {
121
+ const existing = replacedBy.get(predecessorId);
122
+ if (existing !== undefined) {
123
+ errors.push(goalReason('delivery-conflicting-predecessor', `${predecessorId} is replaced by both ${existing} and ${id}`, predecessorId));
124
+ }
125
+ else {
126
+ replacedBy.set(predecessorId, id);
127
+ }
128
+ if (predecessor.superseded_by !== id) {
129
+ errors.push(goalReason('delivery-link-mismatch', `${id} covers ${predecessorId}, but ${predecessorId} records superseded_by ${predecessor.superseded_by ?? 'null'}`, id));
130
+ }
131
+ if (predecessor.status !== 'superseded') {
132
+ errors.push(goalReason('delivery-source-not-superseded', `${id} replaces ${predecessorId}, whose status is ${predecessor.status ?? 'unrecorded'}, not superseded`, predecessorId));
133
+ }
134
+ if (predecessor.endpoint_attainment !== null) {
135
+ errors.push(goalReason('delivery-replaced-after-endpoint', `${predecessorId} already reached its agreed endpoint; later corrections are additive and may not replace it`, predecessorId));
136
+ }
137
+ }
138
+ }
139
+ const successorId = delivery.superseded_by;
140
+ if (successorId !== null) {
141
+ const successor = byId.get(successorId);
142
+ if (successorId === id) {
143
+ errors.push(goalReason('delivery-self-link', `${id} supersedes itself`, id));
144
+ }
145
+ else if (successor === undefined) {
146
+ errors.push(goalReason('delivery-missing-target', `${id} is superseded by ${successorId}, which is not registered`, id));
147
+ }
148
+ else if (successor.covers_scope_of !== id) {
149
+ errors.push(goalReason('delivery-link-mismatch', `${id} is superseded by ${successorId}, but ${successorId} covers ${successor.covers_scope_of ?? 'nothing'}`, id));
150
+ }
151
+ }
152
+ else if (delivery.status === 'superseded') {
153
+ errors.push(goalReason('delivery-missing-link', `${id} is superseded but names no replacement`, id));
154
+ }
155
+ }
156
+ // Cycles: follow each predecessor chain.
157
+ for (const delivery of byId.values()) {
158
+ const seen = new Set();
159
+ let current = delivery;
160
+ while (current !== undefined && current.covers_scope_of !== null) {
161
+ if (seen.has(current.delivery_id)) {
162
+ errors.push(goalReason('delivery-cycle', `the replacement chain through ${delivery.delivery_id} is a cycle`, delivery.delivery_id));
163
+ break;
164
+ }
165
+ seen.add(current.delivery_id);
166
+ current = byId.get(current.covers_scope_of);
167
+ }
168
+ }
169
+ if (errors.length > 0) {
170
+ return { terminals: [], errors: dedupe(errors) };
171
+ }
172
+ const order = new Map(phaseOrder.map((phaseId, index) => [phaseId, index]));
173
+ const rank = (phaseId) => order.get(phaseId) ?? order.size;
174
+ const terminals = [];
175
+ for (const delivery of byId.values()) {
176
+ if (delivery.status === 'superseded')
177
+ continue;
178
+ const scope = new Set(delivery.scope);
179
+ const inherits = [];
180
+ let predecessorId = delivery.covers_scope_of;
181
+ while (predecessorId !== null) {
182
+ const predecessor = byId.get(predecessorId);
183
+ inherits.push(predecessorId);
184
+ for (const phaseId of predecessor.scope)
185
+ scope.add(phaseId);
186
+ predecessorId = predecessor.covers_scope_of;
187
+ }
188
+ terminals.push({
189
+ delivery,
190
+ effectiveScope: [...scope].sort((left, right) => rank(left) - rank(right)),
191
+ inherits,
192
+ });
193
+ }
194
+ for (const delivery of byId.values()) {
195
+ const targetId = delivery.corrects_delivery_id;
196
+ if (targetId === null)
197
+ continue;
198
+ const target = byId.get(targetId);
199
+ if (targetId === delivery.delivery_id) {
200
+ errors.push(goalReason('delivery-self-link', `${delivery.delivery_id} corrects itself`, delivery.delivery_id));
201
+ continue;
202
+ }
203
+ if (target === undefined) {
204
+ errors.push(goalReason('delivery-missing-target', `${delivery.delivery_id} corrects ${targetId}, which is not registered`, delivery.delivery_id));
205
+ continue;
206
+ }
207
+ const targetScope = new Set(target.scope);
208
+ for (const terminal of terminals) {
209
+ if (terminal.delivery.delivery_id === targetId ||
210
+ terminal.inherits.includes(targetId)) {
211
+ for (const phaseId of terminal.effectiveScope)
212
+ targetScope.add(phaseId);
213
+ }
214
+ }
215
+ const claimed = delivery.scope.filter((phaseId) => targetScope.has(phaseId));
216
+ if (claimed.length > 0) {
217
+ errors.push(goalReason('correction-claims-inherited-scope', `${delivery.delivery_id} is an additive correction of ${targetId} but claims its scope (${claimed.join(', ')}); corrections use new phase ids`, delivery.delivery_id));
218
+ }
219
+ }
220
+ return { terminals: errors.length > 0 ? [] : terminals, errors };
221
+ }
222
+ function dedupe(reasons) {
223
+ const seen = new Set();
224
+ return reasons.filter((entry) => {
225
+ const key = `${entry.code}:${entry.subject ?? ''}:${entry.message}`;
226
+ if (seen.has(key))
227
+ return false;
228
+ seen.add(key);
229
+ return true;
230
+ });
231
+ }
232
+ function sameMembers(left, right) {
233
+ return (left.length === right.length &&
234
+ [...left].sort().join(',') === [...right].sort().join(','));
235
+ }
236
+ /** Endpoint satisfaction on observed member state (design "Endpoint satisfaction"). */
237
+ function endpointObserved(delivery) {
238
+ const { endpoint, members } = delivery;
239
+ if (members.length === 0 || endpoint === null)
240
+ return false;
241
+ return endpoint === 'merged'
242
+ ? members.every((member) => member.status === 'merged')
243
+ : members.every((member) => member.status === 'open' || member.status === 'merged');
244
+ }
245
+ /**
246
+ * The goal delivery coverage predicate (A4), over current observations:
247
+ * no registering entries; a valid supersession graph whose terminal
248
+ * deliveries have disjoint effective scopes; every terminal delivery at its
249
+ * agreed endpoint with a passed, fresh integrated review and an allowed,
250
+ * fresh exit-gate result evaluated against its entire effective scope; and
251
+ * every audited-accepted phase in exactly one terminal effective scope, with
252
+ * no claim of an unknown or unaccepted phase.
253
+ */
254
+ export function evaluateGoalCoverage(input) {
255
+ const reasons = [];
256
+ for (const delivery of input.deliveries) {
257
+ if (delivery.status === 'registering') {
258
+ reasons.push(goalReason('delivery-registering', `${delivery.delivery_id} is still registering; resume its idempotent registration`, delivery.delivery_id));
259
+ }
260
+ }
261
+ const graph = resolveDeliveryGraph(input.deliveries, input.phaseOrder);
262
+ if (graph.errors.length > 0) {
263
+ return [...reasons, ...graph.errors];
264
+ }
265
+ const owners = new Map();
266
+ const known = new Set(input.phaseOrder);
267
+ for (const terminal of graph.terminals) {
268
+ const id = terminal.delivery.delivery_id;
269
+ if (terminal.delivery.status === 'registering')
270
+ continue;
271
+ for (const phaseId of terminal.effectiveScope) {
272
+ const owner = owners.get(phaseId);
273
+ if (owner !== undefined) {
274
+ reasons.push(goalReason('terminal-scopes-overlap', `${phaseId} is in the effective scope of both ${owner} and ${id}`, phaseId));
275
+ }
276
+ else {
277
+ owners.set(phaseId, id);
278
+ }
279
+ if (!known.has(phaseId)) {
280
+ reasons.push(goalReason('scope-claim-unknown', `${id} claims ${phaseId}, which the brief does not declare`, phaseId));
281
+ }
282
+ else if (!input.acceptedPhases.has(phaseId)) {
283
+ reasons.push(goalReason('scope-claim-unaccepted', `${id} claims ${phaseId}, which the audit does not record as accepted`, phaseId));
284
+ }
285
+ }
286
+ if (!endpointObserved(terminal.delivery)) {
287
+ reasons.push(goalReason('endpoint-unmet', `${id} is not observed at its agreed endpoint (${terminal.delivery.endpoint ?? 'unrecorded'})`, id));
288
+ }
289
+ const fact = input.evidence[id];
290
+ if (fact === undefined ||
291
+ !sameMembers(fact.scope, terminal.effectiveScope)) {
292
+ reasons.push(goalReason('evidence-unevaluated', `${id}'s evidence was not evaluated against its effective scope (${terminal.effectiveScope.join(', ')})`, id));
293
+ continue;
294
+ }
295
+ if (!fact.review.fresh || fact.review.disposition !== 'passed') {
296
+ reasons.push(goalReason('review-not-fresh', `${id} has no passed, fresh integrated review over ${terminal.effectiveScope.join(', ')}: ${fact.review.reason ?? `disposition ${fact.review.disposition ?? 'unrecorded'}`}`, id));
297
+ }
298
+ if (fact.exit.outcome !== 'allowed' || fact.exit.freshness !== 'fresh') {
299
+ reasons.push(goalReason('exit-carrier-not-fresh', `${id}'s exit-gate result is ${fact.exit.outcome}, ${fact.exit.freshness}${fact.exit.reason ? `: ${fact.exit.reason}` : ''}`, id));
300
+ }
301
+ }
302
+ for (const phaseId of input.phaseOrder) {
303
+ if (input.acceptedPhases.has(phaseId) && !owners.has(phaseId)) {
304
+ reasons.push(goalReason('phase-uncovered', `accepted phase ${phaseId} is in no terminal delivery's effective scope`, phaseId));
305
+ }
306
+ }
307
+ return reasons;
308
+ }
309
+ /**
310
+ * The goal terminal boundary (A3, A4): declaration, an exhaustive audit
311
+ * validated through the single audit predicate, resolved extraction, member
312
+ * observation and delivery coverage, in that order. Returns null when every
313
+ * check holds. Pure: every input arrives as data.
314
+ */
315
+ export function evaluateGoalBoundary(frontmatter, goal) {
316
+ if (goal === undefined) {
317
+ return {
318
+ invariant: 'goal_inputs_unreadable',
319
+ reasons: [
320
+ goalReason('registry-unreadable', 'the goal boundary inputs were not loaded'),
321
+ ],
322
+ };
323
+ }
324
+ const unreadable = [];
325
+ if (goal.planContent === null) {
326
+ unreadable.push(goalReason('plan-unreadable', 'plan.md could not be read'));
327
+ }
328
+ if (goal.implementationContent === null) {
329
+ unreadable.push(goalReason('journal-unreadable', 'implementation.md could not be read'));
330
+ }
331
+ if (goal.deliveries === null) {
332
+ unreadable.push(goalReason('registry-unreadable', 'the delivery registry could not be read'));
333
+ }
334
+ if (unreadable.length > 0 ||
335
+ goal.planContent === null ||
336
+ goal.implementationContent === null ||
337
+ goal.deliveries === null) {
338
+ return { invariant: 'goal_inputs_unreadable', reasons: unreadable };
339
+ }
340
+ if (frontmatter.oat_goal_status !== 'declared_met') {
341
+ return {
342
+ invariant: 'goal_not_declared',
343
+ reasons: [
344
+ goalReason('not-declared', '`oat_goal_status` is not `declared_met`; only oat-project-goal records the terminal declaration'),
345
+ ],
346
+ };
347
+ }
348
+ const inventory = parseTaskProgress(goal.planContent, goal.implementationContent, { phaseUnits: true, extractedUnits: true }).phases;
349
+ const validation = validateGoalOutcomeAudit(frontmatter.oat_goal_outcome_audit, inventory, parseBriefOutcomes(goal.planContent));
350
+ if (!validation.valid || validation.audit === null) {
351
+ return {
352
+ invariant: 'goal_audit_invalid',
353
+ reasons: validation.errors.map((error) => goalReason('audit-invalid', `${error.code}: ${error.message}`, error.subject)),
354
+ };
355
+ }
356
+ const extraction = evaluateExtraction(goal, goal.planContent, inventory, validation.audit);
357
+ if (extraction.length > 0) {
358
+ return { invariant: 'goal_extraction_unresolved', reasons: extraction };
359
+ }
360
+ if (goal.observationFailures.length > 0) {
361
+ return {
362
+ invariant: 'goal_observation_failed',
363
+ reasons: goal.observationFailures.map((failure) => goalReason('member-unobserved', `${failure}; failure to observe a member is not endpoint success`)),
364
+ };
365
+ }
366
+ const acceptedPhases = new Set(Object.entries(validation.audit.phases)
367
+ .filter(([, entry]) => entry.disposition === 'accepted')
368
+ .map(([phaseId]) => phaseId));
369
+ const coverage = evaluateGoalCoverage({
370
+ deliveries: goal.deliveries,
371
+ phaseOrder: inventory.map((phase) => phase.phaseId),
372
+ acceptedPhases,
373
+ evidence: goal.evidence,
374
+ });
375
+ return coverage.length > 0
376
+ ? { invariant: 'goal_coverage_incomplete', reasons: coverage }
377
+ : null;
378
+ }
379
+ /**
380
+ * Every extracted phase needs matching plan and journal targets and a child
381
+ * carrying the matching `oat_extracted_from` backlink that is complete, or
382
+ * whose outstanding obligation the audit defers with a reason and scope
383
+ * decision under `met_with_deferrals`.
384
+ */
385
+ function evaluateExtraction(goal, planContent, inventory, audit) {
386
+ const reasons = [];
387
+ const planMarkers = parsePhaseMarkers(planContent);
388
+ for (const phase of inventory) {
389
+ const planTarget = planMarkers.get(phase.phaseId)?.extractedTo ?? null;
390
+ const journalTarget = phase.extracted?.to ?? null;
391
+ if (planTarget !== journalTarget) {
392
+ reasons.push(goalReason('extraction-target-mismatch', `${phase.phaseId}: the brief names ${planTarget ?? 'no extraction target'} but the journal names ${journalTarget ?? 'none'}`, phase.phaseId));
393
+ continue;
394
+ }
395
+ if (journalTarget === null)
396
+ continue;
397
+ const child = goal.children.find((entry) => entry.slug === journalTarget);
398
+ if (child === undefined || child.status !== 'found') {
399
+ reasons.push(goalReason('child-missing', `${phase.phaseId} is extracted to ${journalTarget}, but that child project is ${child?.status ?? 'missing'}`, phase.phaseId));
400
+ continue;
401
+ }
402
+ if (child.extractedFrom !== goal.goalSlug) {
403
+ reasons.push(goalReason('child-backlink-mismatch', `${journalTarget} records oat_extracted_from ${child.extractedFrom ?? 'null'}, not ${goal.goalSlug}`, phase.phaseId));
404
+ continue;
405
+ }
406
+ if (child.lifecycle === 'complete')
407
+ continue;
408
+ const entry = audit.phases[phase.phaseId];
409
+ const deferred = entry !== undefined &&
410
+ (entry.reason ?? '').trim() !== '' &&
411
+ (entry.scope_decision_ref ?? '').trim() !== '';
412
+ if (!deferred) {
413
+ reasons.push(goalReason('child-incomplete', `${journalTarget} is ${child.lifecycle ?? 'not complete'} and the audit records no approved deferral (reason and scope_decision_ref) for ${phase.phaseId}`, phase.phaseId));
414
+ }
415
+ else if (audit.disposition === 'met') {
416
+ reasons.push(goalReason('child-deferral-under-met', `${phase.phaseId}'s child obligation is deferred, so the audit must be met_with_deferrals`, phase.phaseId));
417
+ }
418
+ }
419
+ return reasons;
420
+ }
421
+ function goalRefusal(verdict, inputs) {
422
+ const skill = REASON_OWNERS[verdict.reasons[0]?.code ?? 'registry-unreadable'];
423
+ return {
424
+ status: 'incomplete',
425
+ invariant: verdict.invariant,
426
+ detail: `the goal closeout boundary refuses: ${verdict.reasons
427
+ .map((entry) => `${entry.code} (${entry.message})`)
428
+ .join('; ')}`,
429
+ route: skill,
430
+ nextOwner: { kind: 'goal', skill, reasons: verdict.reasons },
431
+ inputs,
432
+ };
433
+ }
40
434
  class MalformedSnapshotError extends Error {
41
435
  }
42
436
  function malformed(detail) {
@@ -57,6 +451,16 @@ function readFrontmatter(content) {
57
451
  const value = document.toJS();
58
452
  return value;
59
453
  }
454
+ /** The state's `oat_workflow_mode`, or null when the frontmatter cannot be read. */
455
+ export function readStateWorkflowMode(stateContent) {
456
+ try {
457
+ const mode = readFrontmatter(stateContent).oat_workflow_mode;
458
+ return typeof mode === 'string' ? mode : null;
459
+ }
460
+ catch {
461
+ return null;
462
+ }
463
+ }
60
464
  function readEnum(record, key, allowed, options = {}) {
61
465
  if (!(key in record)) {
62
466
  if (options.optional)
@@ -104,7 +508,40 @@ function assertCompletedPrefix(stored, completed, storedKey, completedKey) {
104
508
  malformed(`\`${completedKey}\` is not a prefix of the stored \`${storedKey}\` order`);
105
509
  }
106
510
  }
107
- export function parseCloseoutSnapshot(value) {
511
+ const SATISFIED_BY_VALUES = ['delivery-coverage'];
512
+ /**
513
+ * Goal only: `satisfied_by` records a step completed by delivery coverage.
514
+ * Only the `pr` step may be satisfied that way, and only once execution of
515
+ * the stored order reached it, so the record must name a completed step.
516
+ */
517
+ function readSatisfiedBy(record, workflowMode, completed) {
518
+ if (!('satisfied_by' in record) || record.satisfied_by === null) {
519
+ return {};
520
+ }
521
+ const value = record.satisfied_by;
522
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
523
+ malformed('`satisfied_by` must be a map of step to `delivery-coverage`');
524
+ }
525
+ if (workflowMode !== 'goal') {
526
+ malformed('`satisfied_by` is recorded only by a goal closeout');
527
+ }
528
+ const satisfiedBy = {};
529
+ for (const [step, source] of Object.entries(value)) {
530
+ if (step !== 'pr') {
531
+ malformed(`\`satisfied_by\` names \`${step}\`; only the \`pr\` step is satisfied by delivery coverage`);
532
+ }
533
+ if (typeof source !== 'string' ||
534
+ !SATISFIED_BY_VALUES.includes(source)) {
535
+ malformed('`satisfied_by.pr` must be `delivery-coverage`');
536
+ }
537
+ if (!completed.includes('pr')) {
538
+ malformed("`satisfied_by.pr` is recorded before the `pr` step's position in the stored order was reached");
539
+ }
540
+ satisfiedBy.pr = 'delivery-coverage';
541
+ }
542
+ return satisfiedBy;
543
+ }
544
+ export function parseCloseoutSnapshot(value, workflowMode = null) {
108
545
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
109
546
  malformed('the snapshot must be a map');
110
547
  }
@@ -148,6 +585,10 @@ export function parseCloseoutSnapshot(value) {
148
585
  approval,
149
586
  postApproval,
150
587
  postApprovalCompleted,
588
+ satisfiedBy: readSatisfiedBy(record, workflowMode, [
589
+ ...preApprovalCompleted,
590
+ ...postApprovalCompleted,
591
+ ]),
151
592
  failure: record.failure ?? null,
152
593
  };
153
594
  if (status === 'complete') {
@@ -234,10 +675,18 @@ export async function evaluateCloseout(options) {
234
675
  snapshot: snapshotPresent ? 'present' : 'absent',
235
676
  snapshotSource: null,
236
677
  };
678
+ // Goal: declaration, an exhaustive audit and delivery coverage are required
679
+ // before either success return; a complete snapshot cannot waive them.
680
+ if (workflowMode === 'goal') {
681
+ const verdict = evaluateGoalBoundary(frontmatter, options.goal);
682
+ if (verdict !== null) {
683
+ return goalRefusal(verdict, inputs);
684
+ }
685
+ }
237
686
  if (snapshotPresent) {
238
687
  let snapshot;
239
688
  try {
240
- snapshot = parseCloseoutSnapshot(rawSnapshot);
689
+ snapshot = parseCloseoutSnapshot(rawSnapshot, workflowMode);
241
690
  }
242
691
  catch (error) {
243
692
  if (!(error instanceof MalformedSnapshotError))
@@ -289,6 +738,9 @@ export async function evaluateCloseout(options) {
289
738
  */
290
739
  export function formatCloseoutRefusal(projectPath, result) {
291
740
  const owner = result.nextOwner;
741
+ if (owner.kind === 'goal') {
742
+ return `Closeout invariant not satisfied for ${projectPath} (${result.invariant}): ${result.detail}. Resume with ${owner.skill}.`;
743
+ }
292
744
  const next = owner.kind === 'step'
293
745
  ? ` Next stored step: \`${owner.step}\` (${owner.skill}).`
294
746
  : owner.kind === 'approval'
@@ -1,10 +1,13 @@
1
1
  import { readFile as defaultReadFile } from 'node:fs/promises';
2
2
  import { buildCommandContext, type CommandContext } from '../../../app/command-context.js';
3
+ import { type GhRunner } from '../../project/links/refresh.js';
4
+ import { type GitRunner } from '../../project/sync/git.js';
3
5
  import { resolveEffectiveConfig } from '../../../config/resolve.js';
4
6
  import { dirExists, fileExists } from '../../../fs/io.js';
7
+ import { type ProjectState } from '@open-agent-toolkit/control-plane';
5
8
  import { Command } from 'commander';
6
- import { type CloseoutResult } from './closeout-invariant.js';
7
- export { evaluateCloseout, formatCloseoutRefusal, type CloseoutResult, } from './closeout-invariant.js';
9
+ import { type CloseoutResult, type GoalCloseoutInputs } from './closeout-invariant.js';
10
+ export { evaluateCloseout, evaluateGoalBoundary, evaluateGoalCoverage, formatCloseoutRefusal, resolveDeliveryGraph, type CloseoutResult, type DeliveryEvidenceFact, type ExtractedChildFact, type GoalCloseoutInputs, } from './closeout-invariant.js';
8
11
  export interface CloseoutCheckDependencies {
9
12
  buildCommandContext: (options: Parameters<typeof buildCommandContext>[0]) => CommandContext;
10
13
  resolveProjectRoot: (cwd: string) => Promise<string>;
@@ -17,8 +20,23 @@ export interface CloseoutCheckDependencies {
17
20
  * `--autonomous` under `OAT_AUTONOMOUS=1` still cannot fail open.
18
21
  */
19
22
  env: NodeJS.ProcessEnv;
23
+ /** Goal only: read-only member observation (`gh pr view`). */
24
+ gh: GhRunner;
25
+ /** Goal only: read-only Git inspection for the shared evidence evaluator. */
26
+ git: GitRunner;
27
+ getProjectState: (projectPath: string) => Promise<ProjectState>;
28
+ listDirectories: (path: string) => Promise<string[]>;
20
29
  }
21
30
  export declare const DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES: CloseoutCheckDependencies;
31
+ /** The dependencies `checkProjectCloseout` reads through. */
32
+ export type CloseoutInputDependencies = Pick<CloseoutCheckDependencies, 'resolveProjectRoot' | 'resolveEffectiveConfig' | 'readFile' | 'dirExists' | 'fileExists' | 'env' | 'gh' | 'git' | 'getProjectState' | 'listDirectories'>;
33
+ /**
34
+ * Load the goal boundary inputs read-only (A3): the brief, the journal, the
35
+ * registry with fresh member observations merged in (never persisted; that is
36
+ * `delivery refresh`'s job), each extracted child's facts, and the shared
37
+ * evaluator's verdict for every terminal delivery's effective scope.
38
+ */
39
+ export declare function loadGoalCloseoutInputs(repoRoot: string, targetProjectPath: string, dependencies: CloseoutInputDependencies): Promise<GoalCloseoutInputs>;
22
40
  export interface ProjectCloseoutCheck {
23
41
  repoRoot: string;
24
42
  targetProjectPath: string;
@@ -33,6 +51,6 @@ export interface ProjectCloseoutCheck {
33
51
  */
34
52
  export declare function checkProjectCloseout(projectPath: string, options: {
35
53
  autonomous: boolean;
36
- }, context: CommandContext, dependencies: Pick<CloseoutCheckDependencies, 'resolveProjectRoot' | 'resolveEffectiveConfig' | 'readFile' | 'dirExists' | 'fileExists' | 'env'>): Promise<ProjectCloseoutCheck>;
54
+ }, context: CommandContext, dependencies: CloseoutInputDependencies): Promise<ProjectCloseoutCheck>;
37
55
  export declare function createProjectCloseoutCheckCommand(overrides?: Partial<CloseoutCheckDependencies>): Command;
38
56
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/closeout-check/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAW,MAAM,kBAAkB,CAAC;AAGxE,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAQhF,OAAO,EAEL,KAAK,QAAQ,EACd,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAE9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE/C,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAKL,KAAK,cAAc,EAGnB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,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;IACvB,8DAA8D;IAC9D,EAAE,EAAE,QAAQ,CAAC;IACb,6EAA6E;IAC7E,GAAG,EAAE,SAAS,CAAC;IACf,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;IAChE,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACtD;AASD,eAAO,MAAM,mCAAmC,EAAE,yBAYjD,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,yBAAyB,EACvB,oBAAoB,GACpB,wBAAwB,GACxB,UAAU,GACV,WAAW,GACX,YAAY,GACZ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,iBAAiB,GACjB,iBAAiB,CACpB,CAAC;AA4EF;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,yBAAyB,GACtC,OAAO,CAAC,kBAAkB,CAAC,CAuH7B;AAED,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,yBAAyB,GACtC,OAAO,CAAC,oBAAoB,CAAC,CAoC/B;AAmED,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,yBAAyB,CAAM,GACjD,OAAO,CA2BT"}