@open-agent-toolkit/cli 0.3.21 → 0.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/reference/cli-reference.md +8 -3
  3. package/assets/docs/reference/configuration.md +3 -0
  4. package/assets/docs/reference/oat-directory-structure.md +1 -1
  5. package/assets/docs/reference/project-artifacts.md +141 -2
  6. package/assets/docs/reference/project-state-machine.md +2 -2
  7. package/assets/docs/workflows/advanced/autonomy.md +29 -12
  8. package/assets/docs/workflows/advanced/cursor-cloud.md +20 -6
  9. package/assets/docs/workflows/advanced/workflow-gates.md +9 -0
  10. package/assets/docs/workflows/projects/closeout/closeout-skills.md +24 -5
  11. package/assets/docs/workflows/projects/closeout/retro.md +6 -2
  12. package/assets/docs/workflows/projects/execution/implementation-execution.md +18 -4
  13. package/assets/docs/workflows/projects/execution/index.md +1 -1
  14. package/assets/docs/workflows/projects/execution/picking-up-projects.md +3 -2
  15. package/assets/docs/workflows/projects/execution/project-log.md +100 -11
  16. package/assets/docs/workflows/projects/lifecycle.md +3 -3
  17. package/assets/docs/workflows/projects/reviews/index.md +18 -0
  18. package/assets/public-package-versions.json +4 -4
  19. package/assets/skills/oat-cursor-cloud-projects/SKILL.md +12 -8
  20. package/assets/skills/oat-cursor-cloud-projects/references/cursor-cloud-mechanics.md +63 -2
  21. package/assets/skills/oat-project-autonomous/SKILL.md +88 -26
  22. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +1 -1
  23. package/assets/skills/oat-project-brief/SKILL.md +1 -1
  24. package/assets/skills/oat-project-brief/references/brief-contract.md +33 -27
  25. package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +1 -1
  26. package/assets/skills/oat-project-complete/SKILL.md +142 -25
  27. package/assets/skills/oat-project-complete-auto/SKILL.md +4 -4
  28. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +10 -5
  29. package/assets/skills/oat-project-document/SKILL.md +1 -1
  30. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +1 -1
  31. package/assets/skills/oat-project-goal/SKILL.md +63 -15
  32. package/assets/skills/oat-project-goal/references/docs/autonomy-contract.md +1 -1
  33. package/assets/skills/oat-project-implement/SKILL.md +6 -6
  34. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +28 -7
  35. package/assets/skills/oat-project-implement/references/dispatch-and-dry-run.md +10 -5
  36. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +1 -1
  37. package/assets/skills/oat-project-implement/references/phase-execution.md +23 -9
  38. package/assets/skills/oat-project-implement/references/plan-and-resume.md +4 -4
  39. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  40. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +1 -1
  41. package/assets/skills/oat-project-pr-final/SKILL.md +6 -1
  42. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +1 -1
  43. package/assets/skills/oat-project-pr-progress/SKILL.md +6 -1
  44. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  45. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +1 -1
  46. package/assets/skills/oat-project-reconcile/SKILL.md +13 -19
  47. package/assets/skills/oat-project-retro/SKILL.md +5 -2
  48. package/assets/skills/oat-project-retro/references/evidence-and-lanes.md +9 -3
  49. package/assets/skills/oat-project-review-provide/SKILL.md +28 -3
  50. package/assets/skills/oat-project-review-receive/SKILL.md +32 -18
  51. package/assets/skills/oat-project-review-receive-remote/SKILL.md +10 -6
  52. package/assets/skills/oat-project-revise/SKILL.md +9 -2
  53. package/assets/skills/oat-project-summary/SKILL.md +166 -37
  54. package/assets/skills/oat-worktree-bootstrap-auto/SKILL.md +3 -3
  55. package/assets/templates/implementation-brief.md +5 -36
  56. package/assets/templates/implementation.md +11 -81
  57. package/assets/templates/project-log.md +2 -31
  58. package/assets/templates/project-retro.md +4 -1
  59. package/assets/templates/summary.md +13 -5
  60. package/dist/commands/config/index.d.ts.map +1 -1
  61. package/dist/commands/config/index.js +44 -1
  62. package/dist/commands/project/archive/archive-utils.d.ts +205 -8
  63. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  64. package/dist/commands/project/archive/archive-utils.js +708 -44
  65. package/dist/commands/project/archive/index.d.ts.map +1 -1
  66. package/dist/commands/project/archive/index.js +1 -0
  67. package/dist/commands/project/archive/push-runner.d.ts +9 -1
  68. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  69. package/dist/commands/project/archive/push-runner.js +56 -18
  70. package/dist/commands/project/archive/sync-runner.d.ts +28 -0
  71. package/dist/commands/project/archive/sync-runner.d.ts.map +1 -1
  72. package/dist/commands/project/archive/sync-runner.js +44 -22
  73. package/dist/commands/project/complete-state/index.d.ts.map +1 -1
  74. package/dist/commands/project/complete-state/index.js +45 -0
  75. package/dist/commands/project/dispatch/index.d.ts.map +1 -1
  76. package/dist/commands/project/dispatch/index.js +24 -2
  77. package/dist/commands/project/dispatch/record.d.ts +54 -1
  78. package/dist/commands/project/dispatch/record.d.ts.map +1 -1
  79. package/dist/commands/project/dispatch/record.js +229 -1
  80. package/dist/commands/project/index.d.ts.map +1 -1
  81. package/dist/commands/project/index.js +2 -0
  82. package/dist/commands/project/journal-check/evaluator.d.ts +17 -0
  83. package/dist/commands/project/journal-check/evaluator.d.ts.map +1 -0
  84. package/dist/commands/project/journal-check/evaluator.js +140 -0
  85. package/dist/commands/project/journal-check/index.d.ts +34 -0
  86. package/dist/commands/project/journal-check/index.d.ts.map +1 -0
  87. package/dist/commands/project/journal-check/index.js +163 -0
  88. package/dist/commands/project/journal-check/rules.d.ts +68 -0
  89. package/dist/commands/project/journal-check/rules.d.ts.map +1 -0
  90. package/dist/commands/project/journal-check/rules.js +994 -0
  91. package/dist/commands/project/journal-check/types.d.ts +63 -0
  92. package/dist/commands/project/journal-check/types.d.ts.map +1 -0
  93. package/dist/commands/project/journal-check/types.js +26 -0
  94. package/dist/commands/project/log/append.d.ts +6 -0
  95. package/dist/commands/project/log/append.d.ts.map +1 -1
  96. package/dist/commands/project/log/append.js +114 -6
  97. package/dist/commands/project/log/check.d.ts +23 -1
  98. package/dist/commands/project/log/check.d.ts.map +1 -1
  99. package/dist/commands/project/log/check.js +20 -3
  100. package/dist/commands/project/log/grammar.d.ts +37 -0
  101. package/dist/commands/project/log/grammar.d.ts.map +1 -1
  102. package/dist/commands/project/log/grammar.js +72 -0
  103. package/dist/commands/project/log/rollup.d.ts +87 -1
  104. package/dist/commands/project/log/rollup.d.ts.map +1 -1
  105. package/dist/commands/project/log/rollup.js +257 -5
  106. package/dist/config/oat-config.d.ts +14 -0
  107. package/dist/config/oat-config.d.ts.map +1 -1
  108. package/dist/config/oat-config.js +17 -0
  109. package/dist/config/resolve.d.ts.map +1 -1
  110. package/dist/config/resolve.js +5 -1
  111. package/dist/providers/identity/generic-dispatch-record.d.ts +33 -0
  112. package/dist/providers/identity/generic-dispatch-record.d.ts.map +1 -1
  113. package/dist/providers/identity/generic-dispatch-record.js +60 -1
  114. 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;AA0BpC,wBAAgB,oBAAoB,IAAI,OAAO,CA0B9C"}
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
+ }