@open-agent-toolkit/cli 0.3.21 → 0.3.23
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/docs-tooling/add-docs-to-a-repo.md +3 -9
- package/assets/docs/reference/cli-reference.md +8 -3
- package/assets/docs/reference/configuration.md +3 -0
- package/assets/docs/reference/oat-directory-structure.md +1 -1
- package/assets/docs/reference/project-artifacts.md +141 -2
- package/assets/docs/reference/project-state-machine.md +2 -2
- package/assets/docs/workflows/advanced/autonomy.md +29 -12
- package/assets/docs/workflows/advanced/cursor-cloud.md +20 -6
- package/assets/docs/workflows/advanced/workflow-gates.md +9 -0
- package/assets/docs/workflows/projects/closeout/closeout-skills.md +24 -5
- package/assets/docs/workflows/projects/closeout/retro.md +6 -2
- package/assets/docs/workflows/projects/execution/implementation-execution.md +18 -4
- package/assets/docs/workflows/projects/execution/index.md +1 -1
- package/assets/docs/workflows/projects/execution/picking-up-projects.md +3 -2
- package/assets/docs/workflows/projects/execution/project-log.md +100 -11
- package/assets/docs/workflows/projects/lifecycle.md +3 -3
- package/assets/docs/workflows/projects/reviews/index.md +18 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-cursor-cloud-projects/SKILL.md +12 -8
- package/assets/skills/oat-cursor-cloud-projects/references/cursor-cloud-mechanics.md +63 -2
- package/assets/skills/oat-docs/SKILL.md +14 -28
- package/assets/skills/oat-docs-analyze/SKILL.md +4 -2
- package/assets/skills/oat-docs-analyze/references/legacy-scaffold-checks.md +16 -0
- package/assets/skills/oat-docs-apply/SKILL.md +2 -2
- package/assets/skills/oat-docs-authoring/SKILL.md +2 -2
- package/assets/skills/oat-docs-authoring/references/lifecycle-boundaries.md +1 -2
- package/assets/skills/oat-docs-authoring/references/mkdocs-to-oat-fumadocs-refactor-guide.md +73 -0
- package/assets/skills/oat-docs-bootstrap/SKILL.md +44 -310
- package/assets/skills/oat-project-autonomous/SKILL.md +88 -26
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
- package/assets/skills/oat-project-brief/SKILL.md +1 -1
- package/assets/skills/oat-project-brief/references/brief-contract.md +33 -27
- package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-complete/SKILL.md +142 -25
- package/assets/skills/oat-project-complete-auto/SKILL.md +4 -4
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +10 -5
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-goal/SKILL.md +63 -15
- package/assets/skills/oat-project-goal/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/SKILL.md +6 -6
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +28 -7
- package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +10 -5
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-implement/references/phase-execution.md +23 -9
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +4 -4
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-pr-final/SKILL.md +6 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-pr-progress/SKILL.md +6 -1
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
- package/assets/skills/oat-project-reconcile/SKILL.md +13 -19
- package/assets/skills/oat-project-retro/SKILL.md +5 -2
- package/assets/skills/oat-project-retro/references/evidence-and-lanes.md +9 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +28 -3
- package/assets/skills/oat-project-review-receive/SKILL.md +32 -18
- package/assets/skills/oat-project-review-receive-remote/SKILL.md +10 -6
- package/assets/skills/oat-project-revise/SKILL.md +9 -2
- package/assets/skills/oat-project-summary/SKILL.md +166 -37
- package/assets/skills/oat-worktree-bootstrap-auto/SKILL.md +3 -3
- package/assets/templates/docs-app-fuma/docs/contributing.md +6 -0
- package/assets/templates/docs-app-fuma/docs/getting-started.md +1 -1
- package/assets/templates/docs-app-fuma/next.config.js +5 -1
- package/assets/templates/implementation-brief.md +5 -36
- package/assets/templates/implementation.md +11 -81
- package/assets/templates/project-log.md +2 -31
- package/assets/templates/project-retro.md +4 -1
- package/assets/templates/summary.md +13 -5
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +44 -1
- package/dist/commands/docs/analyze.js +1 -1
- package/dist/commands/docs/apply.js +1 -1
- package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
- package/dist/commands/docs/init/scaffold.js +35 -2
- package/dist/commands/project/archive/archive-utils.d.ts +205 -8
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +708 -44
- package/dist/commands/project/archive/index.d.ts.map +1 -1
- package/dist/commands/project/archive/index.js +1 -0
- package/dist/commands/project/archive/push-runner.d.ts +9 -1
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +56 -18
- package/dist/commands/project/archive/sync-runner.d.ts +28 -0
- package/dist/commands/project/archive/sync-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/sync-runner.js +44 -22
- package/dist/commands/project/complete-state/index.d.ts.map +1 -1
- package/dist/commands/project/complete-state/index.js +45 -0
- package/dist/commands/project/dispatch/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch/index.js +24 -2
- package/dist/commands/project/dispatch/record.d.ts +54 -1
- package/dist/commands/project/dispatch/record.d.ts.map +1 -1
- package/dist/commands/project/dispatch/record.js +229 -1
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/project/journal-check/evaluator.d.ts +17 -0
- package/dist/commands/project/journal-check/evaluator.d.ts.map +1 -0
- package/dist/commands/project/journal-check/evaluator.js +140 -0
- package/dist/commands/project/journal-check/index.d.ts +34 -0
- package/dist/commands/project/journal-check/index.d.ts.map +1 -0
- package/dist/commands/project/journal-check/index.js +163 -0
- package/dist/commands/project/journal-check/rules.d.ts +68 -0
- package/dist/commands/project/journal-check/rules.d.ts.map +1 -0
- package/dist/commands/project/journal-check/rules.js +994 -0
- package/dist/commands/project/journal-check/types.d.ts +63 -0
- package/dist/commands/project/journal-check/types.d.ts.map +1 -0
- package/dist/commands/project/journal-check/types.js +26 -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 +114 -6
- package/dist/commands/project/log/check.d.ts +23 -1
- package/dist/commands/project/log/check.d.ts.map +1 -1
- package/dist/commands/project/log/check.js +20 -3
- package/dist/commands/project/log/grammar.d.ts +37 -0
- package/dist/commands/project/log/grammar.d.ts.map +1 -1
- package/dist/commands/project/log/grammar.js +72 -0
- package/dist/commands/project/log/rollup.d.ts +87 -1
- package/dist/commands/project/log/rollup.d.ts.map +1 -1
- package/dist/commands/project/log/rollup.js +257 -5
- package/dist/config/oat-config.d.ts +14 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +17 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +5 -1
- package/dist/providers/identity/generic-dispatch-record.d.ts +33 -0
- package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -1
- package/dist/providers/identity/generic-dispatch-record.js +60 -1
- package/package.json +2 -2
|
@@ -2,9 +2,10 @@ import { acceptClaudeLaunchEnvelope, buildClaudeDispatchRecord, collectClaudeLau
|
|
|
2
2
|
import { redactAbsolutePaths } from '../../../providers/identity/absolute-paths.js';
|
|
3
3
|
import { CLAUDE_OBSERVATION_SOURCE } from '../../../providers/identity/claude-runtime-observation.js';
|
|
4
4
|
import { CODEX_OBSERVATION_SOURCE } from '../../../providers/identity/codex-runtime-observation.js';
|
|
5
|
-
import { assertNoSensitiveDispatchContent, collectSensitiveDispatchContent, parseGenericDispatchRecord, redactSensitiveValues, } from '../../../providers/identity/generic-dispatch-record.js';
|
|
5
|
+
import { assertNoSensitiveDispatchContent, classifyDispatchOutcome, collectSensitiveDispatchContent, parseGenericDispatchRecord, redactSensitiveValues, } from '../../../providers/identity/generic-dispatch-record.js';
|
|
6
6
|
import { augmentDispatchRecord, compareObservedRuntimeMetadata, comparedObservationAxes, configuredInvocationForObservation, parseRuntimeObservation, safeParseOatDispatchEvidenceEvent, } from '../../../providers/identity/oat-dispatch-record.js';
|
|
7
7
|
import { observationFromFacts, parseRuntimeObservationEnvelope, projectRuntimeObservation, providerSupportsRuntimeObservation, } from '../../../providers/identity/runtime-observation.js';
|
|
8
|
+
import { parseDispatchStamps } from '../../../providers/identity/stamp.js';
|
|
8
9
|
import { ZodError } from 'zod';
|
|
9
10
|
/**
|
|
10
11
|
* Hard redaction boundary. Producers redact their own messages, but this is the
|
|
@@ -44,6 +45,233 @@ const OBSERVATION_SOURCE_BY_PROVIDER = {
|
|
|
44
45
|
const CALLER_ASSERTED_OBSERVATION_SOURCE = 'caller-asserted';
|
|
45
46
|
/** A managed Claude record's provider is derived, and always this value. */
|
|
46
47
|
const MANAGED_CLAUDE_PROVIDER = 'claude';
|
|
48
|
+
/**
|
|
49
|
+
* One-line journal run record, projection v1.
|
|
50
|
+
*
|
|
51
|
+
* An additive rendering of the same validated, redacted record and event: one
|
|
52
|
+
* line per request id, written by the owning skill under
|
|
53
|
+
* `## Orchestration Runs` and updated in place with the terminal outcome. The
|
|
54
|
+
* renderer writes nothing.
|
|
55
|
+
*
|
|
56
|
+
* Grammar: `run/v1` followed by named `key=value` fields in the fixed order of
|
|
57
|
+
* `DISPATCH_RUN_LINE_FIELDS`, joined by ` · `. Inside a value, `\\`, `\·`,
|
|
58
|
+
* `\~`, `\n` and `\r` escape a backslash, the delimiter, a literal tilde, a
|
|
59
|
+
* newline and a carriage return; a bare `~` value is null. The configured
|
|
60
|
+
* identity (`target`, `model`, `effort`) and the observed identity are
|
|
61
|
+
* separate fields: an observation never replaces the configured invocation.
|
|
62
|
+
*/
|
|
63
|
+
export const DISPATCH_RUN_LINE_PREFIX = 'run/v1';
|
|
64
|
+
export const DISPATCH_RUN_LINE_DELIMITER = ' · ';
|
|
65
|
+
export const DISPATCH_RUN_LINE_FIELDS = [
|
|
66
|
+
'request',
|
|
67
|
+
'scope',
|
|
68
|
+
'action',
|
|
69
|
+
'role',
|
|
70
|
+
'target',
|
|
71
|
+
'model',
|
|
72
|
+
'effort',
|
|
73
|
+
'stamp',
|
|
74
|
+
'launch',
|
|
75
|
+
'outcome',
|
|
76
|
+
'terminal',
|
|
77
|
+
'class',
|
|
78
|
+
'continuation',
|
|
79
|
+
'observed',
|
|
80
|
+
'match',
|
|
81
|
+
];
|
|
82
|
+
const NULL_VALUE = '~';
|
|
83
|
+
function escapeRunLineValue(value) {
|
|
84
|
+
if (value === null) {
|
|
85
|
+
return NULL_VALUE;
|
|
86
|
+
}
|
|
87
|
+
return value
|
|
88
|
+
.replaceAll('\\', '\\\\')
|
|
89
|
+
.replaceAll('·', '\\·')
|
|
90
|
+
.replaceAll('~', '\\~')
|
|
91
|
+
.replaceAll('\n', '\\n')
|
|
92
|
+
.replaceAll('\r', '\\r');
|
|
93
|
+
}
|
|
94
|
+
export function renderDispatchRunLine(fields) {
|
|
95
|
+
return [
|
|
96
|
+
DISPATCH_RUN_LINE_PREFIX,
|
|
97
|
+
...DISPATCH_RUN_LINE_FIELDS.map((field) => `${field}=${escapeRunLineValue(fields[field])}`),
|
|
98
|
+
].join(DISPATCH_RUN_LINE_DELIMITER);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Split a run line on unescaped delimiters, keeping escapes intact so a bare
|
|
102
|
+
* `~` (null) stays distinguishable from an escaped `\~` (literal tilde).
|
|
103
|
+
*/
|
|
104
|
+
function splitRunLine(text) {
|
|
105
|
+
const parts = [];
|
|
106
|
+
let current = '';
|
|
107
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
108
|
+
const character = text[index];
|
|
109
|
+
if (character === '\\') {
|
|
110
|
+
const next = text[index + 1];
|
|
111
|
+
if (next === undefined) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
current += character + next;
|
|
115
|
+
index += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (text.startsWith(DISPATCH_RUN_LINE_DELIMITER, index)) {
|
|
119
|
+
parts.push(current);
|
|
120
|
+
current = '';
|
|
121
|
+
index += DISPATCH_RUN_LINE_DELIMITER.length - 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
current += character;
|
|
125
|
+
}
|
|
126
|
+
parts.push(current);
|
|
127
|
+
return parts;
|
|
128
|
+
}
|
|
129
|
+
function unescapeRunLineValue(raw) {
|
|
130
|
+
if (raw === NULL_VALUE) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
let value = '';
|
|
134
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
135
|
+
const character = raw[index];
|
|
136
|
+
if (character !== '\\') {
|
|
137
|
+
if (character === '~') {
|
|
138
|
+
// An unescaped tilde inside a value is not a valid encoding.
|
|
139
|
+
throw new Error('unescaped ~ inside a value');
|
|
140
|
+
}
|
|
141
|
+
value += character;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const next = raw[index + 1];
|
|
145
|
+
index += 1;
|
|
146
|
+
if (next === 'n')
|
|
147
|
+
value += '\n';
|
|
148
|
+
else if (next === 'r')
|
|
149
|
+
value += '\r';
|
|
150
|
+
else if (next === '\\' || next === '·' || next === '~')
|
|
151
|
+
value += next;
|
|
152
|
+
else
|
|
153
|
+
throw new Error(`unknown escape \\${next ?? ''}`);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Parse one journal run line (optionally written as a `- ` list item). Every
|
|
159
|
+
* field must be present exactly once, in order; anything else is malformed.
|
|
160
|
+
* Callers derive terminality from `launch` and `outcome` with
|
|
161
|
+
* `classifyDispatchOutcome`, never from the recorded `terminal` text alone.
|
|
162
|
+
*/
|
|
163
|
+
export function parseDispatchRunLine(text) {
|
|
164
|
+
const body = text.trim().replace(/^[-*][ \t]+/, '');
|
|
165
|
+
const parts = splitRunLine(body);
|
|
166
|
+
if (parts == null || parts[0] !== DISPATCH_RUN_LINE_PREFIX) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
reason: `run line must start with ${DISPATCH_RUN_LINE_PREFIX}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const values = parts.slice(1);
|
|
173
|
+
if (values.length !== DISPATCH_RUN_LINE_FIELDS.length) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
reason: `run line carries ${values.length} fields; ${DISPATCH_RUN_LINE_FIELDS.length} are required`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const fields = {};
|
|
180
|
+
for (const [index, field] of DISPATCH_RUN_LINE_FIELDS.entries()) {
|
|
181
|
+
const raw = values[index] ?? '';
|
|
182
|
+
const prefix = `${field}=`;
|
|
183
|
+
if (!raw.startsWith(prefix)) {
|
|
184
|
+
return { ok: false, reason: `field ${index + 1} must be ${field}` };
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
fields[field] = unescapeRunLineValue(raw.slice(prefix.length));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: `${field}: ${error instanceof Error ? error.message : String(error)}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { ok: true, fields };
|
|
197
|
+
}
|
|
198
|
+
function continuationReference(entry) {
|
|
199
|
+
if (typeof entry === 'string' || typeof entry === 'number') {
|
|
200
|
+
return String(entry);
|
|
201
|
+
}
|
|
202
|
+
if (!isRecord(entry)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
for (const key of [
|
|
206
|
+
'request_id',
|
|
207
|
+
'requestId',
|
|
208
|
+
'continuation_event',
|
|
209
|
+
'event',
|
|
210
|
+
'event_id',
|
|
211
|
+
'eventId',
|
|
212
|
+
'id',
|
|
213
|
+
]) {
|
|
214
|
+
const value = entry[key];
|
|
215
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
216
|
+
return value;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Validate a caller-supplied formal `Dispatch:` stamp: one line with the
|
|
223
|
+
* canonical prefix that parses as a modern stamp. The stamp comes from the
|
|
224
|
+
* resolver's `dispatchStamp` field; the projection carries it verbatim and
|
|
225
|
+
* never assembles one.
|
|
226
|
+
*/
|
|
227
|
+
export function assertFormalDispatchStamp(stamp) {
|
|
228
|
+
if (/[\r\n]/.test(stamp) || !stamp.startsWith('Dispatch: ')) {
|
|
229
|
+
throw new Error('A dispatch stamp must be one line beginning with the canonical "Dispatch: " prefix.');
|
|
230
|
+
}
|
|
231
|
+
const parsed = parseDispatchStamps(stamp);
|
|
232
|
+
if (parsed.length !== 1 || parsed[0]?.legacy !== false) {
|
|
233
|
+
throw new Error('A dispatch stamp must be a formal Dispatch: line with scope, action and role fields.');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Project a validated record result onto the one-line run record. */
|
|
237
|
+
export function projectDispatchRunLine(result, options = {}) {
|
|
238
|
+
const { record, runtimeIdentity } = result;
|
|
239
|
+
const classification = classifyDispatchOutcome(record.launch_status, record.child_outcome);
|
|
240
|
+
const continuation = record.continuation_events
|
|
241
|
+
.map(continuationReference)
|
|
242
|
+
.filter((reference) => reference !== null);
|
|
243
|
+
const observed = runtimeIdentity.observed;
|
|
244
|
+
const fields = {
|
|
245
|
+
request: record.request_id,
|
|
246
|
+
scope: record.scope,
|
|
247
|
+
action: record.action,
|
|
248
|
+
role: record.role_name,
|
|
249
|
+
target: record.role_selector,
|
|
250
|
+
model: record.model_selector,
|
|
251
|
+
effort: record.effort_selector,
|
|
252
|
+
stamp: options.dispatchStamp ?? null,
|
|
253
|
+
launch: record.launch_status,
|
|
254
|
+
outcome: record.child_outcome,
|
|
255
|
+
terminal: classification.terminal ? 'yes' : 'no',
|
|
256
|
+
class: classification.classification,
|
|
257
|
+
continuation: continuation.length === 0 ? null : continuation.join(','),
|
|
258
|
+
observed: observed === null
|
|
259
|
+
? null
|
|
260
|
+
: [
|
|
261
|
+
observed.provider,
|
|
262
|
+
observed.model ?? '?',
|
|
263
|
+
observed.effort ?? '?',
|
|
264
|
+
].join('/'),
|
|
265
|
+
match: runtimeIdentity.match,
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
version: 1,
|
|
269
|
+
line: renderDispatchRunLine(fields),
|
|
270
|
+
fields,
|
|
271
|
+
classification: classification.classification,
|
|
272
|
+
terminal: classification.terminal,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
47
275
|
function isRecord(value) {
|
|
48
276
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
49
277
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/project/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BpC,wBAAgB,oBAAoB,IAAI,OAAO,CA2B9C"}
|
|
@@ -6,6 +6,7 @@ import { createProjectCompleteStateCommand } from './complete-state/index.js';
|
|
|
6
6
|
import { createProjectDeliveryCommand } from './delivery/index.js';
|
|
7
7
|
import { createProjectDispatchCommand } from './dispatch/index.js';
|
|
8
8
|
import { createProjectDispatchCeilingCommand } from './dispatch-ceiling/index.js';
|
|
9
|
+
import { createProjectJournalCommand } from './journal-check/index.js';
|
|
9
10
|
import { createProjectLinksCommand } from './links/index.js';
|
|
10
11
|
import { createProjectListCommand } from './list.js';
|
|
11
12
|
import { createProjectLogCommand } from './log/index.js';
|
|
@@ -32,6 +33,7 @@ export function createProjectCommand() {
|
|
|
32
33
|
.addCommand(createProjectDeliveryCommand())
|
|
33
34
|
.addCommand(createProjectDispatchCommand())
|
|
34
35
|
.addCommand(createProjectDispatchCeilingCommand())
|
|
36
|
+
.addCommand(createProjectJournalCommand())
|
|
35
37
|
.addCommand(createProjectListCommand())
|
|
36
38
|
.addCommand(createProjectLinksCommand())
|
|
37
39
|
.addCommand(createProjectLogCommand())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JournalCheckInput, JournalCheckResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Evaluate one journal at one lifecycle boundary. Pure and read-only: the
|
|
4
|
+
* same bytes always give the same result, and nothing is written.
|
|
5
|
+
*
|
|
6
|
+
* Task modes (spec-driven, quick, import, lite) use the shared journal record
|
|
7
|
+
* reader: normalized plan task ids map to unique journal blocks with exactly
|
|
8
|
+
* one owned terminal status and owned commit evidence or an approved scope
|
|
9
|
+
* disposition. Brief and goal use the landed phase-unit progress and goal
|
|
10
|
+
* outcome audit readers; failed, cancelled and no-change execution records
|
|
11
|
+
* are valid history without a SHA and approved deferrals stay incomplete.
|
|
12
|
+
* Delivery coverage and the post-implementation sequence stay with the
|
|
13
|
+
* closeout invariant (`closeout-check`), which `complete-state` evaluates
|
|
14
|
+
* beside this evaluator.
|
|
15
|
+
*/
|
|
16
|
+
export declare function evaluateJournal(input: JournalCheckInput): JournalCheckResult;
|
|
17
|
+
//# sourceMappingURL=evaluator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/journal-check/evaluator.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EACV,iBAAiB,EAEjB,kBAAkB,EAEnB,MAAM,SAAS,CAAC;AA6CjB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,kBAAkB,CA4G5E"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { parseBriefOutcomes, parseTaskProgress, readFrontmatterMetadata, readJournalRecords, validateGoalOutcomeAudit, } from '@open-agent-toolkit/control-plane';
|
|
2
|
+
import { checkDispatchPayloads, checkMetadata, checkOverviewTotals, checkPhaseUnitRecords, checkReceiptsAndDeferrals, checkReferencesLast, checkRunLines, checkTaskModePhaseStatus, checkTaskModeRecords, checkTemplateResidue, isBriefMode, OVERRIDE_ELIGIBILITY, overrideCoversSubject, readOverrides, } from './rules.js';
|
|
3
|
+
function workflowModeOf(stateRecord) {
|
|
4
|
+
const mode = stateRecord?.oat_workflow_mode;
|
|
5
|
+
return typeof mode === 'string' && mode.length > 0 ? mode : 'spec-driven';
|
|
6
|
+
}
|
|
7
|
+
function approvedDeferredPhases(workflowMode, stateRecord, input, progress) {
|
|
8
|
+
const phases = new Set();
|
|
9
|
+
if (workflowMode !== 'goal') {
|
|
10
|
+
return phases;
|
|
11
|
+
}
|
|
12
|
+
// Only a valid, exhaustive outcome audit can approve a deferral; an invalid
|
|
13
|
+
// or missing audit approves nothing (the closeout invariant reports it).
|
|
14
|
+
const validation = validateGoalOutcomeAudit(stateRecord?.oat_goal_outcome_audit, progress.phases, parseBriefOutcomes(input.planContent));
|
|
15
|
+
if (!validation.valid || validation.audit === null) {
|
|
16
|
+
return phases;
|
|
17
|
+
}
|
|
18
|
+
for (const [phaseId, entry] of Object.entries(validation.audit.phases)) {
|
|
19
|
+
if (entry.disposition === 'deferred' || entry.disposition === 'retired') {
|
|
20
|
+
phases.add(phaseId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return phases;
|
|
24
|
+
}
|
|
25
|
+
function toReason(finding) {
|
|
26
|
+
return {
|
|
27
|
+
code: finding.code,
|
|
28
|
+
path: finding.path,
|
|
29
|
+
line: finding.line,
|
|
30
|
+
owner: finding.owner,
|
|
31
|
+
message: finding.message,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Evaluate one journal at one lifecycle boundary. Pure and read-only: the
|
|
36
|
+
* same bytes always give the same result, and nothing is written.
|
|
37
|
+
*
|
|
38
|
+
* Task modes (spec-driven, quick, import, lite) use the shared journal record
|
|
39
|
+
* reader: normalized plan task ids map to unique journal blocks with exactly
|
|
40
|
+
* one owned terminal status and owned commit evidence or an approved scope
|
|
41
|
+
* disposition. Brief and goal use the landed phase-unit progress and goal
|
|
42
|
+
* outcome audit readers; failed, cancelled and no-change execution records
|
|
43
|
+
* are valid history without a SHA and approved deferrals stay incomplete.
|
|
44
|
+
* Delivery coverage and the post-implementation sequence stay with the
|
|
45
|
+
* closeout invariant (`closeout-check`), which `complete-state` evaluates
|
|
46
|
+
* beside this evaluator.
|
|
47
|
+
*/
|
|
48
|
+
export function evaluateJournal(input) {
|
|
49
|
+
const journalMeta = readFrontmatterMetadata(input.journalContent);
|
|
50
|
+
const stateMeta = readFrontmatterMetadata(input.stateContent);
|
|
51
|
+
const stateRecord = stateMeta.state === 'ok' ? stateMeta.record : null;
|
|
52
|
+
const workflowMode = workflowModeOf(stateRecord);
|
|
53
|
+
const briefMode = isBriefMode(workflowMode);
|
|
54
|
+
const progress = parseTaskProgress(input.planContent, input.journalContent, {
|
|
55
|
+
phaseUnits: briefMode,
|
|
56
|
+
extractedUnits: workflowMode === 'goal',
|
|
57
|
+
});
|
|
58
|
+
const scope = input.boundary === 'review' && input.scope != null
|
|
59
|
+
? [...new Set(input.scope)]
|
|
60
|
+
: null;
|
|
61
|
+
const context = {
|
|
62
|
+
boundary: input.boundary,
|
|
63
|
+
workflowMode,
|
|
64
|
+
scope: scope === null ? null : new Set(scope),
|
|
65
|
+
journal: readJournalRecords(input.journalContent),
|
|
66
|
+
plan: readJournalRecords(input.planContent),
|
|
67
|
+
journalMeta,
|
|
68
|
+
stateMeta,
|
|
69
|
+
progress,
|
|
70
|
+
approvedDeferredPhases: approvedDeferredPhases(workflowMode, stateRecord, input, progress),
|
|
71
|
+
findings: [],
|
|
72
|
+
};
|
|
73
|
+
checkMetadata(context, input.stateContent.split('\n'));
|
|
74
|
+
if (briefMode) {
|
|
75
|
+
checkPhaseUnitRecords(context);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
checkTaskModeRecords(context);
|
|
79
|
+
checkTaskModePhaseStatus(context);
|
|
80
|
+
}
|
|
81
|
+
checkOverviewTotals(context);
|
|
82
|
+
checkReferencesLast(context);
|
|
83
|
+
checkReceiptsAndDeferrals(context);
|
|
84
|
+
checkDispatchPayloads(context);
|
|
85
|
+
checkRunLines(context);
|
|
86
|
+
checkTemplateResidue(context);
|
|
87
|
+
const parsedOverrides = readOverrides(context);
|
|
88
|
+
// Overrides suppress only eligible gaps whose subject the override's scope
|
|
89
|
+
// covers; nothing else is overridable, and the checker never writes one.
|
|
90
|
+
const applied = new Set();
|
|
91
|
+
const remaining = context.findings.filter((finding) => {
|
|
92
|
+
if (finding.severity !== 'refuse' || finding.subject === undefined) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const subject = finding.subject;
|
|
96
|
+
const covering = parsedOverrides.find((override) => OVERRIDE_ELIGIBILITY[override.code].has(finding.code) &&
|
|
97
|
+
overrideCoversSubject(override, subject));
|
|
98
|
+
if (covering === undefined) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
applied.add(covering.line);
|
|
102
|
+
return false;
|
|
103
|
+
});
|
|
104
|
+
const overrides = parsedOverrides.map((override) => ({
|
|
105
|
+
code: override.code,
|
|
106
|
+
authorizedBy: override.authorizedBy,
|
|
107
|
+
scope: override.scope,
|
|
108
|
+
reason: override.reason,
|
|
109
|
+
line: override.line,
|
|
110
|
+
applied: applied.has(override.line),
|
|
111
|
+
}));
|
|
112
|
+
const warnings = remaining
|
|
113
|
+
.filter((finding) => finding.severity === 'warn')
|
|
114
|
+
.map(toReason);
|
|
115
|
+
for (const override of overrides) {
|
|
116
|
+
if (!override.applied) {
|
|
117
|
+
warnings.push({
|
|
118
|
+
code: 'override-unused',
|
|
119
|
+
path: 'implementation.md',
|
|
120
|
+
line: override.line,
|
|
121
|
+
owner: 'oat-project-complete',
|
|
122
|
+
message: `Override ${override.code} for ${override.scope} covers no eligible gap.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const reasons = remaining
|
|
127
|
+
.filter((finding) => finding.severity === 'refuse')
|
|
128
|
+
.map(toReason)
|
|
129
|
+
.sort((left, right) => (left.line ?? Number.MAX_SAFE_INTEGER) -
|
|
130
|
+
(right.line ?? Number.MAX_SAFE_INTEGER));
|
|
131
|
+
return {
|
|
132
|
+
status: reasons.length === 0 ? 'ok' : 'refused',
|
|
133
|
+
boundary: input.boundary,
|
|
134
|
+
workflowMode,
|
|
135
|
+
scope,
|
|
136
|
+
reasons,
|
|
137
|
+
warnings,
|
|
138
|
+
overrides,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type CommandContext, type GlobalOptions } from '../../../app/command-context.js';
|
|
2
|
+
import { type ActiveProjectResolution } from '../../../config/oat-config.js';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { type JournalCheckReason, type JournalCheckResult } from './types.js';
|
|
5
|
+
export { evaluateJournal } from './evaluator.js';
|
|
6
|
+
export type { JournalCheckBoundary, JournalCheckReason, JournalCheckResult, } from './types.js';
|
|
7
|
+
export interface JournalCheckFiles {
|
|
8
|
+
planContent: string;
|
|
9
|
+
journalContent: string;
|
|
10
|
+
stateContent: string;
|
|
11
|
+
}
|
|
12
|
+
export interface JournalCheckDependencies {
|
|
13
|
+
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
14
|
+
resolveProjectRoot: (cwd: string) => Promise<string>;
|
|
15
|
+
resolveActiveProject: (repoRoot: string) => Promise<ActiveProjectResolution>;
|
|
16
|
+
readFile: (path: string, encoding: 'utf8') => Promise<string>;
|
|
17
|
+
dirExists: (path: string) => Promise<boolean>;
|
|
18
|
+
fileExists: (path: string) => Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read the three inputs the evaluator needs. A missing journal or plan is a
|
|
22
|
+
* command error, never an empty file: an unreadable input must not read as a
|
|
23
|
+
* valid journal.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readJournalCheckFiles(projectDir: string, dependencies: Pick<JournalCheckDependencies, 'readFile' | 'fileExists'>): Promise<JournalCheckFiles>;
|
|
26
|
+
/**
|
|
27
|
+
* Parse `--scope pNN[,pNN]` into normalized phase ids, each of which must name
|
|
28
|
+
* a phase the plan declares.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseJournalScope(raw: string, planContent: string, workflowMode: 'task' | 'phase-unit'): string[];
|
|
31
|
+
export declare function formatJournalReason(reason: JournalCheckReason): string;
|
|
32
|
+
export declare function formatJournalRefusal(projectPath: string, result: JournalCheckResult): string;
|
|
33
|
+
export declare function createProjectJournalCommand(overrides?: Partial<JournalCheckDependencies>): Command;
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/journal-check/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAS5B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,kBAAkB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,oBAAoB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC7E,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAChD;AAWD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,IAAI,CAAC,wBAAwB,EAAE,UAAU,GAAG,YAAY,CAAC,GACtE,OAAO,CAAC,iBAAiB,CAAC,CAa5B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GAAG,YAAY,GAClC,MAAM,EAAE,CAuBV;AASD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAItE;AAED,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,kBAAkB,GACzB,MAAM,CAKR;AAwGD,wBAAgB,2BAA2B,CACzC,SAAS,GAAE,OAAO,CAAC,wBAAwB,CAAM,GAChD,OAAO,CAiCT"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFile as defaultReadFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { buildCommandContext, } from '../../../app/command-context.js';
|
|
4
|
+
import { readGlobalOptions } from '../../shared/shared.utils.js';
|
|
5
|
+
import { resolveActiveProject, } from '../../../config/oat-config.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 { normalizePhaseId, parseTaskProgress, readFrontmatterMetadata, } from '@open-agent-toolkit/control-plane';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
import { evaluateJournal } from './evaluator.js';
|
|
12
|
+
import { JOURNAL_CHECK_BOUNDARIES, } from './types.js';
|
|
13
|
+
export { evaluateJournal } from './evaluator.js';
|
|
14
|
+
const DEFAULT_DEPENDENCIES = {
|
|
15
|
+
buildCommandContext,
|
|
16
|
+
resolveProjectRoot,
|
|
17
|
+
resolveActiveProject,
|
|
18
|
+
readFile: (path, encoding) => defaultReadFile(path, encoding),
|
|
19
|
+
dirExists,
|
|
20
|
+
fileExists,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Read the three inputs the evaluator needs. A missing journal or plan is a
|
|
24
|
+
* command error, never an empty file: an unreadable input must not read as a
|
|
25
|
+
* valid journal.
|
|
26
|
+
*/
|
|
27
|
+
export async function readJournalCheckFiles(projectDir, dependencies) {
|
|
28
|
+
const read = async (name) => {
|
|
29
|
+
const path = join(projectDir, name);
|
|
30
|
+
if (!(await dependencies.fileExists(path))) {
|
|
31
|
+
throw new CliError(`Project ${name} not found: ${path}`, 1);
|
|
32
|
+
}
|
|
33
|
+
return dependencies.readFile(path, 'utf8');
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
planContent: await read('plan.md'),
|
|
37
|
+
journalContent: await read('implementation.md'),
|
|
38
|
+
stateContent: await read('state.md'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parse `--scope pNN[,pNN]` into normalized phase ids, each of which must name
|
|
43
|
+
* a phase the plan declares.
|
|
44
|
+
*/
|
|
45
|
+
export function parseJournalScope(raw, planContent, workflowMode) {
|
|
46
|
+
const tokens = raw
|
|
47
|
+
.split(',')
|
|
48
|
+
.map((token) => token.trim())
|
|
49
|
+
.filter((token) => token.length > 0);
|
|
50
|
+
if (tokens.length === 0) {
|
|
51
|
+
throw new CliError('--scope needs at least one phase id (pNN).', 1);
|
|
52
|
+
}
|
|
53
|
+
const declared = new Set(parseTaskProgress(planContent, '', {
|
|
54
|
+
phaseUnits: workflowMode === 'phase-unit',
|
|
55
|
+
}).phases.map((phase) => phase.phaseId));
|
|
56
|
+
return tokens.map((token) => {
|
|
57
|
+
const phaseId = normalizePhaseId(token);
|
|
58
|
+
if (phaseId === null) {
|
|
59
|
+
throw new CliError(`--scope ${token} is not a phase id (pNN).`, 1);
|
|
60
|
+
}
|
|
61
|
+
if (!declared.has(phaseId)) {
|
|
62
|
+
throw new CliError(`--scope ${token} names no phase in plan.md.`, 1);
|
|
63
|
+
}
|
|
64
|
+
return phaseId;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function readWorkflowMode(stateContent) {
|
|
68
|
+
const metadata = readFrontmatterMetadata(stateContent);
|
|
69
|
+
const mode = metadata.state === 'ok' ? metadata.record.oat_workflow_mode : undefined;
|
|
70
|
+
return typeof mode === 'string' && mode.length > 0 ? mode : 'spec-driven';
|
|
71
|
+
}
|
|
72
|
+
export function formatJournalReason(reason) {
|
|
73
|
+
const location = reason.line === null ? reason.path : `${reason.path}:${reason.line}`;
|
|
74
|
+
return `${reason.code} ${location} (${reason.owner}): ${reason.message}`;
|
|
75
|
+
}
|
|
76
|
+
export function formatJournalRefusal(projectPath, result) {
|
|
77
|
+
return [
|
|
78
|
+
`Journal check refused ${projectPath} at ${result.boundary} (${result.reasons.length} reason${result.reasons.length === 1 ? '' : 's'}):`,
|
|
79
|
+
...result.reasons.map((reason) => `- ${formatJournalReason(reason)}`),
|
|
80
|
+
].join('\n');
|
|
81
|
+
}
|
|
82
|
+
async function resolveProjectDir(options, repoRoot, dependencies) {
|
|
83
|
+
if (options.project) {
|
|
84
|
+
const projectDir = isAbsolute(options.project)
|
|
85
|
+
? resolve(options.project)
|
|
86
|
+
: resolve(repoRoot, options.project);
|
|
87
|
+
if (!(await dependencies.dirExists(projectDir)) ||
|
|
88
|
+
!(await dependencies.fileExists(join(projectDir, 'state.md')))) {
|
|
89
|
+
throw new CliError(`Project path is missing or invalid: ${options.project}. Pass --project <path> for a directory containing state.md.`, 1);
|
|
90
|
+
}
|
|
91
|
+
return projectDir;
|
|
92
|
+
}
|
|
93
|
+
const active = await dependencies.resolveActiveProject(repoRoot);
|
|
94
|
+
if (active.status !== 'active' || !active.path) {
|
|
95
|
+
throw new CliError('No active project resolves. Set activeProject or pass --project <path>.', 1);
|
|
96
|
+
}
|
|
97
|
+
return resolve(repoRoot, active.path);
|
|
98
|
+
}
|
|
99
|
+
async function runJournalCheck(options, context, dependencies) {
|
|
100
|
+
try {
|
|
101
|
+
const boundary = options.at;
|
|
102
|
+
if (boundary === undefined ||
|
|
103
|
+
!JOURNAL_CHECK_BOUNDARIES.includes(boundary)) {
|
|
104
|
+
throw new CliError(`--at must be one of ${JOURNAL_CHECK_BOUNDARIES.join(', ')}.`, 1);
|
|
105
|
+
}
|
|
106
|
+
if (boundary === 'review' && !options.scope) {
|
|
107
|
+
throw new CliError('--at review needs an explicit --scope pNN[,pNN].', 1);
|
|
108
|
+
}
|
|
109
|
+
if (boundary !== 'review' && options.scope) {
|
|
110
|
+
throw new CliError('--scope applies only to --at review.', 1);
|
|
111
|
+
}
|
|
112
|
+
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
113
|
+
const projectDir = await resolveProjectDir(options, repoRoot, dependencies);
|
|
114
|
+
const files = await readJournalCheckFiles(projectDir, dependencies);
|
|
115
|
+
const workflowMode = readWorkflowMode(files.stateContent);
|
|
116
|
+
const scope = boundary === 'review' && options.scope
|
|
117
|
+
? parseJournalScope(options.scope, files.planContent, workflowMode === 'brief' || workflowMode === 'goal'
|
|
118
|
+
? 'phase-unit'
|
|
119
|
+
: 'task')
|
|
120
|
+
: null;
|
|
121
|
+
const result = evaluateJournal({ boundary, scope, ...files });
|
|
122
|
+
if (context.json) {
|
|
123
|
+
context.logger.json({ ...result, projectPath: projectDir });
|
|
124
|
+
}
|
|
125
|
+
else if (result.status === 'ok') {
|
|
126
|
+
context.logger.info(`Journal check passed at ${boundary}${scope === null ? '' : ` for ${scope.join(', ')}`}.`);
|
|
127
|
+
for (const warning of result.warnings) {
|
|
128
|
+
context.logger.warn(`warning: ${formatJournalReason(warning)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
context.logger.info(formatJournalRefusal(projectDir, result));
|
|
133
|
+
for (const warning of result.warnings) {
|
|
134
|
+
context.logger.warn(`warning: ${formatJournalReason(warning)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
process.exitCode = result.status === 'ok' ? 0 : 1;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
if (context.json) {
|
|
142
|
+
context.logger.json({ status: 'error', message });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
context.logger.error(message);
|
|
146
|
+
}
|
|
147
|
+
process.exitCode = error instanceof CliError ? error.exitCode : 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function createProjectJournalCommand(overrides = {}) {
|
|
151
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
152
|
+
return new Command('journal')
|
|
153
|
+
.description('Validate the implementation journal (read-only)')
|
|
154
|
+
.addCommand(new Command('check')
|
|
155
|
+
.description('Evaluate implementation.md against plan.md and state.md at one lifecycle boundary (writes nothing)')
|
|
156
|
+
.option('--project <path>', 'Project directory (defaults to the active project)')
|
|
157
|
+
.option('--at <boundary>', 'Boundary: implement-preflight, review, or completion')
|
|
158
|
+
.option('--scope <phases>', 'Review only: comma-separated phase ids to check (pNN[,pNN])')
|
|
159
|
+
.action(async (options, command) => {
|
|
160
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
161
|
+
await runJournalCheck(options, context, dependencies);
|
|
162
|
+
}));
|
|
163
|
+
}
|