@open-agent-toolkit/cli 0.3.19 → 0.3.21
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/agents/oat-phase-implementer.md +26 -3
- package/assets/agents/oat-reviewer.md +8 -3
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/reference/cli-reference.md +12 -7
- package/assets/docs/reference/oat-directory-structure.md +1 -0
- package/assets/docs/reference/project-artifacts.md +56 -1
- package/assets/docs/reference/project-state-machine.md +41 -7
- package/assets/docs/skills/brainstorm.md +3 -2
- package/assets/docs/skills/index.md +13 -10
- package/assets/docs/workflows/advanced/autonomy.md +3 -1
- package/assets/docs/workflows/advanced/dispatch-ceiling.md +2 -2
- package/assets/docs/workflows/advanced/workflow-gates.md +17 -4
- package/assets/docs/workflows/approvals-and-automation.md +21 -10
- package/assets/docs/workflows/choose-workflow.md +17 -9
- package/assets/docs/workflows/ideas/index.md +1 -1
- package/assets/docs/workflows/ideas/lifecycle.md +3 -2
- package/assets/docs/workflows/projects/closeout/closeout-skills.md +80 -7
- package/assets/docs/workflows/projects/closeout/pr-flow.md +7 -1
- package/assets/docs/workflows/projects/execution/execution-skills.md +16 -3
- package/assets/docs/workflows/projects/execution/implementation-execution.md +1 -1
- package/assets/docs/workflows/projects/execution/picking-up-projects.md +11 -1
- package/assets/docs/workflows/projects/lifecycle.md +108 -9
- package/assets/docs/workflows/projects/planning/hill-checkpoints.md +6 -6
- package/assets/docs/workflows/projects/planning/planning-skills.md +9 -7
- package/assets/docs/workflows/projects/planning/starting-projects.md +57 -11
- package/assets/docs/workflows/projects/reviews/index.md +22 -1
- package/assets/docs/workflows/projects/reviews/review-flavors.md +5 -3
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-brainstorm/SKILL.md +34 -9
- package/assets/skills/oat-brainstorm/references/destinations.md +6 -3
- package/assets/skills/oat-dispatch-subagents/SKILL.md +1 -1
- package/assets/skills/oat-dispatch-subagents/references/record-schema.md +6 -0
- package/assets/skills/oat-docs/SKILL.md +7 -3
- package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/SKILL.md +9 -3
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +151 -132
- package/assets/skills/oat-project-brief/SKILL.md +1 -1
- package/assets/skills/oat-project-brief/references/brief-contract.md +465 -9
- package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +151 -132
- package/assets/skills/oat-project-brief/references/docs/gate-approval-record.md +9 -0
- package/assets/skills/oat-project-capture/SKILL.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +14 -5
- package/assets/skills/oat-project-discover/SKILL.md +3 -2
- package/assets/skills/oat-project-document/SKILL.md +12 -6
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +151 -132
- package/assets/skills/oat-project-goal/SKILL.md +697 -0
- package/assets/skills/oat-project-goal/references/docs/autonomy-contract.md +412 -0
- package/assets/skills/oat-project-goal/references/docs/gate-approval-record.md +93 -0
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +67 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +151 -132
- package/assets/skills/oat-project-implement/references/docs/gate-approval-record.md +9 -0
- package/assets/skills/oat-project-implement/references/phase-execution.md +10 -3
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +28 -1
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +151 -132
- package/assets/skills/oat-project-new/SKILL.md +3 -3
- package/assets/skills/oat-project-next/SKILL.md +45 -15
- package/assets/skills/oat-project-next/references/docs/gate-approval-record.md +9 -0
- package/assets/skills/oat-project-plan/SKILL.md +14 -3
- package/assets/skills/oat-project-plan-writing/SKILL.md +6 -4
- package/assets/skills/oat-project-pr-final/SKILL.md +51 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +151 -132
- package/assets/skills/oat-project-pr-progress/SKILL.md +97 -3
- package/assets/skills/oat-project-progress/SKILL.md +38 -5
- package/assets/skills/oat-project-progress/references/docs/gate-approval-record.md +9 -0
- package/assets/skills/oat-project-quick-start/SKILL.md +14 -9
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +151 -132
- package/assets/skills/oat-project-quick-start/references/docs/gate-approval-record.md +9 -0
- package/assets/skills/oat-project-reconcile/SKILL.md +31 -2
- package/assets/skills/oat-project-review-provide/SKILL.md +79 -13
- package/assets/skills/oat-project-review-receive/SKILL.md +70 -1
- package/assets/skills/oat-project-revise/SKILL.md +25 -1
- package/assets/skills/oat-project-summary/SKILL.md +60 -5
- package/assets/skills/oat-wave-execute/SKILL.md +2 -2
- package/assets/templates/state.md +1 -1
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts +119 -3
- package/dist/commands/project/closeout-check/closeout-invariant.d.ts.map +1 -1
- package/dist/commands/project/closeout-check/closeout-invariant.js +454 -2
- package/dist/commands/project/closeout-check/index.d.ts +21 -3
- package/dist/commands/project/closeout-check/index.d.ts.map +1 -1
- package/dist/commands/project/closeout-check/index.js +190 -4
- package/dist/commands/project/complete-state/index.d.ts +2 -1
- package/dist/commands/project/complete-state/index.d.ts.map +1 -1
- package/dist/commands/project/complete-state/index.js +7 -1
- package/dist/commands/project/delivery/check.d.ts +66 -0
- package/dist/commands/project/delivery/check.d.ts.map +1 -0
- package/dist/commands/project/delivery/check.js +232 -0
- package/dist/commands/project/delivery/evidence.d.ts +171 -0
- package/dist/commands/project/delivery/evidence.d.ts.map +1 -0
- package/dist/commands/project/delivery/evidence.js +902 -0
- package/dist/commands/project/delivery/index.d.ts +7 -0
- package/dist/commands/project/delivery/index.d.ts.map +1 -0
- package/dist/commands/project/delivery/index.js +13 -0
- package/dist/commands/project/delivery/refresh.d.ts +110 -0
- package/dist/commands/project/delivery/refresh.d.ts.map +1 -0
- package/dist/commands/project/delivery/refresh.js +441 -0
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/project/log/append.d.ts +6 -0
- package/dist/commands/project/log/append.d.ts.map +1 -1
- package/dist/commands/project/log/append.js +6 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +47 -1
- package/dist/commands/project/promote/promote.d.ts +15 -1
- package/dist/commands/project/promote/promote.d.ts.map +1 -1
- package/dist/commands/project/promote/promote.js +99 -15
- package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/index.js +10 -0
- package/dist/commands/project/validate-plan/profiles.d.ts +19 -4
- package/dist/commands/project/validate-plan/profiles.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/profiles.js +153 -2
- package/dist/commands/shared/frontmatter.d.ts +1 -1
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +1 -0
- package/dist/commands/state/generate.d.ts.map +1 -1
- package/dist/commands/state/generate.js +35 -4
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +1 -0
- package/package.json +2 -2
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import { readFile as defaultReadFile } from 'node:fs/promises';
|
|
2
|
-
import { isAbsolute, join } from 'node:path';
|
|
1
|
+
import { readFile as defaultReadFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, isAbsolute, join } from 'node:path';
|
|
3
3
|
import { buildCommandContext } from '../../../app/command-context.js';
|
|
4
|
+
import { loadGoalDeliveryProject } from '../../project/delivery/check.js';
|
|
5
|
+
import { deliveryEvidenceScope, evaluateDeliveryReview, evaluateGateCarrier, } from '../../project/delivery/evidence.js';
|
|
6
|
+
import { observeMember } from '../../project/delivery/refresh.js';
|
|
7
|
+
import { defaultGhRunner, } from '../../project/links/refresh.js';
|
|
8
|
+
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
4
9
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
5
10
|
import { resolveEffectiveConfig } from '../../../config/resolve.js';
|
|
6
11
|
import { CliError } from '../../../errors/cli-error.js';
|
|
7
12
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
8
13
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
14
|
+
import { getProjectState, parsePhaseMarkers, } from '@open-agent-toolkit/control-plane';
|
|
9
15
|
import { Command } from 'commander';
|
|
10
|
-
import
|
|
11
|
-
|
|
16
|
+
import YAML from 'yaml';
|
|
17
|
+
import { evaluateCloseout, formatCloseoutRefusal, readStateWorkflowMode, resolveDeliveryGraph, } from './closeout-invariant.js';
|
|
18
|
+
export { evaluateCloseout, evaluateGoalBoundary, evaluateGoalCoverage, formatCloseoutRefusal, resolveDeliveryGraph, } from './closeout-invariant.js';
|
|
19
|
+
async function listDirectories(path) {
|
|
20
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
21
|
+
return entries
|
|
22
|
+
.filter((entry) => entry.isDirectory())
|
|
23
|
+
.map((entry) => entry.name);
|
|
24
|
+
}
|
|
12
25
|
export const DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES = {
|
|
13
26
|
buildCommandContext,
|
|
14
27
|
resolveProjectRoot,
|
|
@@ -17,7 +30,176 @@ export const DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES = {
|
|
|
17
30
|
dirExists,
|
|
18
31
|
fileExists,
|
|
19
32
|
env: process.env,
|
|
33
|
+
gh: defaultGhRunner,
|
|
34
|
+
git: defaultGitRunner,
|
|
35
|
+
getProjectState,
|
|
36
|
+
listDirectories,
|
|
20
37
|
};
|
|
38
|
+
async function readOptional(dependencies, path) {
|
|
39
|
+
try {
|
|
40
|
+
return await dependencies.readFile(path, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function frontmatterRecord(content) {
|
|
47
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
48
|
+
if (!match)
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = YAML.parse(match[1] ?? '');
|
|
52
|
+
return parsed !== null &&
|
|
53
|
+
typeof parsed === 'object' &&
|
|
54
|
+
!Array.isArray(parsed)
|
|
55
|
+
? parsed
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Find an extracted phase's child project by slug under any scope of the
|
|
64
|
+
* projects root, and read its lifecycle and `oat_extracted_from` backlink.
|
|
65
|
+
*/
|
|
66
|
+
async function observeChild(projectsRoot, slug, dependencies) {
|
|
67
|
+
const unresolved = (status) => ({
|
|
68
|
+
slug,
|
|
69
|
+
status,
|
|
70
|
+
lifecycle: null,
|
|
71
|
+
extractedFrom: null,
|
|
72
|
+
});
|
|
73
|
+
let scopes;
|
|
74
|
+
try {
|
|
75
|
+
scopes = await dependencies.listDirectories(projectsRoot);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return unresolved('unreadable');
|
|
79
|
+
}
|
|
80
|
+
const candidates = [];
|
|
81
|
+
for (const scope of scopes) {
|
|
82
|
+
const statePath = join(projectsRoot, scope, slug, 'state.md');
|
|
83
|
+
if (await dependencies.fileExists(statePath)) {
|
|
84
|
+
candidates.push(statePath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (candidates.length === 0)
|
|
88
|
+
return unresolved('missing');
|
|
89
|
+
if (candidates.length > 1)
|
|
90
|
+
return unresolved('ambiguous');
|
|
91
|
+
const content = await readOptional(dependencies, candidates[0]);
|
|
92
|
+
const frontmatter = content === null ? null : frontmatterRecord(content);
|
|
93
|
+
if (frontmatter === null)
|
|
94
|
+
return unresolved('unreadable');
|
|
95
|
+
const read = (key) => typeof frontmatter[key] === 'string' ? frontmatter[key] : null;
|
|
96
|
+
return {
|
|
97
|
+
slug,
|
|
98
|
+
status: 'found',
|
|
99
|
+
lifecycle: read('oat_lifecycle'),
|
|
100
|
+
extractedFrom: read('oat_extracted_from'),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Load the goal boundary inputs read-only (A3): the brief, the journal, the
|
|
105
|
+
* registry with fresh member observations merged in (never persisted; that is
|
|
106
|
+
* `delivery refresh`'s job), each extracted child's facts, and the shared
|
|
107
|
+
* evaluator's verdict for every terminal delivery's effective scope.
|
|
108
|
+
*/
|
|
109
|
+
export async function loadGoalCloseoutInputs(repoRoot, targetProjectPath, dependencies) {
|
|
110
|
+
const planContent = await readOptional(dependencies, join(targetProjectPath, 'plan.md'));
|
|
111
|
+
const implementationContent = await readOptional(dependencies, join(targetProjectPath, 'implementation.md'));
|
|
112
|
+
const inputs = {
|
|
113
|
+
goalSlug: basename(targetProjectPath),
|
|
114
|
+
planContent,
|
|
115
|
+
implementationContent,
|
|
116
|
+
deliveries: null,
|
|
117
|
+
observationFailures: [],
|
|
118
|
+
children: [],
|
|
119
|
+
evidence: {},
|
|
120
|
+
};
|
|
121
|
+
if (implementationContent !== null) {
|
|
122
|
+
const projectsRoot = dirname(dirname(targetProjectPath));
|
|
123
|
+
const slugs = new Set();
|
|
124
|
+
for (const markers of parsePhaseMarkers(implementationContent).values()) {
|
|
125
|
+
if (markers.status === 'extracted' && markers.extractedTo !== null) {
|
|
126
|
+
slugs.add(markers.extractedTo);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const slug of slugs) {
|
|
130
|
+
inputs.children.push(await observeChild(projectsRoot, slug, dependencies));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let project;
|
|
134
|
+
try {
|
|
135
|
+
project = await loadGoalDeliveryProject(targetProjectPath, repoRoot, {
|
|
136
|
+
resolveProjectRoot: async () => repoRoot,
|
|
137
|
+
readFile: (path, encoding) => dependencies.readFile(path, encoding),
|
|
138
|
+
getProjectState: dependencies.getProjectState,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return inputs;
|
|
143
|
+
}
|
|
144
|
+
const registered = project.state.deliveries ?? [];
|
|
145
|
+
// Observe every member of every non-superseded delivery read-only.
|
|
146
|
+
const observed = [];
|
|
147
|
+
for (const delivery of registered) {
|
|
148
|
+
if (delivery.status === 'superseded' || delivery.status === 'registering') {
|
|
149
|
+
observed.push(delivery);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const members = [];
|
|
153
|
+
for (const member of delivery.members) {
|
|
154
|
+
const observation = member.url === null
|
|
155
|
+
? 'a member has no URL to observe'
|
|
156
|
+
: await observeMember(dependencies.gh, member.url);
|
|
157
|
+
if (typeof observation === 'string') {
|
|
158
|
+
inputs.observationFailures.push(`${delivery.delivery_id} ${member.url ?? '(no url)'}: ${observation}`);
|
|
159
|
+
members.push(member);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
members.push({
|
|
163
|
+
...member,
|
|
164
|
+
observed_head: observation.head,
|
|
165
|
+
observed_base: observation.base,
|
|
166
|
+
status: observation.status,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
observed.push({ ...delivery, members });
|
|
171
|
+
}
|
|
172
|
+
inputs.deliveries = observed;
|
|
173
|
+
if (inputs.observationFailures.length > 0) {
|
|
174
|
+
return inputs;
|
|
175
|
+
}
|
|
176
|
+
// Evaluate evidence for every terminal delivery over its effective scope.
|
|
177
|
+
const graph = resolveDeliveryGraph(observed, project.state.progress.phases.map((phase) => phase.phaseId));
|
|
178
|
+
for (const terminal of graph.terminals) {
|
|
179
|
+
const { delivery, effectiveScope } = terminal;
|
|
180
|
+
if (delivery.status === 'registering')
|
|
181
|
+
continue;
|
|
182
|
+
const evidenceScope = deliveryEvidenceScope(project, observed, delivery);
|
|
183
|
+
const fact = {
|
|
184
|
+
scope: effectiveScope,
|
|
185
|
+
review: await evaluateDeliveryReview(dependencies.git, evidenceScope, delivery, effectiveScope),
|
|
186
|
+
// The accepted exit-gate binding persisted with the delivery's evidence,
|
|
187
|
+
// never the milestone's current working carrier.
|
|
188
|
+
exit: await evaluateGateCarrier(delivery.exit_gate, {
|
|
189
|
+
kind: 'code',
|
|
190
|
+
scope: effectiveScope,
|
|
191
|
+
match: 'covers',
|
|
192
|
+
overrideDisabled: project.exitGateDisabled,
|
|
193
|
+
}, {
|
|
194
|
+
git: dependencies.git,
|
|
195
|
+
scope: evidenceScope,
|
|
196
|
+
publishedHeads: delivery.members.map((member) => member.observed_head),
|
|
197
|
+
}),
|
|
198
|
+
};
|
|
199
|
+
inputs.evidence[delivery.delivery_id] = fact;
|
|
200
|
+
}
|
|
201
|
+
return inputs;
|
|
202
|
+
}
|
|
21
203
|
/**
|
|
22
204
|
* Resolve a project, read its `state.md` from disk, and evaluate the closeout
|
|
23
205
|
* invariant. Shared by `closeout-check` and `complete-state` so both read the
|
|
@@ -36,8 +218,12 @@ export async function checkProjectCloseout(projectPath, options, context, depend
|
|
|
36
218
|
throw new CliError(`Project state.md not found: ${statePath}`, 1);
|
|
37
219
|
}
|
|
38
220
|
const stateContent = await dependencies.readFile(statePath, 'utf8');
|
|
221
|
+
const goal = readStateWorkflowMode(stateContent) === 'goal'
|
|
222
|
+
? await loadGoalCloseoutInputs(repoRoot, targetProjectPath, dependencies)
|
|
223
|
+
: undefined;
|
|
39
224
|
const result = await evaluateCloseout({
|
|
40
225
|
stateContent,
|
|
226
|
+
...(goal === undefined ? {} : { goal }),
|
|
41
227
|
autonomousFlag: options.autonomous,
|
|
42
228
|
env: dependencies.env,
|
|
43
229
|
resolveConfigured: async () => {
|
|
@@ -1,9 +1,10 @@
|
|
|
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 { type CloseoutInputDependencies } from '../../project/closeout-check/index.js';
|
|
3
4
|
import { resolveEffectiveConfig } from '../../../config/resolve.js';
|
|
4
5
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
5
6
|
import { Command } from 'commander';
|
|
6
|
-
interface ProjectCompleteStateDependencies {
|
|
7
|
+
interface ProjectCompleteStateDependencies extends CloseoutInputDependencies {
|
|
7
8
|
buildCommandContext: (options: Parameters<typeof buildCommandContext>[0]) => CommandContext;
|
|
8
9
|
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
9
10
|
readFile: typeof defaultReadFile;
|
|
@@ -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;AAE1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,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;AAChF,OAAO,EAIL,KAAK,yBAAyB,EAC/B,MAAM,wCAAwC,CAAC;AAEhD,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,gCAAiC,SAAQ,yBAAyB;IAC1E,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;AAkFD,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,gCAAgC,CAAM,GACxD,OAAO,CA+BT"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile as defaultReadFile, writeFile as defaultWriteFile, } from 'node:fs/promises';
|
|
2
2
|
import { buildCommandContext } from '../../../app/command-context.js';
|
|
3
|
-
import { checkProjectCloseout, formatCloseoutRefusal, } from '../../project/closeout-check/index.js';
|
|
3
|
+
import { checkProjectCloseout, DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES, formatCloseoutRefusal, } from '../../project/closeout-check/index.js';
|
|
4
4
|
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
5
5
|
import { resolveEffectiveConfig } from '../../../config/resolve.js';
|
|
6
6
|
import { CliError } from '../../../errors/cli-error.js';
|
|
@@ -10,6 +10,12 @@ import { assertValidProjectStateFilesystemContent } from '../../../validation/pr
|
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import { renderCompletedProjectState } from './state-utils.js';
|
|
12
12
|
const DEFAULT_DEPENDENCIES = {
|
|
13
|
+
// Goal boundary observation (gh, git, project reader, child lookup) is
|
|
14
|
+
// read-only and shared with closeout-check.
|
|
15
|
+
gh: DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES.gh,
|
|
16
|
+
git: DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES.git,
|
|
17
|
+
getProjectState: DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES.getProjectState,
|
|
18
|
+
listDirectories: DEFAULT_CLOSEOUT_CHECK_DEPENDENCIES.listDirectories,
|
|
13
19
|
buildCommandContext,
|
|
14
20
|
resolveProjectRoot,
|
|
15
21
|
readFile: defaultReadFile,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type CommandContext, type GlobalOptions } from '../../../app/command-context.js';
|
|
2
|
+
import { type GitRunner } from '../../project/sync/git.js';
|
|
3
|
+
import { type GoalDelivery, type ProjectState } from '@open-agent-toolkit/control-plane';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { type CarrierEvaluation, type ReviewEvaluation } from './evidence.js';
|
|
6
|
+
export interface GoalDeliveryProject {
|
|
7
|
+
repoRoot: string;
|
|
8
|
+
projectPath: string;
|
|
9
|
+
projectDir: string;
|
|
10
|
+
statePath: string;
|
|
11
|
+
stateContent: string;
|
|
12
|
+
state: ProjectState;
|
|
13
|
+
/** Whether the project override currently disables the exit gate. */
|
|
14
|
+
exitGateDisabled: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface DeliveryProjectDependencies {
|
|
17
|
+
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
18
|
+
readFile: (path: string, encoding: 'utf8') => Promise<string>;
|
|
19
|
+
getProjectState: (projectPath: string) => Promise<ProjectState>;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_DELIVERY_PROJECT_DEPENDENCIES: DeliveryProjectDependencies;
|
|
22
|
+
/**
|
|
23
|
+
* Load a goal project for the delivery commands through the control-plane
|
|
24
|
+
* reader, so the registry, carriers, membership and Reviews rows are read
|
|
25
|
+
* exactly as the router reads them. Fails closed for any other mode.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadGoalDeliveryProject(project: string, cwd: string, dependencies: DeliveryProjectDependencies): Promise<GoalDeliveryProject>;
|
|
28
|
+
/**
|
|
29
|
+
* A delivery's effective scope: its frozen scope plus every predecessor it
|
|
30
|
+
* replaces through `covers_scope_of`. A cycle or missing link stops the
|
|
31
|
+
* walk; the closeout coverage predicate reports those.
|
|
32
|
+
*/
|
|
33
|
+
export declare function deliveryEffectiveScope(deliveries: readonly GoalDelivery[], delivery: GoalDelivery): string[];
|
|
34
|
+
export { reviewRowsFor } from './evidence.js';
|
|
35
|
+
export interface SubjectEvidenceReport {
|
|
36
|
+
subject: string;
|
|
37
|
+
kind: 'milestone' | 'delivery';
|
|
38
|
+
milestone: string | null;
|
|
39
|
+
scope: string[];
|
|
40
|
+
publishedHeads: (string | null)[];
|
|
41
|
+
review: ReviewEvaluation;
|
|
42
|
+
/**
|
|
43
|
+
* `source` names the carrier evaluated: `delivery check` judges the
|
|
44
|
+
* milestone's working carrier (`oat_milestone_gates[mNN].exit`), while the
|
|
45
|
+
* closeout boundary judges the delivery's persisted `exit_gate` binding.
|
|
46
|
+
*/
|
|
47
|
+
exit: CarrierEvaluation & {
|
|
48
|
+
source: ExitCarrierSource;
|
|
49
|
+
};
|
|
50
|
+
/** Passed, fresh integrated review and an allowed, fresh exit carrier. */
|
|
51
|
+
deliverable: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Which exit carrier an evaluation judged. */
|
|
54
|
+
export type ExitCarrierSource = 'milestone-carrier' | 'delivery-binding';
|
|
55
|
+
/**
|
|
56
|
+
* Evaluate the delivery evidence for one subject. A registered delivery is
|
|
57
|
+
* judged against its persisted member observations; a milestone (before
|
|
58
|
+
* registration) against the project branch head.
|
|
59
|
+
*/
|
|
60
|
+
export declare function evaluateSubjectEvidence(project: GoalDeliveryProject, subject: string, git: GitRunner): Promise<SubjectEvidenceReport>;
|
|
61
|
+
export interface DeliveryCheckDependencies extends DeliveryProjectDependencies {
|
|
62
|
+
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
63
|
+
git: GitRunner;
|
|
64
|
+
}
|
|
65
|
+
export declare function createProjectDeliveryCheckCommand(overrides?: Partial<DeliveryCheckDependencies>): Command;
|
|
66
|
+
//# sourceMappingURL=check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/delivery/check.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAS9E,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAML,KAAK,iBAAiB,EAEtB,KAAK,gBAAgB,EACtB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,qEAAqE;IACrE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;CACjE;AAED,eAAO,MAAM,qCAAqC,EAAE,2BAKjD,CAAC;AAEJ;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,2BAA2B,GACxC,OAAO,CAAC,mBAAmB,CAAC,CA+B9B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,SAAS,YAAY,EAAE,EACnC,QAAQ,EAAE,YAAY,GACrB,MAAM,EAAE,CAgBV;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC;IAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,cAAc,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;IAClC,MAAM,EAAE,gBAAgB,CAAC;IACzB;;;;OAIG;IACH,IAAI,EAAE,iBAAiB,GAAG;QAAE,MAAM,EAAE,iBAAiB,CAAA;KAAE,CAAC;IACxD,0EAA0E;IAC1E,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,+CAA+C;AAC/C,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,kBAAkB,CAAC;AAEzE;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,SAAS,GACb,OAAO,CAAC,qBAAqB,CAAC,CA4HhC;AAED,MAAM,WAAW,yBAA0B,SAAQ,2BAA2B;IAC5E,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,GAAG,EAAE,SAAS,CAAC;CAChB;AAwBD,wBAAgB,iCAAiC,CAC/C,SAAS,GAAE,OAAO,CAAC,yBAAyB,CAAM,GACjD,OAAO,CAsDT"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join, relative, sep } from 'node:path';
|
|
3
|
+
import { buildCommandContext, } from '../../../app/command-context.js';
|
|
4
|
+
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
5
|
+
import { getFrontmatterBlock, parseSkillGateOverrides, SKILL_GATE_OVERRIDE_DISABLED, } from '../../shared/frontmatter.js';
|
|
6
|
+
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
7
|
+
import { CliError } from '../../../errors/cli-error.js';
|
|
8
|
+
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
9
|
+
import { getProjectState, milestonePhaseScope, normalizeMilestoneId, } from '@open-agent-toolkit/control-plane';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { deliveryEvidenceScope, reviewRowsFor, evaluateDeliveryReview, evaluateGateCarrier, evaluateSuccessor, } from './evidence.js';
|
|
12
|
+
/** The skill whose gate the milestone exit carrier records. */
|
|
13
|
+
const EXIT_GATE_SKILL = 'oat-project-implement';
|
|
14
|
+
export const DEFAULT_DELIVERY_PROJECT_DEPENDENCIES = {
|
|
15
|
+
resolveProjectRoot,
|
|
16
|
+
readFile: (path, encoding) => readFile(path, encoding),
|
|
17
|
+
getProjectState,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Load a goal project for the delivery commands through the control-plane
|
|
21
|
+
* reader, so the registry, carriers, membership and Reviews rows are read
|
|
22
|
+
* exactly as the router reads them. Fails closed for any other mode.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadGoalDeliveryProject(project, cwd, dependencies) {
|
|
25
|
+
const repoRoot = await dependencies.resolveProjectRoot(cwd);
|
|
26
|
+
const projectPath = isAbsolute(project) ? project : join(repoRoot, project);
|
|
27
|
+
const statePath = join(projectPath, 'state.md');
|
|
28
|
+
let stateContent;
|
|
29
|
+
try {
|
|
30
|
+
stateContent = await dependencies.readFile(statePath, 'utf8');
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new CliError(`Project state.md not found: ${statePath}`, 1);
|
|
34
|
+
}
|
|
35
|
+
const state = await dependencies.getProjectState(projectPath);
|
|
36
|
+
if (state.workflowMode !== 'goal') {
|
|
37
|
+
throw new CliError(`Delivery evidence applies to goal projects; ${project} is in ${state.workflowMode} mode.`, 1);
|
|
38
|
+
}
|
|
39
|
+
const overrides = parseSkillGateOverrides(getFrontmatterBlock(stateContent) ?? '', statePath);
|
|
40
|
+
return {
|
|
41
|
+
repoRoot,
|
|
42
|
+
projectPath,
|
|
43
|
+
projectDir: relative(repoRoot, projectPath).split(sep).join('/'),
|
|
44
|
+
statePath,
|
|
45
|
+
stateContent,
|
|
46
|
+
state,
|
|
47
|
+
exitGateDisabled: overrides.overrides[EXIT_GATE_SKILL] === SKILL_GATE_OVERRIDE_DISABLED,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A delivery's effective scope: its frozen scope plus every predecessor it
|
|
52
|
+
* replaces through `covers_scope_of`. A cycle or missing link stops the
|
|
53
|
+
* walk; the closeout coverage predicate reports those.
|
|
54
|
+
*/
|
|
55
|
+
export function deliveryEffectiveScope(deliveries, delivery) {
|
|
56
|
+
const scope = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
let current = delivery;
|
|
59
|
+
while (current !== undefined && !seen.has(current.delivery_id)) {
|
|
60
|
+
seen.add(current.delivery_id);
|
|
61
|
+
for (const phaseId of current.scope) {
|
|
62
|
+
if (!scope.includes(phaseId))
|
|
63
|
+
scope.push(phaseId);
|
|
64
|
+
}
|
|
65
|
+
const predecessor = current.covers_scope_of;
|
|
66
|
+
current =
|
|
67
|
+
predecessor === null
|
|
68
|
+
? undefined
|
|
69
|
+
: deliveries.find((entry) => entry.delivery_id === predecessor);
|
|
70
|
+
}
|
|
71
|
+
return scope;
|
|
72
|
+
}
|
|
73
|
+
export { reviewRowsFor } from './evidence.js';
|
|
74
|
+
/**
|
|
75
|
+
* Evaluate the delivery evidence for one subject. A registered delivery is
|
|
76
|
+
* judged against its persisted member observations; a milestone (before
|
|
77
|
+
* registration) against the project branch head.
|
|
78
|
+
*/
|
|
79
|
+
export async function evaluateSubjectEvidence(project, subject, git) {
|
|
80
|
+
const deliveries = project.state.deliveries ?? [];
|
|
81
|
+
const delivery = deliveries.find((entry) => entry.delivery_id === subject);
|
|
82
|
+
if (delivery) {
|
|
83
|
+
const milestoneId = delivery.milestone;
|
|
84
|
+
const scope = deliveryEffectiveScope(deliveries, delivery);
|
|
85
|
+
const evidence = deliveryEvidenceScope(project, deliveries, delivery);
|
|
86
|
+
const publishedHeads = delivery.members.map((member) => member.observed_head);
|
|
87
|
+
const review = await evaluateDeliveryReview(git, evidence, delivery, scope);
|
|
88
|
+
const exitEvaluation = await evaluateGateCarrier(milestoneId === null
|
|
89
|
+
? null
|
|
90
|
+
: (project.state.milestoneGates?.[milestoneId]?.exit ?? null), {
|
|
91
|
+
kind: 'code',
|
|
92
|
+
scope,
|
|
93
|
+
match: 'covers',
|
|
94
|
+
overrideDisabled: project.exitGateDisabled,
|
|
95
|
+
}, { git, scope: evidence, publishedHeads });
|
|
96
|
+
const exit = { ...exitEvaluation, source: 'milestone-carrier' };
|
|
97
|
+
return {
|
|
98
|
+
subject,
|
|
99
|
+
kind: 'delivery',
|
|
100
|
+
milestone: milestoneId,
|
|
101
|
+
scope,
|
|
102
|
+
publishedHeads,
|
|
103
|
+
review,
|
|
104
|
+
exit,
|
|
105
|
+
deliverable: review.fresh &&
|
|
106
|
+
review.disposition === 'passed' &&
|
|
107
|
+
exit.outcome === 'allowed' &&
|
|
108
|
+
exit.freshness === 'fresh',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const milestoneId = normalizeMilestoneId(subject);
|
|
112
|
+
if (milestoneId === null) {
|
|
113
|
+
throw new CliError(`Unknown delivery subject "${subject}": name a milestone (mNN) or a registered delivery (dNN).`, 1);
|
|
114
|
+
}
|
|
115
|
+
const membership = project.state.milestoneMembership;
|
|
116
|
+
const scope = membership
|
|
117
|
+
? milestonePhaseScope(membership, milestoneId)
|
|
118
|
+
: null;
|
|
119
|
+
if (scope === null) {
|
|
120
|
+
const [firstError] = membership?.errors ?? [];
|
|
121
|
+
throw new CliError(firstError
|
|
122
|
+
? `Milestone membership is invalid (${firstError.code}: ${firstError.message}); repair it with oat-project-goal.`
|
|
123
|
+
: `No ## Milestones row for ${milestoneId}.`, 1);
|
|
124
|
+
}
|
|
125
|
+
const head = await git.run(['rev-parse', '--verify', 'HEAD'], {
|
|
126
|
+
cwd: project.repoRoot,
|
|
127
|
+
allowFailure: true,
|
|
128
|
+
});
|
|
129
|
+
const publishedHead = head.code === 0 ? head.stdout.trim() : null;
|
|
130
|
+
const rows = reviewRowsFor(project.state.reviews, [milestoneId]);
|
|
131
|
+
const row = rows.at(-1) ?? null;
|
|
132
|
+
const evidence = {
|
|
133
|
+
repoRoot: project.repoRoot,
|
|
134
|
+
projectDir: project.projectDir,
|
|
135
|
+
evidenceArtifacts: rows
|
|
136
|
+
.map((entry) => entry.artifact)
|
|
137
|
+
.filter((artifact) => artifact !== '' && artifact !== '-'),
|
|
138
|
+
};
|
|
139
|
+
let review;
|
|
140
|
+
if (row === null || row.artifact === '' || row.artifact === '-') {
|
|
141
|
+
review = {
|
|
142
|
+
artifact: null,
|
|
143
|
+
disposition: row?.status ?? null,
|
|
144
|
+
fresh: false,
|
|
145
|
+
reason: `review-missing: no ${milestoneId} code review row names an artifact`,
|
|
146
|
+
members: [],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const successor = await evaluateSuccessor(git, evidence, row.reviewedHead ?? null, publishedHead);
|
|
151
|
+
review = {
|
|
152
|
+
artifact: row.artifact,
|
|
153
|
+
disposition: row.status,
|
|
154
|
+
fresh: successor.fresh,
|
|
155
|
+
reason: successor.fresh ? null : `${successor.code}: ${successor.reason}`,
|
|
156
|
+
members: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const exit = {
|
|
160
|
+
...(await evaluateGateCarrier(project.state.milestoneGates?.[milestoneId]?.exit ?? null, { kind: 'code', scope, overrideDisabled: project.exitGateDisabled }, { git, scope: evidence, publishedHeads: [publishedHead] })),
|
|
161
|
+
source: 'milestone-carrier',
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
subject,
|
|
165
|
+
kind: 'milestone',
|
|
166
|
+
milestone: milestoneId,
|
|
167
|
+
scope,
|
|
168
|
+
publishedHeads: [publishedHead],
|
|
169
|
+
review,
|
|
170
|
+
exit,
|
|
171
|
+
deliverable: review.fresh &&
|
|
172
|
+
review.disposition?.toLowerCase() === 'passed' &&
|
|
173
|
+
exit.outcome === 'allowed' &&
|
|
174
|
+
exit.freshness === 'fresh',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const DEFAULT_CHECK_DEPENDENCIES = {
|
|
178
|
+
...DEFAULT_DELIVERY_PROJECT_DEPENDENCIES,
|
|
179
|
+
buildCommandContext,
|
|
180
|
+
git: defaultGitRunner,
|
|
181
|
+
};
|
|
182
|
+
function formatEvidenceReport(report) {
|
|
183
|
+
const review = report.review.fresh
|
|
184
|
+
? `fresh (${report.review.disposition ?? 'no disposition'})`
|
|
185
|
+
: `stale: ${report.review.reason ?? 'unknown'}`;
|
|
186
|
+
const exit = report.exit.freshness === 'fresh'
|
|
187
|
+
? `${report.exit.outcome}, fresh`
|
|
188
|
+
: `${report.exit.outcome}, stale: ${report.exit.reason ?? 'unknown'}`;
|
|
189
|
+
return [
|
|
190
|
+
`Delivery evidence for ${report.subject} (${report.kind}; scope ${report.scope.join(', ')}):`,
|
|
191
|
+
` integrated review: ${review}`,
|
|
192
|
+
` exit carrier (${report.exit.source}): ${exit}`,
|
|
193
|
+
` deliverable: ${report.deliverable ? 'yes' : 'no'}`,
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
196
|
+
export function createProjectDeliveryCheckCommand(overrides = {}) {
|
|
197
|
+
const dependencies = { ...DEFAULT_CHECK_DEPENDENCIES, ...overrides };
|
|
198
|
+
return new Command('check')
|
|
199
|
+
.description('Report integrated-review and milestone exit-carrier freshness for a goal milestone or delivery under the bookkeeping-successor contract (read-only; status undeliverable and exit 1 when not deliverable)')
|
|
200
|
+
.requiredOption('--project <path>', 'Goal project path')
|
|
201
|
+
.requiredOption('--subject <id>', 'Milestone (mNN) or registered delivery (dNN) to evaluate')
|
|
202
|
+
.action(async (options, command) => {
|
|
203
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
204
|
+
try {
|
|
205
|
+
const project = await loadGoalDeliveryProject(options.project, context.cwd, dependencies);
|
|
206
|
+
const report = await evaluateSubjectEvidence(project, options.subject, dependencies.git);
|
|
207
|
+
if (context.json) {
|
|
208
|
+
// `ok` means deliverable; an evaluated but refusing subject is
|
|
209
|
+
// `undeliverable`, with the full report and exit 1.
|
|
210
|
+
context.logger.json({
|
|
211
|
+
status: report.deliverable ? 'ok' : 'undeliverable',
|
|
212
|
+
...report,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
context.logger.info(formatEvidenceReport(report));
|
|
217
|
+
}
|
|
218
|
+
// Fail closed: a caller that ignores the report still stops.
|
|
219
|
+
process.exitCode = report.deliverable ? 0 : 1;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
if (context.json) {
|
|
224
|
+
context.logger.json({ status: 'error', message });
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
context.logger.error(message);
|
|
228
|
+
}
|
|
229
|
+
process.exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|