@open-agent-toolkit/cli 0.3.18 → 0.3.19
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/agents/oat-phase-implementer.md +81 -3
- package/assets/agents/oat-reviewer.md +15 -5
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/reference/cli-reference.md +8 -5
- package/assets/docs/reference/oat-directory-structure.md +1 -0
- package/assets/docs/reference/project-artifacts.md +17 -1
- package/assets/docs/reference/project-state-machine.md +19 -4
- package/assets/docs/skills/brainstorm.md +2 -1
- package/assets/docs/skills/index.md +1 -0
- package/assets/docs/workflows/advanced/autonomy.md +9 -1
- package/assets/docs/workflows/advanced/workflow-gates.md +11 -6
- package/assets/docs/workflows/approvals-and-automation.md +13 -8
- package/assets/docs/workflows/choose-workflow.md +16 -8
- package/assets/docs/workflows/projects/closeout/closeout-skills.md +8 -3
- package/assets/docs/workflows/projects/closeout/pr-flow.md +2 -0
- package/assets/docs/workflows/projects/execution/execution-skills.md +14 -2
- package/assets/docs/workflows/projects/execution/implementation-execution.md +15 -0
- package/assets/docs/workflows/projects/execution/picking-up-projects.md +10 -1
- package/assets/docs/workflows/projects/lifecycle.md +73 -9
- package/assets/docs/workflows/projects/planning/hill-checkpoints.md +18 -1
- package/assets/docs/workflows/projects/planning/planning-skills.md +11 -10
- package/assets/docs/workflows/projects/planning/starting-projects.md +92 -20
- package/assets/docs/workflows/projects/reviews/index.md +15 -2
- 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 +33 -9
- package/assets/skills/oat-brainstorm/references/destinations.md +9 -4
- package/assets/skills/oat-docs/SKILL.md +9 -4
- package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -8
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +17 -2
- package/assets/skills/oat-project-brief/SKILL.md +592 -0
- package/assets/skills/oat-project-brief/references/brief-contract.md +294 -0
- package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +393 -0
- package/assets/skills/oat-project-brief/references/docs/gate-approval-record.md +84 -0
- package/assets/skills/oat-project-capture/SKILL.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +7 -2
- package/assets/skills/oat-project-discover/SKILL.md +3 -2
- package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -1
- package/assets/skills/oat-project-document/SKILL.md +11 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +40 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-implement/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-implement/references/phase-execution.md +166 -1
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +97 -3
- package/assets/skills/oat-project-import-plan/SKILL.md +5 -1
- package/assets/skills/oat-project-lite/SKILL.md +4 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-new/SKILL.md +5 -1
- package/assets/skills/oat-project-next/SKILL.md +31 -4
- package/assets/skills/oat-project-next/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-plan/SKILL.md +12 -2
- package/assets/skills/oat-project-plan-writing/SKILL.md +6 -4
- package/assets/skills/oat-project-pr-final/SKILL.md +15 -3
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-pr-progress/SKILL.md +31 -4
- package/assets/skills/oat-project-progress/SKILL.md +30 -8
- package/assets/skills/oat-project-progress/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/SKILL.md +60 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +17 -2
- package/assets/skills/oat-project-quick-start/references/docs/gate-approval-record.md +4 -0
- package/assets/skills/oat-project-reconcile/SKILL.md +65 -3
- package/assets/skills/oat-project-review-provide/SKILL.md +331 -12
- package/assets/skills/oat-project-review-receive/SKILL.md +86 -1
- package/assets/skills/oat-project-revise/SKILL.md +27 -4
- package/assets/skills/oat-project-summary/SKILL.md +95 -2
- package/assets/skills/oat-wave-execute/SKILL.md +3 -2
- package/assets/templates/implementation-brief.md +165 -0
- package/assets/templates/plan-brief.md +140 -0
- package/assets/templates/plan.md +1 -1
- package/assets/templates/state.md +8 -2
- package/dist/commands/project/new/scaffold.d.ts +1 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +35 -7
- package/dist/commands/project/promote/promote.d.ts +32 -1
- package/dist/commands/project/promote/promote.d.ts.map +1 -1
- package/dist/commands/project/promote/promote.js +562 -5
- package/dist/commands/project/validate-plan/frontmatter.d.ts +12 -0
- package/dist/commands/project/validate-plan/frontmatter.d.ts.map +1 -0
- package/dist/commands/project/validate-plan/frontmatter.js +27 -0
- package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/index.js +52 -18
- package/dist/commands/project/validate-plan/profiles.d.ts +54 -0
- package/dist/commands/project/validate-plan/profiles.d.ts.map +1 -0
- package/dist/commands/project/validate-plan/profiles.js +269 -0
- package/dist/commands/project/validate-plan/validate-plan.d.ts +3 -19
- package/dist/commands/project/validate-plan/validate-plan.d.ts.map +1 -1
- package/dist/commands/project/validate-plan/validate-plan.js +5 -82
- 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 +51 -2
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +3 -0
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@ import { buildCommandContext, } from '../../../app/command-context.js';
|
|
|
5
5
|
import { applyTemplateReplacements } from '../../project/new/scaffold.js';
|
|
6
6
|
import { defaultGitRunner } from '../../project/sync/git.js';
|
|
7
7
|
import { buildSyncTarget, pushSynced as defaultPushSynced, } from '../../project/sync/ref-sync.js';
|
|
8
|
+
import { parsePhaseHeadingLine } from '../../project/validate-plan/profiles.js';
|
|
8
9
|
import { commitExactPaths, } from '../../shared/exact-path-commit.js';
|
|
9
10
|
import { getFrontmatterBlock } from '../../shared/frontmatter.js';
|
|
10
11
|
import { replaceFrontmatter } from '../../shared/frontmatter-write.js';
|
|
@@ -15,6 +16,7 @@ import { resolveTemplate as defaultResolveTemplate } from '../../shared/template
|
|
|
15
16
|
import { resolveAssetsRoot } from '../../../fs/assets.js';
|
|
16
17
|
import { fileExists as defaultFileExists } from '../../../fs/io.js';
|
|
17
18
|
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
19
|
+
import { parsePhaseMarkers, parseTaskProgress, recordLineMask, } from '@open-agent-toolkit/control-plane';
|
|
18
20
|
import { Command } from 'commander';
|
|
19
21
|
import YAML from 'yaml';
|
|
20
22
|
const PROMOTED_FILES = [
|
|
@@ -181,7 +183,7 @@ function renderQuickState(stateContent, statePath, nowUtc) {
|
|
|
181
183
|
function repoRelativePath(repoRoot, path) {
|
|
182
184
|
return relative(repoRoot, path).split(sep).join('/');
|
|
183
185
|
}
|
|
184
|
-
async function persistPromotion(repoRoot, projectsRoot, projectRoot, projectName, scope, identity, dependencies) {
|
|
186
|
+
async function persistPromotion(repoRoot, projectsRoot, projectRoot, projectName, scope, identity, dependencies, files = PROMOTED_FILES) {
|
|
185
187
|
const message = `chore(oat): promote ${projectName} to quick`;
|
|
186
188
|
if (scope === 'synced') {
|
|
187
189
|
const target = buildSyncTarget(repoRoot, projectsRoot, projectName);
|
|
@@ -194,7 +196,7 @@ async function persistPromotion(repoRoot, projectsRoot, projectRoot, projectName
|
|
|
194
196
|
if (scope === 'local') {
|
|
195
197
|
return;
|
|
196
198
|
}
|
|
197
|
-
const pathspecs =
|
|
199
|
+
const pathspecs = files.map((file) => repoRelativePath(repoRoot, join(projectRoot, file)));
|
|
198
200
|
await dependencies.gitRunner.run(['rev-parse', '--is-inside-work-tree'], {
|
|
199
201
|
cwd: repoRoot,
|
|
200
202
|
});
|
|
@@ -243,6 +245,17 @@ async function promoteProject(projectPath, options, context, dependencies) {
|
|
|
243
245
|
catch {
|
|
244
246
|
return { status: 'refused', reason: 'project-unreadable', files: [] };
|
|
245
247
|
}
|
|
248
|
+
if (stateFrontmatter['oat_workflow_mode'] === 'brief') {
|
|
249
|
+
return promoteBriefProject({
|
|
250
|
+
repoRoot,
|
|
251
|
+
projectRoot,
|
|
252
|
+
projectName,
|
|
253
|
+
statePath,
|
|
254
|
+
planPath,
|
|
255
|
+
stateContent,
|
|
256
|
+
planContent,
|
|
257
|
+
}, context, dependencies);
|
|
258
|
+
}
|
|
246
259
|
if (stateFrontmatter['oat_workflow_mode'] !== 'lite') {
|
|
247
260
|
return { status: 'refused', reason: 'not-lite', files: [] };
|
|
248
261
|
}
|
|
@@ -332,11 +345,555 @@ async function promoteProject(projectPath, options, context, dependencies) {
|
|
|
332
345
|
files: [...PROMOTED_FILES],
|
|
333
346
|
};
|
|
334
347
|
}
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// brief → quick
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
const BRIEF_ARCHIVE_FILE = 'references/brief-archive.md';
|
|
352
|
+
const SYNTHESIZED_TASK_TITLE = 'Accepted via phase evidence';
|
|
353
|
+
/** Brief sections rendered into `discovery.md` and dropped from the quick plan. */
|
|
354
|
+
const BRIEF_ONLY_SECTIONS = [
|
|
355
|
+
'goal',
|
|
356
|
+
'outcomes',
|
|
357
|
+
'approach',
|
|
358
|
+
'constraints and out of scope',
|
|
359
|
+
'constraints',
|
|
360
|
+
'out of scope',
|
|
361
|
+
'decisions',
|
|
362
|
+
'known unknowns',
|
|
363
|
+
'delivery endpoint and authority',
|
|
364
|
+
'delivery authority',
|
|
365
|
+
'milestones',
|
|
366
|
+
'phases',
|
|
367
|
+
];
|
|
368
|
+
const TASK_RECORD_HEADING = /^### Task ((p-?rev\d+|p\d+)-t(\d+)): (.+)$/;
|
|
369
|
+
const RECORD_STATUS_LINE = /^\*\*Status:\*\*\s+(\S+)$/;
|
|
370
|
+
const RECORD_COMMIT_LINE = /^\*\*Commit:\*\*\s+(\S+)$/;
|
|
371
|
+
const RECORD_MARKER_LINE = /^\*\*(Status|Commit|Executor):\*\*/;
|
|
372
|
+
const SECTION_HEADING_LINE = /^#{1,2}[ \t]/;
|
|
373
|
+
const HEADING_LINE = /^#/;
|
|
374
|
+
function stripFrontmatter(content) {
|
|
375
|
+
const match = /^---\n[\s\S]*?\n---[^\n]*(?:\n|$)/.exec(content);
|
|
376
|
+
return match ? content.slice(match[0].length) : content;
|
|
377
|
+
}
|
|
378
|
+
function normalizeOrdinal(digits, width) {
|
|
379
|
+
return digits.replace(/^0+(?=\d)/, '').padStart(width, '0');
|
|
380
|
+
}
|
|
381
|
+
function taskPhaseId(prefix) {
|
|
382
|
+
const revision = /^p-?rev(\d+)$/.exec(prefix);
|
|
383
|
+
if (revision?.[1])
|
|
384
|
+
return `p-rev${normalizeOrdinal(revision[1], 1)}`;
|
|
385
|
+
return `p${normalizeOrdinal(prefix.slice(1), 2)}`;
|
|
386
|
+
}
|
|
387
|
+
function taskPrefixForPhase(phaseId) {
|
|
388
|
+
return phaseId.startsWith('p-rev') ? `prev${phaseId.slice(5)}` : phaseId;
|
|
389
|
+
}
|
|
390
|
+
/** Split markdown into `## ` sections, ignoring headings inside code. */
|
|
391
|
+
function splitSections(body) {
|
|
392
|
+
const lines = body.split('\n');
|
|
393
|
+
const mask = recordLineMask(lines);
|
|
394
|
+
const sections = [{ heading: null, lines: [] }];
|
|
395
|
+
for (const [index, line] of lines.entries()) {
|
|
396
|
+
if (mask[index] && /^##[ \t]/.test(line)) {
|
|
397
|
+
sections.push({ heading: line, lines: [line] });
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
sections[sections.length - 1].lines.push(line);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return sections;
|
|
404
|
+
}
|
|
405
|
+
function sectionTitle(heading) {
|
|
406
|
+
return heading.replace(/^##[ \t]+/, '').trim();
|
|
407
|
+
}
|
|
408
|
+
function sectionBody(section) {
|
|
409
|
+
return section.lines.slice(1).join('\n').trim();
|
|
410
|
+
}
|
|
411
|
+
function findBriefSection(sections, matches) {
|
|
412
|
+
const section = sections.find((candidate) => candidate.heading != null &&
|
|
413
|
+
matches(sectionTitle(candidate.heading).toLowerCase()));
|
|
414
|
+
const body = section ? sectionBody(section) : '';
|
|
415
|
+
return body.length > 0 ? body : null;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Execution records per journal phase section: `### Task` headings whose
|
|
419
|
+
* normalized phase id matches the enclosing `## Phase` section. Also returns
|
|
420
|
+
* each phase section's end line (exclusive) for appending records.
|
|
421
|
+
*/
|
|
422
|
+
function readJournalRecords(lines) {
|
|
423
|
+
const mask = recordLineMask(lines);
|
|
424
|
+
const records = new Map();
|
|
425
|
+
const sectionEnds = new Map();
|
|
426
|
+
let phaseId = null;
|
|
427
|
+
let current = null;
|
|
428
|
+
const closeSection = (end) => {
|
|
429
|
+
if (phaseId != null)
|
|
430
|
+
sectionEnds.set(phaseId, end);
|
|
431
|
+
};
|
|
432
|
+
for (const [index, line] of lines.entries()) {
|
|
433
|
+
if (!mask[index]) {
|
|
434
|
+
current?.body.push(line);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (SECTION_HEADING_LINE.test(line)) {
|
|
438
|
+
closeSection(index);
|
|
439
|
+
current = null;
|
|
440
|
+
phaseId = parsePhaseHeadingLine(line)?.phaseId ?? null;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const task = TASK_RECORD_HEADING.exec(line);
|
|
444
|
+
if (task && phaseId != null) {
|
|
445
|
+
current = null;
|
|
446
|
+
if (taskPhaseId(task[2]) === phaseId) {
|
|
447
|
+
current = {
|
|
448
|
+
taskId: task[1],
|
|
449
|
+
ordinal: Number.parseInt(task[3], 10),
|
|
450
|
+
title: task[4],
|
|
451
|
+
status: null,
|
|
452
|
+
commit: null,
|
|
453
|
+
body: [],
|
|
454
|
+
};
|
|
455
|
+
const list = records.get(phaseId) ?? [];
|
|
456
|
+
list.push(current);
|
|
457
|
+
records.set(phaseId, list);
|
|
458
|
+
}
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (HEADING_LINE.test(line)) {
|
|
462
|
+
current = null;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (current == null)
|
|
466
|
+
continue;
|
|
467
|
+
const trimmed = line.trim();
|
|
468
|
+
const status = RECORD_STATUS_LINE.exec(trimmed)?.[1];
|
|
469
|
+
if (status !== undefined) {
|
|
470
|
+
current.status ??= status;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
const commit = RECORD_COMMIT_LINE.exec(trimmed)?.[1];
|
|
474
|
+
if (commit !== undefined) {
|
|
475
|
+
current.commit ??= commit;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (!RECORD_MARKER_LINE.test(trimmed))
|
|
479
|
+
current.body.push(line);
|
|
480
|
+
}
|
|
481
|
+
closeSection(lines.length);
|
|
482
|
+
return { records, sectionEnds };
|
|
483
|
+
}
|
|
484
|
+
/** Plan phase block lines owned by the phase heading (up to the first `#`). */
|
|
485
|
+
function ownedPhaseLines(section) {
|
|
486
|
+
const owned = [];
|
|
487
|
+
const mask = recordLineMask(section.lines);
|
|
488
|
+
for (const [index, line] of section.lines.slice(1).entries()) {
|
|
489
|
+
if (mask[index + 1] && HEADING_LINE.test(line))
|
|
490
|
+
break;
|
|
491
|
+
owned.push(line);
|
|
492
|
+
}
|
|
493
|
+
while (owned.length > 0 && owned[owned.length - 1].trim() === '') {
|
|
494
|
+
owned.pop();
|
|
495
|
+
}
|
|
496
|
+
return owned;
|
|
497
|
+
}
|
|
498
|
+
function trimBlank(lines) {
|
|
499
|
+
let start = 0;
|
|
500
|
+
let end = lines.length;
|
|
501
|
+
while (start < end && lines[start].trim() === '')
|
|
502
|
+
start += 1;
|
|
503
|
+
while (end > start && lines[end - 1].trim() === '')
|
|
504
|
+
end -= 1;
|
|
505
|
+
return lines.slice(start, end);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* The brief's `## Reviews` section with every row left byte-identical (Scope,
|
|
509
|
+
* Type and Artifact are review-event identity) and one provenance line under
|
|
510
|
+
* the heading marking the rows recorded before promotion.
|
|
511
|
+
*/
|
|
512
|
+
function reviewsWithProvenance(section, today) {
|
|
513
|
+
return [
|
|
514
|
+
section.lines[0],
|
|
515
|
+
'',
|
|
516
|
+
`Rows dated on or before ${today} were recorded pre-promotion under workflow mode brief.`,
|
|
517
|
+
'',
|
|
518
|
+
...trimBlank(section.lines.slice(1)),
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
function renderPlanTask(record) {
|
|
522
|
+
const commit = record.commit && record.commit !== '-' ? record.commit : null;
|
|
523
|
+
return [
|
|
524
|
+
`### Task ${record.taskId}: ${record.title}`,
|
|
525
|
+
'',
|
|
526
|
+
`> Accepted before promotion (brief). Execution record: \`implementation.md\`${commit ? ` (commit \`${commit}\`)` : ''}.`,
|
|
527
|
+
...(trimBlank(record.body).length > 0
|
|
528
|
+
? ['', ...trimBlank(record.body)]
|
|
529
|
+
: []),
|
|
530
|
+
'',
|
|
531
|
+
];
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Pure brief → quick migration. Phase states follow the phase-unit progress
|
|
535
|
+
* contract: accepted (journal `completed`, not provisional), active (any other
|
|
536
|
+
* started status), pending, or provisional. An extracted phase refuses.
|
|
537
|
+
*/
|
|
538
|
+
export function migrateBriefToQuick(input) {
|
|
539
|
+
const { planContent, implementationContent, projectName, today } = input;
|
|
540
|
+
const markers = parsePhaseMarkers(implementationContent);
|
|
541
|
+
if ([...markers.values()].some((marker) => marker.status === 'extracted')) {
|
|
542
|
+
return { ok: false, reason: 'extracted-phases' };
|
|
543
|
+
}
|
|
544
|
+
const progress = parseTaskProgress(planContent, implementationContent, {
|
|
545
|
+
phaseUnits: true,
|
|
546
|
+
});
|
|
547
|
+
if (progress.phases.length === 0) {
|
|
548
|
+
return { ok: false, reason: 'invalid-brief-plan' };
|
|
549
|
+
}
|
|
550
|
+
const phaseStates = {};
|
|
551
|
+
for (const phase of progress.phases) {
|
|
552
|
+
const status = markers.get(phase.phaseId)?.status ?? 'pending';
|
|
553
|
+
phaseStates[phase.phaseId] = phase.provisional
|
|
554
|
+
? 'provisional'
|
|
555
|
+
: phase.completed === 1
|
|
556
|
+
? 'accepted'
|
|
557
|
+
: status === 'pending'
|
|
558
|
+
? 'pending'
|
|
559
|
+
: 'active';
|
|
560
|
+
}
|
|
561
|
+
const pendingPhases = progress.phases
|
|
562
|
+
.map((phase) => phase.phaseId)
|
|
563
|
+
.filter((phaseId, index, all) => phaseStates[phaseId] !== 'accepted' && all.indexOf(phaseId) === index);
|
|
564
|
+
const journalLines = implementationContent.split('\n');
|
|
565
|
+
const { records, sectionEnds } = readJournalRecords(journalLines);
|
|
566
|
+
const insertions = [];
|
|
567
|
+
// An accepted phase without a completed record is synthesized against its
|
|
568
|
+
// phase head; without a head there is no evidence anchor, so refuse rather
|
|
569
|
+
// than fabricate a completed record.
|
|
570
|
+
const headless = progress.phases
|
|
571
|
+
.map((phase) => phase.phaseId)
|
|
572
|
+
.filter((phaseId, index, all) => all.indexOf(phaseId) === index &&
|
|
573
|
+
phaseStates[phaseId] === 'accepted' &&
|
|
574
|
+
!(records.get(phaseId) ?? []).some((record) => record.status === 'completed') &&
|
|
575
|
+
markers.get(phaseId)?.head == null);
|
|
576
|
+
if (headless.length > 0) {
|
|
577
|
+
return { ok: false, reason: 'missing-phase-head', phases: headless };
|
|
578
|
+
}
|
|
579
|
+
const frontmatter = getFrontmatterBlock(planContent)
|
|
580
|
+
? readObjectFrontmatter(planContent, 'plan.md')
|
|
581
|
+
: {};
|
|
582
|
+
const sections = splitSections(stripFrontmatter(planContent));
|
|
583
|
+
const goal = findBriefSection(sections, (title) => title === 'goal');
|
|
584
|
+
const output = [
|
|
585
|
+
`# Implementation Plan: ${projectName}`,
|
|
586
|
+
'',
|
|
587
|
+
`> Promoted from a brief on ${today}. The original brief is archived at \`${BRIEF_ARCHIVE_FILE}\`; its outcomes, decisions, known unknowns and delivery authority are carried into \`discovery.md\`.`,
|
|
588
|
+
'',
|
|
589
|
+
`**Goal:** ${goal ? goal.split('\n\n')[0].replace(/\n/g, ' ') : '{Author from discovery}'}`,
|
|
590
|
+
'',
|
|
591
|
+
'**Commit Convention:** `{type}({scope}): {description}` - e.g., `feat(p01-t01): add user auth endpoint`',
|
|
592
|
+
'',
|
|
593
|
+
];
|
|
594
|
+
let referencesSeen = false;
|
|
595
|
+
const seenPhases = new Set();
|
|
596
|
+
for (const section of sections) {
|
|
597
|
+
if (section.heading == null)
|
|
598
|
+
continue;
|
|
599
|
+
const title = sectionTitle(section.heading);
|
|
600
|
+
const phaseHeading = parsePhaseHeadingLine(section.heading);
|
|
601
|
+
if (phaseHeading) {
|
|
602
|
+
const { phaseId, name } = phaseHeading;
|
|
603
|
+
if (seenPhases.has(phaseId))
|
|
604
|
+
continue;
|
|
605
|
+
seenPhases.add(phaseId);
|
|
606
|
+
const state = phaseStates[phaseId] ?? 'pending';
|
|
607
|
+
const phaseRecords = records.get(phaseId) ?? [];
|
|
608
|
+
const lines = [`## Phase ${phaseId}: ${name}`, ''];
|
|
609
|
+
const owned = trimBlank(ownedPhaseLines(section));
|
|
610
|
+
if (owned.length > 0)
|
|
611
|
+
lines.push(...owned, '');
|
|
612
|
+
if (state === 'accepted' || state === 'active') {
|
|
613
|
+
const completed = phaseRecords.filter((r) => r.status === 'completed');
|
|
614
|
+
const unfinished = state === 'active'
|
|
615
|
+
? phaseRecords.filter((r) => r.status === 'pending' || r.status === 'in_progress')
|
|
616
|
+
: [];
|
|
617
|
+
const historical = phaseRecords.filter((r) => r.status !== 'completed' && !unfinished.includes(r));
|
|
618
|
+
if (historical.length > 0) {
|
|
619
|
+
lines.push(`**Historical records:** ${historical
|
|
620
|
+
.map((r) => `${r.taskId} (${r.status ?? 'unknown'})`)
|
|
621
|
+
.join(', ')}`, '');
|
|
622
|
+
}
|
|
623
|
+
if (unfinished.length > 0) {
|
|
624
|
+
lines.push(`**Unfinished records:** ${unfinished
|
|
625
|
+
.map((r) => `${r.taskId} (${r.status})`)
|
|
626
|
+
.join(', ')}`, '');
|
|
627
|
+
}
|
|
628
|
+
for (const record of completed)
|
|
629
|
+
lines.push(...renderPlanTask(record));
|
|
630
|
+
if (state === 'accepted' && completed.length === 0) {
|
|
631
|
+
const next = Math.max(0, ...phaseRecords.map((r) => r.ordinal)) + 1;
|
|
632
|
+
const taskId = `${taskPrefixForPhase(phaseId)}-t${String(next).padStart(2, '0')}`;
|
|
633
|
+
const head = markers.get(phaseId).head;
|
|
634
|
+
lines.push(`### Task ${taskId}: ${SYNTHESIZED_TASK_TITLE}`, '', `> Synthesized at promotion: the phase was accepted on its exit evidence (head \`${head}\`) without a completed execution record.`, '');
|
|
635
|
+
const end = sectionEnds.get(phaseId) ?? journalLines.length;
|
|
636
|
+
insertions.push({
|
|
637
|
+
at: end,
|
|
638
|
+
lines: [
|
|
639
|
+
`### Task ${taskId}: ${SYNTHESIZED_TASK_TITLE}`,
|
|
640
|
+
'',
|
|
641
|
+
'**Status:** completed',
|
|
642
|
+
`**Commit:** ${head}`,
|
|
643
|
+
'**Executor:** reconciled',
|
|
644
|
+
'',
|
|
645
|
+
`Synthesized by \`oat project promote --to quick\` on ${today}: the phase was accepted on its exit evidence without a completed execution record.`,
|
|
646
|
+
'',
|
|
647
|
+
],
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
output.push(...lines);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (BRIEF_ONLY_SECTIONS.includes(title.toLowerCase()))
|
|
655
|
+
continue;
|
|
656
|
+
if (/^delivery\b/i.test(title))
|
|
657
|
+
continue;
|
|
658
|
+
if (title.toLowerCase() === 'reviews') {
|
|
659
|
+
output.push(...reviewsWithProvenance(section, today), '');
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (title.toLowerCase() === 'references') {
|
|
663
|
+
referencesSeen = true;
|
|
664
|
+
output.push(...trimBlank(section.lines), `- Brief archive: \`${BRIEF_ARCHIVE_FILE}\``, '- Discovery: `discovery.md`', '');
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
output.push(...trimBlank(section.lines), '');
|
|
668
|
+
}
|
|
669
|
+
if (!referencesSeen) {
|
|
670
|
+
output.push('## References', '', `- Brief archive: \`${BRIEF_ARCHIVE_FILE}\``, '- Discovery: `discovery.md`', '');
|
|
671
|
+
}
|
|
672
|
+
frontmatter['oat_status'] = 'in_progress';
|
|
673
|
+
frontmatter['oat_ready_for'] = null;
|
|
674
|
+
frontmatter['oat_last_updated'] = today;
|
|
675
|
+
frontmatter['oat_template'] = true;
|
|
676
|
+
frontmatter['oat_template_name'] = 'plan';
|
|
677
|
+
frontmatter['oat_plan_source'] = 'quick';
|
|
678
|
+
frontmatter['oat_promoted_from'] = 'brief';
|
|
679
|
+
frontmatter['oat_promotion_pending_phases'] = pendingPhases;
|
|
680
|
+
const plan = `---\n${YAML.stringify(frontmatter)}---\n\n${output
|
|
681
|
+
.join('\n')
|
|
682
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
683
|
+
.trimEnd()}\n`;
|
|
684
|
+
const nextJournal = [...journalLines];
|
|
685
|
+
for (const insertion of [...insertions].sort((a, b) => b.at - a.at)) {
|
|
686
|
+
const before = nextJournal.slice(0, insertion.at);
|
|
687
|
+
while (before.length > 0 && before[before.length - 1].trim() === '') {
|
|
688
|
+
before.pop();
|
|
689
|
+
}
|
|
690
|
+
const after = nextJournal.slice(insertion.at);
|
|
691
|
+
nextJournal.splice(0, nextJournal.length, ...before, '', ...insertion.lines, ...after);
|
|
692
|
+
}
|
|
693
|
+
const implementation = nextJournal.join('\n');
|
|
694
|
+
return {
|
|
695
|
+
ok: true,
|
|
696
|
+
migration: {
|
|
697
|
+
plan,
|
|
698
|
+
implementation,
|
|
699
|
+
implementationChanged: insertions.length > 0,
|
|
700
|
+
pendingPhases,
|
|
701
|
+
phaseStates,
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
function upsertDiscoverySection(content, heading, body) {
|
|
706
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
707
|
+
if (new RegExp(`^## ${escapedHeading}[ \\t]*$`, 'm').test(content)) {
|
|
708
|
+
return replaceSection(content, heading, body);
|
|
709
|
+
}
|
|
710
|
+
const section = `## ${heading}\n\n${body.trim()}\n\n`;
|
|
711
|
+
const nextSteps = /^## Next Steps[ \t]*$/m.exec(content);
|
|
712
|
+
return nextSteps
|
|
713
|
+
? `${content.slice(0, nextSteps.index)}${section}${content.slice(nextSteps.index)}`
|
|
714
|
+
: `${content.trimEnd()}\n\n${section}`;
|
|
715
|
+
}
|
|
716
|
+
function renderBriefDiscovery(template, projectName, today, nowUtc, briefPlan) {
|
|
717
|
+
let rendered = applyTemplateReplacements(template, projectName, today, nowUtc, 'quick');
|
|
718
|
+
const frontmatter = readObjectFrontmatter(rendered, 'rendered discovery.md');
|
|
719
|
+
frontmatter['oat_ready_for'] = 'oat-project-quick-start';
|
|
720
|
+
rendered = replaceFrontmatter(rendered, YAML.stringify(frontmatter).trimEnd());
|
|
721
|
+
const sections = splitSections(stripFrontmatter(briefPlan));
|
|
722
|
+
const carried = [
|
|
723
|
+
['Initial Request', findBriefSection(sections, (t) => t === 'goal')],
|
|
724
|
+
['Success Criteria', findBriefSection(sections, (t) => t === 'outcomes')],
|
|
725
|
+
['Key Decisions', findBriefSection(sections, (t) => t === 'decisions')],
|
|
726
|
+
[
|
|
727
|
+
'Constraints',
|
|
728
|
+
findBriefSection(sections, (t) => t.startsWith('constraints')),
|
|
729
|
+
],
|
|
730
|
+
[
|
|
731
|
+
'Open Questions',
|
|
732
|
+
findBriefSection(sections, (t) => t === 'known unknowns'),
|
|
733
|
+
],
|
|
734
|
+
[
|
|
735
|
+
'Delivery Endpoint and Authority (from brief)',
|
|
736
|
+
findBriefSection(sections, (t) => t.startsWith('delivery')),
|
|
737
|
+
],
|
|
738
|
+
];
|
|
739
|
+
for (const [heading, body] of carried) {
|
|
740
|
+
if (body)
|
|
741
|
+
rendered = upsertDiscoverySection(rendered, heading, body);
|
|
742
|
+
}
|
|
743
|
+
const approach = findBriefSection(sections, (t) => t === 'approach');
|
|
744
|
+
if (approach)
|
|
745
|
+
rendered = renderChosenDirection(rendered, approach);
|
|
746
|
+
return rendered;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Carry the brief's `## Approach` into discovery's `## Solution Space` as the
|
|
750
|
+
* `### Chosen Direction` body. Headings inside the approach (for example
|
|
751
|
+
* `### Design`) are demoted one level so they stay under Chosen Direction;
|
|
752
|
+
* lines inside fenced or indented code are left byte-for-byte.
|
|
753
|
+
*/
|
|
754
|
+
function renderChosenDirection(content, approach) {
|
|
755
|
+
const approachLines = approach.split('\n');
|
|
756
|
+
const mask = recordLineMask(approachLines);
|
|
757
|
+
const body = approachLines
|
|
758
|
+
.map((line, index) => mask[index] ? line.replace(/^(#{3,5})(?=[ \t])/, '#$1') : line)
|
|
759
|
+
.join('\n')
|
|
760
|
+
.trim();
|
|
761
|
+
const chosen = /^### Chosen Direction[ \t]*$/m.exec(content);
|
|
762
|
+
if (chosen) {
|
|
763
|
+
const bodyStart = chosen.index + chosen[0].length;
|
|
764
|
+
const next = /^#{1,3}[ \t]/m.exec(content.slice(bodyStart));
|
|
765
|
+
const bodyEnd = next ? bodyStart + next.index : content.length;
|
|
766
|
+
return `${content.slice(0, bodyStart)}\n\n${body}\n\n${content.slice(bodyEnd).replace(/^\s*/, '')}`;
|
|
767
|
+
}
|
|
768
|
+
const solutionSpace = /^## Solution Space[ \t]*$/m.exec(content);
|
|
769
|
+
if (solutionSpace) {
|
|
770
|
+
const bodyStart = solutionSpace.index + solutionSpace[0].length;
|
|
771
|
+
const next = /^##[ \t]/m.exec(content.slice(bodyStart));
|
|
772
|
+
const at = next ? bodyStart + next.index : content.length;
|
|
773
|
+
return `${content.slice(0, at).trimEnd()}\n\n### Chosen Direction\n\n${body}\n\n${content.slice(at)}`;
|
|
774
|
+
}
|
|
775
|
+
return upsertDiscoverySection(content, 'Solution Space', `### Chosen Direction\n\n${body}`);
|
|
776
|
+
}
|
|
777
|
+
function renderPromotedBriefState(stateContent, statePath, nowUtc) {
|
|
778
|
+
const frontmatter = readObjectFrontmatter(stateContent, statePath);
|
|
779
|
+
frontmatter['oat_workflow_mode'] = 'quick';
|
|
780
|
+
frontmatter['oat_phase'] = 'plan';
|
|
781
|
+
frontmatter['oat_phase_status'] = 'in_progress';
|
|
782
|
+
frontmatter['oat_ready_for'] = 'oat-project-quick-start';
|
|
783
|
+
frontmatter['oat_project_state_updated'] = nowUtc;
|
|
784
|
+
return replaceFrontmatter(stateContent, YAML.stringify(frontmatter).trimEnd());
|
|
785
|
+
}
|
|
786
|
+
async function promoteBriefProject(input, context, dependencies) {
|
|
787
|
+
const { repoRoot, projectRoot, projectName, statePath, planPath } = input;
|
|
788
|
+
const archivePath = join(projectRoot, BRIEF_ARCHIVE_FILE);
|
|
789
|
+
const implementationPath = join(projectRoot, 'implementation.md');
|
|
790
|
+
if (await dependencies.fileExists(archivePath)) {
|
|
791
|
+
return { status: 'refused', reason: 'brief-archive-exists', files: [] };
|
|
792
|
+
}
|
|
793
|
+
let implementationContent = '';
|
|
794
|
+
if (await dependencies.fileExists(implementationPath)) {
|
|
795
|
+
try {
|
|
796
|
+
implementationContent = await dependencies.readFile(implementationPath, 'utf8');
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
return { status: 'refused', reason: 'project-unreadable', files: [] };
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const now = dependencies.now();
|
|
803
|
+
const nowUtc = now.toISOString();
|
|
804
|
+
const today = nowUtc.slice(0, 10);
|
|
805
|
+
let migrated;
|
|
806
|
+
try {
|
|
807
|
+
migrated = migrateBriefToQuick({
|
|
808
|
+
planContent: input.planContent,
|
|
809
|
+
implementationContent,
|
|
810
|
+
projectName,
|
|
811
|
+
today,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
return { status: 'refused', reason: 'invalid-brief-plan', files: [] };
|
|
816
|
+
}
|
|
817
|
+
if (!migrated.ok) {
|
|
818
|
+
return {
|
|
819
|
+
status: 'refused',
|
|
820
|
+
reason: migrated.reason,
|
|
821
|
+
files: [],
|
|
822
|
+
...(migrated.reason === 'missing-phase-head'
|
|
823
|
+
? { phases: migrated.phases }
|
|
824
|
+
: {}),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
const { migration } = migrated;
|
|
828
|
+
const projectsRoot = await dependencies.resolveProjectsRoot(repoRoot, dependencies.processEnv);
|
|
829
|
+
const scope = resolveProjectScope(projectRoot, projectsRoot, repoRoot);
|
|
830
|
+
if (!scope) {
|
|
831
|
+
return { status: 'refused', reason: 'scope-unresolved', files: [] };
|
|
832
|
+
}
|
|
833
|
+
let discoveryContent;
|
|
834
|
+
let quickStateContent;
|
|
835
|
+
try {
|
|
836
|
+
const discoveryTemplate = (await dependencies.resolveTemplate({
|
|
837
|
+
name: 'discovery.md',
|
|
838
|
+
templatesRoot: join(repoRoot, '.oat', 'templates'),
|
|
839
|
+
home: context.home,
|
|
840
|
+
assetsRoot: () => resolveAssetsRoot(),
|
|
841
|
+
})).content;
|
|
842
|
+
discoveryContent = renderBriefDiscovery(discoveryTemplate, projectName, today, nowUtc, input.planContent);
|
|
843
|
+
quickStateContent = renderPromotedBriefState(input.stateContent, statePath, nowUtc);
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
return { status: 'refused', reason: 'template-unreadable', files: [] };
|
|
847
|
+
}
|
|
848
|
+
const files = [
|
|
849
|
+
'discovery.md',
|
|
850
|
+
BRIEF_ARCHIVE_FILE,
|
|
851
|
+
...(migration.implementationChanged ? ['implementation.md'] : []),
|
|
852
|
+
'plan.md',
|
|
853
|
+
'state.md',
|
|
854
|
+
];
|
|
855
|
+
const identity = `promote:${projectRoot}:brief-to-quick:${randomUUID()}`;
|
|
856
|
+
const writtenFiles = [];
|
|
857
|
+
try {
|
|
858
|
+
await dependencies.mkdir(join(projectRoot, 'references'), {
|
|
859
|
+
recursive: true,
|
|
860
|
+
});
|
|
861
|
+
await dependencies.writeFile(join(projectRoot, 'discovery.md'), discoveryContent, 'utf8');
|
|
862
|
+
writtenFiles.push('discovery.md');
|
|
863
|
+
await dependencies.writeFile(archivePath, input.planContent, 'utf8');
|
|
864
|
+
writtenFiles.push(BRIEF_ARCHIVE_FILE);
|
|
865
|
+
if (migration.implementationChanged) {
|
|
866
|
+
await dependencies.writeFile(implementationPath, migration.implementation, 'utf8');
|
|
867
|
+
writtenFiles.push('implementation.md');
|
|
868
|
+
}
|
|
869
|
+
await dependencies.writeFile(planPath, migration.plan, 'utf8');
|
|
870
|
+
writtenFiles.push('plan.md');
|
|
871
|
+
await dependencies.writeFile(statePath, quickStateContent, 'utf8');
|
|
872
|
+
writtenFiles.push('state.md');
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
return { status: 'refused', reason: 'write-failed', files: writtenFiles };
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
await persistPromotion(repoRoot, projectsRoot, projectRoot, projectName, scope, identity, dependencies, files);
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
return {
|
|
882
|
+
status: 'refused',
|
|
883
|
+
reason: 'persistence-failed',
|
|
884
|
+
files: writtenFiles,
|
|
885
|
+
...(error instanceof PromotionPersistenceError
|
|
886
|
+
? { recovery: error.recovery }
|
|
887
|
+
: {}),
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
return { status: 'promoted', reason: 'promoted', files };
|
|
891
|
+
}
|
|
335
892
|
export function createProjectPromoteCommand(overrides = {}) {
|
|
336
893
|
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
337
894
|
return new Command('promote')
|
|
338
|
-
.description('Promote an authored lite project to the quick workflow')
|
|
339
|
-
.argument('<project-path>', 'Lite project path to promote')
|
|
895
|
+
.description('Promote an authored lite or brief project to the quick workflow')
|
|
896
|
+
.argument('<project-path>', 'Lite or brief project path to promote')
|
|
340
897
|
.requiredOption('--to <mode>', 'Target workflow mode')
|
|
341
898
|
.action(async (projectPath, options, command) => {
|
|
342
899
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
@@ -348,7 +905,7 @@ export function createProjectPromoteCommand(overrides = {}) {
|
|
|
348
905
|
context.logger.info(`Promoted ${projectPath} to quick.`);
|
|
349
906
|
}
|
|
350
907
|
else {
|
|
351
|
-
context.logger.error(`Promotion refused: ${result.reason}.${result.recovery ? ` Exact-path persistence ${result.recovery.outcome}; attempts ${result.recovery.attempts}; lock ${result.recovery.lockClass ?? 'none'}; identity ${result.recovery.identity}; receipt ${result.recovery.receipt ?? 'unavailable'}. ${result.recovery.error ?? ''} Recover existing Quick artifacts with: ${result.recovery.command}` : ''}`);
|
|
908
|
+
context.logger.error(`Promotion refused: ${result.reason}${result.phases ? ` (${result.phases.join(', ')})` : ''}.${result.recovery ? ` Exact-path persistence ${result.recovery.outcome}; attempts ${result.recovery.attempts}; lock ${result.recovery.lockClass ?? 'none'}; identity ${result.recovery.identity}; receipt ${result.recovery.receipt ?? 'unavailable'}. ${result.recovery.error ?? ''} Recover existing Quick artifacts with: ${result.recovery.command}` : ''}`);
|
|
352
909
|
}
|
|
353
910
|
process.exitCode = result.status === 'promoted' ? 0 : 1;
|
|
354
911
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type FrontmatterResult = {
|
|
2
|
+
kind: 'ok';
|
|
3
|
+
data: Record<string, unknown>;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'no-frontmatter';
|
|
6
|
+
data: Record<string, unknown>;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'invalid';
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function parseFrontmatterFromContent(content: string): FrontmatterResult;
|
|
12
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/frontmatter.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzC,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,GACd,iBAAiB,CAyBnB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import YAML from 'yaml';
|
|
2
|
+
const FRONTMATTER_PATTERN = /^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/;
|
|
3
|
+
export function parseFrontmatterFromContent(content) {
|
|
4
|
+
const match = content.match(FRONTMATTER_PATTERN);
|
|
5
|
+
if (!match?.[1]) {
|
|
6
|
+
return { kind: 'no-frontmatter', data: {} };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
const parsed = YAML.parse(match[1]);
|
|
10
|
+
if (parsed !== null &&
|
|
11
|
+
typeof parsed === 'object' &&
|
|
12
|
+
!Array.isArray(parsed)) {
|
|
13
|
+
return { kind: 'ok', data: parsed };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
kind: 'invalid',
|
|
17
|
+
message: 'Frontmatter did not parse as an object',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
22
|
+
return {
|
|
23
|
+
kind: 'invalid',
|
|
24
|
+
message: `Malformed YAML in frontmatter: ${message}`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/validate-plan/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,UAAU,+BAA+B;IACvC,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;CACjE;AAMD,wBAAgB,gCAAgC,CAC9C,SAAS,GAAE,OAAO,CAAC,+BAA+B,CAAM,GACvD,OAAO,CAmIT"}
|