@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.
Files changed (99) hide show
  1. package/assets/agents/oat-phase-implementer.md +81 -3
  2. package/assets/agents/oat-reviewer.md +15 -5
  3. package/assets/bundle-metadata.json +1 -1
  4. package/assets/docs/reference/cli-reference.md +8 -5
  5. package/assets/docs/reference/oat-directory-structure.md +1 -0
  6. package/assets/docs/reference/project-artifacts.md +17 -1
  7. package/assets/docs/reference/project-state-machine.md +19 -4
  8. package/assets/docs/skills/brainstorm.md +2 -1
  9. package/assets/docs/skills/index.md +1 -0
  10. package/assets/docs/workflows/advanced/autonomy.md +9 -1
  11. package/assets/docs/workflows/advanced/workflow-gates.md +11 -6
  12. package/assets/docs/workflows/approvals-and-automation.md +13 -8
  13. package/assets/docs/workflows/choose-workflow.md +16 -8
  14. package/assets/docs/workflows/projects/closeout/closeout-skills.md +8 -3
  15. package/assets/docs/workflows/projects/closeout/pr-flow.md +2 -0
  16. package/assets/docs/workflows/projects/execution/execution-skills.md +14 -2
  17. package/assets/docs/workflows/projects/execution/implementation-execution.md +15 -0
  18. package/assets/docs/workflows/projects/execution/picking-up-projects.md +10 -1
  19. package/assets/docs/workflows/projects/lifecycle.md +73 -9
  20. package/assets/docs/workflows/projects/planning/hill-checkpoints.md +18 -1
  21. package/assets/docs/workflows/projects/planning/planning-skills.md +11 -10
  22. package/assets/docs/workflows/projects/planning/starting-projects.md +92 -20
  23. package/assets/docs/workflows/projects/reviews/index.md +15 -2
  24. package/assets/docs/workflows/projects/reviews/review-flavors.md +5 -3
  25. package/assets/public-package-versions.json +4 -4
  26. package/assets/skills/oat-brainstorm/SKILL.md +33 -9
  27. package/assets/skills/oat-brainstorm/references/destinations.md +9 -4
  28. package/assets/skills/oat-docs/SKILL.md +9 -4
  29. package/assets/skills/oat-pjm-review-backlog/SKILL.md +2 -2
  30. package/assets/skills/oat-project-autonomous/SKILL.md +14 -8
  31. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +17 -2
  32. package/assets/skills/oat-project-brief/SKILL.md +592 -0
  33. package/assets/skills/oat-project-brief/references/brief-contract.md +294 -0
  34. package/assets/skills/oat-project-brief/references/docs/autonomy-contract.md +393 -0
  35. package/assets/skills/oat-project-brief/references/docs/gate-approval-record.md +84 -0
  36. package/assets/skills/oat-project-capture/SKILL.md +2 -2
  37. package/assets/skills/oat-project-complete/SKILL.md +7 -2
  38. package/assets/skills/oat-project-discover/SKILL.md +3 -2
  39. package/assets/skills/oat-project-dispatch-subagents/SKILL.md +9 -1
  40. package/assets/skills/oat-project-document/SKILL.md +11 -1
  41. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +17 -2
  42. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  43. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +40 -0
  44. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +17 -2
  45. package/assets/skills/oat-project-implement/references/docs/gate-approval-record.md +4 -0
  46. package/assets/skills/oat-project-implement/references/phase-execution.md +166 -1
  47. package/assets/skills/oat-project-implement/references/plan-and-resume.md +97 -3
  48. package/assets/skills/oat-project-import-plan/SKILL.md +5 -1
  49. package/assets/skills/oat-project-lite/SKILL.md +4 -1
  50. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +17 -2
  51. package/assets/skills/oat-project-new/SKILL.md +5 -1
  52. package/assets/skills/oat-project-next/SKILL.md +31 -4
  53. package/assets/skills/oat-project-next/references/docs/gate-approval-record.md +4 -0
  54. package/assets/skills/oat-project-plan/SKILL.md +12 -2
  55. package/assets/skills/oat-project-plan-writing/SKILL.md +6 -4
  56. package/assets/skills/oat-project-pr-final/SKILL.md +15 -3
  57. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +17 -2
  58. package/assets/skills/oat-project-pr-progress/SKILL.md +31 -4
  59. package/assets/skills/oat-project-progress/SKILL.md +30 -8
  60. package/assets/skills/oat-project-progress/references/docs/gate-approval-record.md +4 -0
  61. package/assets/skills/oat-project-promote-spec-driven/SKILL.md +2 -2
  62. package/assets/skills/oat-project-quick-start/SKILL.md +60 -1
  63. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +17 -2
  64. package/assets/skills/oat-project-quick-start/references/docs/gate-approval-record.md +4 -0
  65. package/assets/skills/oat-project-reconcile/SKILL.md +65 -3
  66. package/assets/skills/oat-project-review-provide/SKILL.md +331 -12
  67. package/assets/skills/oat-project-review-receive/SKILL.md +86 -1
  68. package/assets/skills/oat-project-revise/SKILL.md +27 -4
  69. package/assets/skills/oat-project-summary/SKILL.md +95 -2
  70. package/assets/skills/oat-wave-execute/SKILL.md +3 -2
  71. package/assets/templates/implementation-brief.md +165 -0
  72. package/assets/templates/plan-brief.md +140 -0
  73. package/assets/templates/plan.md +1 -1
  74. package/assets/templates/state.md +8 -2
  75. package/dist/commands/project/new/scaffold.d.ts +1 -1
  76. package/dist/commands/project/new/scaffold.d.ts.map +1 -1
  77. package/dist/commands/project/new/scaffold.js +35 -7
  78. package/dist/commands/project/promote/promote.d.ts +32 -1
  79. package/dist/commands/project/promote/promote.d.ts.map +1 -1
  80. package/dist/commands/project/promote/promote.js +562 -5
  81. package/dist/commands/project/validate-plan/frontmatter.d.ts +12 -0
  82. package/dist/commands/project/validate-plan/frontmatter.d.ts.map +1 -0
  83. package/dist/commands/project/validate-plan/frontmatter.js +27 -0
  84. package/dist/commands/project/validate-plan/index.d.ts.map +1 -1
  85. package/dist/commands/project/validate-plan/index.js +52 -18
  86. package/dist/commands/project/validate-plan/profiles.d.ts +54 -0
  87. package/dist/commands/project/validate-plan/profiles.d.ts.map +1 -0
  88. package/dist/commands/project/validate-plan/profiles.js +269 -0
  89. package/dist/commands/project/validate-plan/validate-plan.d.ts +3 -19
  90. package/dist/commands/project/validate-plan/validate-plan.d.ts.map +1 -1
  91. package/dist/commands/project/validate-plan/validate-plan.js +5 -82
  92. package/dist/commands/shared/frontmatter.d.ts +1 -1
  93. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  94. package/dist/commands/shared/frontmatter.js +1 -0
  95. package/dist/commands/state/generate.d.ts.map +1 -1
  96. package/dist/commands/state/generate.js +51 -2
  97. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  98. package/dist/commands/tools/shared/pack-manifest.js +3 -0
  99. 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 = PROMOTED_FILES.map((file) => repoRelativePath(repoRoot, join(projectRoot, file)));
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;AAE9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,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,CAsGT"}
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"}