@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.
- package/README.md +2 -1
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/meta.json +13 -0
- package/assets/docs/cli-utilities/tool-packs.md +8 -2
- package/assets/docs/cli-utilities/workflow-gates.md +13 -0
- package/assets/docs/contributing/code.md +8 -2
- package/assets/docs/contributing/documentation.md +7 -1
- package/assets/docs/contributing/meta.json +15 -0
- package/assets/docs/docs-tooling/add-docs-to-a-repo.md +14 -5
- package/assets/docs/docs-tooling/commands.md +26 -8
- package/assets/docs/docs-tooling/meta.json +4 -0
- package/assets/docs/docs-tooling/workflows.md +2 -2
- package/assets/docs/guide/meta.json +4 -0
- package/assets/docs/meta.json +14 -0
- package/assets/docs/provider-sync/instruction-sync.md +8 -0
- package/assets/docs/provider-sync/meta.json +11 -0
- package/assets/docs/provider-sync/scope-and-surface.md +3 -4
- package/assets/docs/reference/cli-reference.md +5 -2
- package/assets/docs/reference/docs-index-contract.md +23 -6
- package/assets/docs/reference/file-locations.md +7 -2
- package/assets/docs/reference/index.md +1 -1
- package/assets/docs/reference/meta.json +10 -0
- package/assets/docs/reference/troubleshooting.md +41 -6
- package/assets/docs/workflows/ideas/meta.json +4 -0
- package/assets/docs/workflows/meta.json +4 -0
- package/assets/docs/workflows/projects/cursor-cloud.md +8 -3
- package/assets/docs/workflows/projects/evidence-layers.md +6 -8
- package/assets/docs/workflows/projects/implementation-execution.md +30 -4
- package/assets/docs/workflows/projects/lifecycle.md +7 -1
- package/assets/docs/workflows/projects/meta.json +25 -0
- package/assets/docs/workflows/projects/orchestration-model.md +5 -6
- package/assets/docs/workflows/skills/meta.json +10 -0
- package/assets/docs/workflows/skills/recon.md +31 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-cursor-cloud-projects/SKILL.md +30 -16
- package/assets/skills/oat-dispatch-subagents/SKILL.md +4 -12
- package/assets/skills/oat-dispatch-subagents/references/managed-claude-example.json +1 -1
- package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +51 -0
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +8 -7
- package/assets/skills/oat-docs-analyze/SKILL.md +28 -3
- package/assets/skills/oat-docs-apply/SKILL.md +3 -3
- package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
- package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +5 -2
- package/assets/skills/oat-docs-bootstrap/SKILL.md +4 -3
- package/assets/skills/oat-docs-bootstrap/assets/AGENTS.md.template +3 -3
- package/assets/skills/oat-pjm-decision/SKILL.md +3 -2
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +4 -4
- package/assets/skills/oat-project-complete/SKILL.md +47 -2
- package/assets/skills/oat-project-design/SKILL.md +10 -6
- package/assets/skills/oat-project-discover/SKILL.md +3 -3
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +3 -10
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +135 -3
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +7 -9
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +1 -1
- package/assets/skills/oat-project-import-plan/SKILL.md +3 -3
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-new/SKILL.md +3 -3
- package/assets/skills/oat-project-next/SKILL.md +50 -7
- package/assets/skills/oat-project-plan/SKILL.md +4 -4
- package/assets/skills/oat-project-plan-writing/SKILL.md +2 -2
- package/assets/skills/oat-project-pr-final/SKILL.md +11 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/SKILL.md +12 -8
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +4 -4
- package/assets/skills/oat-project-retro/SKILL.md +6 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +2 -2
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +2 -2
- package/assets/skills/oat-project-spec/SKILL.md +3 -3
- package/assets/skills/oat-project-summary/SKILL.md +22 -3
- package/assets/skills/oat-repo-improve/SKILL.md +2 -2
- package/assets/skills/recon/SKILL.md +24 -3
- package/assets/skills/recon/references/packet-contract.md +36 -6
- package/assets/skills/recon/references/profiles.md +3 -0
- package/assets/skills/recon/references/worker-contract.md +16 -3
- package/assets/skills/recon/scripts/create-review-brief.mjs +17 -86
- package/assets/skills/recon/scripts/lib/contracts.mjs +158 -41
- package/assets/skills/recon/scripts/lib/review-binding.mjs +123 -0
- package/assets/skills/recon/scripts/lib/routing.mjs +5 -2
- package/assets/skills/recon/scripts/reconcile-ledger.mjs +18 -7
- package/assets/skills/recon/scripts/render-packet.mjs +120 -1
- package/assets/skills/recon/scripts/validate-packet.mjs +119 -88
- package/assets/templates/state.md +1 -0
- package/dist/commands/backlog/new.d.ts +2 -2
- package/dist/commands/backlog/new.d.ts.map +1 -1
- package/dist/commands/backlog/new.js +2 -2
- package/dist/commands/decision/new.d.ts +2 -2
- package/dist/commands/decision/new.d.ts.map +1 -1
- package/dist/commands/decision/new.js +2 -2
- package/dist/commands/docs/nav/contents.d.ts +9 -0
- package/dist/commands/docs/nav/contents.d.ts.map +1 -1
- package/dist/commands/docs/nav/contents.js +7 -2
- package/dist/commands/docs/nav/fumadocs.d.ts +58 -0
- package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -0
- package/dist/commands/docs/nav/fumadocs.js +326 -0
- package/dist/commands/docs/nav/sync.d.ts +31 -1
- package/dist/commands/docs/nav/sync.d.ts.map +1 -1
- package/dist/commands/docs/nav/sync.js +163 -6
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/instructions/instructions.utils.d.ts +19 -0
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +89 -17
- package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
- package/dist/commands/instructions/sync/sync.js +49 -2
- package/dist/commands/pjm/init.d.ts.map +1 -1
- package/dist/commands/pjm/init.js +2 -2
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts +97 -0
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -0
- package/dist/commands/project/closeout-check/closeout-invariant.js +301 -0
- package/dist/commands/project/closeout-check/index.d.ts +38 -0
- package/dist/commands/project/closeout-check/index.d.ts.map +1 -0
- package/dist/commands/project/closeout-check/index.js +117 -0
- package/dist/commands/project/complete-state/index.d.ts +4 -0
- package/dist/commands/project/complete-state/index.d.ts.map +1 -1
- package/dist/commands/project/complete-state/index.js +11 -13
- package/dist/commands/project/dispatch/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch/index.js +6 -30
- package/dist/commands/project/dispatch/record.d.ts +9 -18
- package/dist/commands/project/dispatch/record.d.ts.map +1 -1
- package/dist/commands/project/dispatch/record.js +15 -212
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/project/new/scaffold.d.ts +0 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +19 -20
- package/dist/commands/project/promote/promote.d.ts +2 -2
- package/dist/commands/project/promote/promote.d.ts.map +1 -1
- package/dist/commands/project/promote/promote.js +13 -11
- package/dist/commands/shared/frontmatter.d.ts +21 -0
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +70 -4
- package/dist/commands/shared/template-source.d.ts +29 -0
- package/dist/commands/shared/template-source.d.ts.map +1 -0
- package/dist/commands/{pjm → shared}/template-source.js +24 -7
- package/dist/commands/state/generate.js +2 -2
- package/dist/commands/template/index.d.ts +10 -0
- package/dist/commands/template/index.d.ts.map +1 -0
- package/dist/commands/template/index.js +87 -0
- package/dist/commands/template/resolve.d.ts +31 -0
- package/dist/commands/template/resolve.d.ts.map +1 -0
- package/dist/commands/template/resolve.js +86 -0
- package/dist/fs/io.d.ts +0 -41
- package/dist/fs/io.d.ts.map +1 -1
- package/dist/fs/io.js +2 -414
- package/dist/providers/identity/absolute-paths.d.ts +4 -13
- package/dist/providers/identity/absolute-paths.d.ts.map +1 -1
- package/dist/providers/identity/absolute-paths.js +4 -17
- package/dist/providers/identity/generic-dispatch-record.d.ts +1 -1
- package/dist/providers/identity/generic-dispatch-record.js +2 -2
- package/dist/providers/identity/oat-dispatch-record.d.ts +4 -357
- package/dist/providers/identity/oat-dispatch-record.d.ts.map +1 -1
- package/dist/providers/identity/oat-dispatch-record.js +4 -236
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +12 -4
- package/dist/validation/skills.d.ts.map +1 -1
- package/dist/validation/skills.js +32 -6
- package/package.json +2 -2
- package/dist/commands/pjm/template-source.d.ts +0 -14
- 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;
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
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), {
|
|
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
|
|
106
|
+
.description('Validate project dispatch provenance')
|
|
130
107
|
.addCommand(new Command('record')
|
|
131
|
-
.description('
|
|
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));
|