agentxchain 2.14.0 → 2.16.0

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 (39) hide show
  1. package/README.md +9 -3
  2. package/bin/agentxchain.js +20 -2
  3. package/package.json +3 -1
  4. package/scripts/release-downstream-truth.sh +15 -14
  5. package/scripts/release-postflight.sh +21 -5
  6. package/scripts/sync-homebrew.sh +225 -0
  7. package/src/commands/init.js +16 -6
  8. package/src/commands/intake-approve.js +2 -10
  9. package/src/commands/intake-handoff.js +58 -0
  10. package/src/commands/intake-plan.js +2 -11
  11. package/src/commands/intake-record.js +2 -10
  12. package/src/commands/intake-resolve.js +2 -10
  13. package/src/commands/intake-scan.js +2 -10
  14. package/src/commands/intake-start.js +2 -10
  15. package/src/commands/intake-status.js +6 -10
  16. package/src/commands/intake-triage.js +2 -10
  17. package/src/commands/intake-workspace.js +58 -0
  18. package/src/commands/migrate.js +7 -3
  19. package/src/commands/multi.js +58 -2
  20. package/src/commands/run.js +29 -1
  21. package/src/commands/template-set.js +51 -2
  22. package/src/lib/adapter-interface.js +31 -0
  23. package/src/lib/coordinator-acceptance.js +24 -98
  24. package/src/lib/coordinator-barriers.js +116 -0
  25. package/src/lib/coordinator-config.js +124 -0
  26. package/src/lib/coordinator-dispatch.js +10 -1
  27. package/src/lib/coordinator-gates.js +28 -1
  28. package/src/lib/coordinator-recovery.js +133 -68
  29. package/src/lib/coordinator-state.js +74 -0
  30. package/src/lib/cross-repo-context.js +68 -1
  31. package/src/lib/governed-templates.js +60 -0
  32. package/src/lib/intake-handoff.js +58 -0
  33. package/src/lib/intake.js +300 -11
  34. package/src/lib/report.js +759 -27
  35. package/src/lib/workflow-gate-semantics.js +209 -0
  36. package/src/templates/governed/api-service.json +8 -1
  37. package/src/templates/governed/cli-tool.json +8 -1
  38. package/src/templates/governed/library.json +8 -1
  39. package/src/templates/governed/web-app.json +8 -1
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import { handoffIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
+
5
+ export async function intakeHandoffCommand(opts) {
6
+ const root = requireIntakeWorkspaceOrExit(opts);
7
+
8
+ if (!opts.intent) {
9
+ const msg = '--intent <id> is required';
10
+ if (opts.json) {
11
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
12
+ } else {
13
+ console.log(chalk.red(msg));
14
+ }
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!opts.coordinatorRoot) {
19
+ const msg = '--coordinator-root <path> is required';
20
+ if (opts.json) {
21
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
22
+ } else {
23
+ console.log(chalk.red(msg));
24
+ }
25
+ process.exit(1);
26
+ }
27
+
28
+ if (!opts.workstream) {
29
+ const msg = '--workstream <id> is required';
30
+ if (opts.json) {
31
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
32
+ } else {
33
+ console.log(chalk.red(msg));
34
+ }
35
+ process.exit(1);
36
+ }
37
+
38
+ const result = handoffIntent(root, opts.intent, {
39
+ coordinatorRoot: opts.coordinatorRoot,
40
+ workstreamId: opts.workstream,
41
+ });
42
+
43
+ if (opts.json) {
44
+ console.log(JSON.stringify(result, null, 2));
45
+ } else if (result.ok) {
46
+ console.log('');
47
+ console.log(chalk.green(` Handed off intent ${result.intent.intent_id}`));
48
+ console.log(` Workstream: ${result.intent.target_workstream.workstream_id}`);
49
+ console.log(` Super Run: ${result.super_run_id}`);
50
+ console.log(` Handoff: ${result.handoff_path}`);
51
+ console.log(chalk.dim(' Status: planned → executing (coordinator-managed)'));
52
+ console.log('');
53
+ } else {
54
+ console.log(chalk.red(` ${result.error}`));
55
+ }
56
+
57
+ process.exit(result.exitCode);
58
+ }
@@ -1,18 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { planIntent } from '../lib/intake.js';
4
- import { basename } from 'node:path';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
5
4
 
6
5
  export async function intakePlanCommand(opts) {
7
- const root = findProjectRoot(process.cwd());
8
- if (!root) {
9
- if (opts.json) {
10
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
11
- } else {
12
- console.log(chalk.red('agentxchain.json not found'));
13
- }
14
- process.exit(2);
15
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
16
7
 
17
8
  if (!opts.intent) {
18
9
  const msg = '--intent <id> is required';
@@ -1,19 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { findProjectRoot } from '../lib/config.js';
5
4
  import { recordEvent } from '../lib/intake.js';
5
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
6
6
 
7
7
  export async function intakeRecordCommand(opts) {
8
- const root = findProjectRoot(process.cwd());
9
- if (!root) {
10
- if (opts.json) {
11
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
12
- } else {
13
- console.log(chalk.red('agentxchain.json not found'));
14
- }
15
- process.exit(2);
16
- }
8
+ const root = requireIntakeWorkspaceOrExit(opts);
17
9
 
18
10
  let payload;
19
11
  try {
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { resolveIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeResolveCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  if (!opts.intent) {
17
9
  const msg = '--intent <id> is required';
@@ -1,19 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
- import { findProjectRoot } from '../lib/config.js';
5
4
  import { scanSource, SCAN_SOURCES } from '../lib/intake.js';
5
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
6
6
 
7
7
  export async function intakeScanCommand(opts) {
8
- const root = findProjectRoot(process.cwd());
9
- if (!root) {
10
- if (opts.json) {
11
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
12
- } else {
13
- console.log(chalk.red('agentxchain.json not found'));
14
- }
15
- process.exit(2);
16
- }
8
+ const root = requireIntakeWorkspaceOrExit(opts);
17
9
 
18
10
  if (!opts.source) {
19
11
  const msg = `--source is required. Supported scan sources: ${SCAN_SOURCES.join(', ')}`;
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { startIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeStartCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  if (!opts.intent) {
17
9
  const msg = '--intent <id> is required';
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { intakeStatus } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeStatusCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  const result = intakeStatus(root, opts.intent || null);
17
9
 
@@ -72,6 +64,10 @@ function printIntentDetail(intent, event) {
72
64
  console.log(` ${chalk.dim('Charter:')} ${intent.charter || '—'}`);
73
65
  console.log(` ${chalk.dim('Created:')} ${intent.created_at}`);
74
66
  console.log(` ${chalk.dim('Updated:')} ${intent.updated_at}`);
67
+ if (intent.target_workstream) {
68
+ console.log(` ${chalk.dim('Workstream:')} ${intent.target_workstream.workstream_id}`);
69
+ console.log(` ${chalk.dim('Super Run:')} ${intent.target_workstream.super_run_id}`);
70
+ }
75
71
 
76
72
  if (intent.acceptance_contract && intent.acceptance_contract.length > 0) {
77
73
  console.log('');
@@ -1,17 +1,9 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
3
2
  import { triageIntent } from '../lib/intake.js';
3
+ import { requireIntakeWorkspaceOrExit } from './intake-workspace.js';
4
4
 
5
5
  export async function intakeTriageCommand(opts) {
6
- const root = findProjectRoot(process.cwd());
7
- if (!root) {
8
- if (opts.json) {
9
- console.log(JSON.stringify({ ok: false, error: 'agentxchain.json not found' }, null, 2));
10
- } else {
11
- console.log(chalk.red('agentxchain.json not found'));
12
- }
13
- process.exit(2);
14
- }
6
+ const root = requireIntakeWorkspaceOrExit(opts);
15
7
 
16
8
  if (!opts.intent) {
17
9
  const msg = '--intent <id> is required';
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join, parse as pathParse, resolve } from 'node:path';
4
+ import { findProjectRoot } from '../lib/config.js';
5
+ import { COORDINATOR_CONFIG_FILE } from '../lib/coordinator-config.js';
6
+
7
+ function findCoordinatorWorkspaceRoot(startDir = process.cwd()) {
8
+ let dir = resolve(startDir);
9
+ const { root: fsRoot } = pathParse(dir);
10
+
11
+ while (true) {
12
+ if (existsSync(join(dir, COORDINATOR_CONFIG_FILE))) {
13
+ return dir;
14
+ }
15
+ if (dir === fsRoot) {
16
+ return null;
17
+ }
18
+ dir = join(dir, '..');
19
+ }
20
+ }
21
+
22
+ function listCoordinatorChildRepos(coordinatorRoot) {
23
+ const configPath = join(coordinatorRoot, COORDINATOR_CONFIG_FILE);
24
+ if (!existsSync(configPath)) {
25
+ return [];
26
+ }
27
+
28
+ try {
29
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
30
+ return Object.keys(raw?.repos || {});
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+
36
+ export function requireIntakeWorkspaceOrExit(opts, startDir = process.cwd()) {
37
+ const projectRoot = findProjectRoot(startDir);
38
+ if (projectRoot) {
39
+ return projectRoot;
40
+ }
41
+
42
+ const coordinatorRoot = findCoordinatorWorkspaceRoot(startDir);
43
+ const childRepos = coordinatorRoot ? listCoordinatorChildRepos(coordinatorRoot) : [];
44
+ const repoHint = childRepos.length > 0
45
+ ? ` Available child repos: ${childRepos.join(', ')}.`
46
+ : '';
47
+ const error = coordinatorRoot
48
+ ? `intake commands are repo-local only. Found coordinator workspace at ${coordinatorRoot} (${COORDINATOR_CONFIG_FILE}). Run intake inside a child governed repo (agentxchain.json).${repoHint} Then use \`agentxchain multi step\` for cross-repo coordination.`
49
+ : 'agentxchain.json not found';
50
+
51
+ if (opts.json) {
52
+ console.log(JSON.stringify({ ok: false, error }, null, 2));
53
+ } else {
54
+ console.log(chalk.red(error));
55
+ }
56
+
57
+ process.exit(2);
58
+ }
@@ -165,14 +165,15 @@ export async function migrateCommand(opts) {
165
165
  routing,
166
166
  gates: {
167
167
  planning_signoff: {
168
- requires_files: ['.planning/PM_SIGNOFF.md', '.planning/ROADMAP.md'],
168
+ requires_files: ['.planning/PM_SIGNOFF.md', '.planning/ROADMAP.md', '.planning/SYSTEM_SPEC.md'],
169
169
  requires_human_approval: true
170
170
  },
171
171
  implementation_complete: {
172
+ requires_files: ['.planning/IMPLEMENTATION_NOTES.md'],
172
173
  requires_verification_pass: true
173
174
  },
174
175
  qa_ship_verdict: {
175
- requires_files: ['.planning/acceptance-matrix.md', '.planning/ship-verdict.md'],
176
+ requires_files: ['.planning/acceptance-matrix.md', '.planning/ship-verdict.md', '.planning/RELEASE_NOTES.md'],
176
177
  requires_human_approval: true
177
178
  }
178
179
  },
@@ -311,8 +312,11 @@ ${report.requires_human_review.map((r, i) => `${i + 1}. ${r}`).join('\n')}
311
312
  const planningFiles = {
312
313
  'PM_SIGNOFF.md': `# PM Signoff — ${projectName}\n\nApproved: NO\n`,
313
314
  'ROADMAP.md': `# Roadmap — ${projectName}\n\n(Migrated from v3. Review and update.)\n`,
315
+ 'SYSTEM_SPEC.md': `# System Spec — ${projectName}\n\n## Purpose\n\n(Describe the migrated subsystem purpose.)\n\n## Interface\n\n(List the commands, files, APIs, or contracts this repo owns.)\n\n## Behavior\n\n(Describe the expected runtime behavior.)\n\n## Error Cases\n\n(List the important failure modes.)\n\n## Acceptance Tests\n\n- [ ] Name the executable checks required before implementation resumes.\n\n## Open Questions\n\n- (Capture migration-specific gaps here.)\n`,
316
+ 'IMPLEMENTATION_NOTES.md': `# Implementation Notes — ${projectName}\n\n## Changes\n\n(Dev fills this during implementation)\n\n## Verification\n\n(Dev fills this during implementation)\n\n## Unresolved Follow-ups\n\n(Dev lists any known gaps, tech debt, or follow-up items here.)\n`,
314
317
  'acceptance-matrix.md': `# Acceptance Matrix — ${projectName}\n\n(QA fills this.)\n`,
315
- 'ship-verdict.md': `# Ship Verdict — ${projectName}\n\n## Verdict: PENDING\n`
318
+ 'ship-verdict.md': `# Ship Verdict — ${projectName}\n\n## Verdict: PENDING\n`,
319
+ 'RELEASE_NOTES.md': `# Release Notes — ${projectName}\n\n## User Impact\n\n(QA fills this during the QA phase)\n\n## Verification Summary\n\n(QA fills this during the QA phase)\n\n## Upgrade Notes\n\n(QA fills this during the QA phase)\n\n## Known Issues\n\n(QA fills this during the QA phase)\n`
316
320
  };
317
321
  for (const [file, content] of Object.entries(planningFiles)) {
318
322
  const path = join(root, '.planning', file);
@@ -5,6 +5,7 @@
5
5
  * multi init — bootstrap a multi-repo coordinator run
6
6
  * multi status — show coordinator status and repo-run snapshots
7
7
  * multi step — reconcile repo truth, then dispatch or request the next coordinator gate
8
+ * multi resume — clear coordinator blocked state after operator recovery
8
9
  * multi approve-gate — approve a pending phase transition or completion gate
9
10
  * multi resync — detect divergence and rebuild coordinator state from repo authority
10
11
  */
@@ -26,7 +27,11 @@ import {
26
27
  requestCoordinatorCompletion,
27
28
  requestPhaseTransition,
28
29
  } from '../lib/coordinator-gates.js';
29
- import { detectDivergence, resyncFromRepoAuthority } from '../lib/coordinator-recovery.js';
30
+ import {
31
+ detectDivergence,
32
+ resyncFromRepoAuthority,
33
+ resumeCoordinatorFromBlockedState,
34
+ } from '../lib/coordinator-recovery.js';
30
35
  import {
31
36
  fireCoordinatorHook,
32
37
  buildAssignmentPayload,
@@ -154,7 +159,7 @@ export async function multiStepCommand(options) {
154
159
  // Fire on_escalation hook (advisory — cannot block, only notifies)
155
160
  fireEscalationHook(workspacePath, configResult.config, state, state.blocked_reason || 'unknown reason');
156
161
  console.error(`Coordinator is blocked: ${state.blocked_reason || 'unknown reason'}`);
157
- console.error('Resolve the blocked state before stepping.');
162
+ console.error('Resolve the blocked state, then run `agentxchain multi resume` before stepping again.');
158
163
  process.exitCode = 1;
159
164
  return;
160
165
  }
@@ -362,6 +367,57 @@ function maybeRequestCoordinatorGate(workspacePath, state, config) {
362
367
  return { ok: false, type: 'run_completion', blockers: completionEvaluation.blockers };
363
368
  }
364
369
 
370
+ // ── multi resume ───────────────────────────────────────────────────────────
371
+
372
+ export async function multiResumeCommand(options) {
373
+ const workspacePath = process.cwd();
374
+ const configResult = loadCoordinatorConfig(workspacePath);
375
+
376
+ if (!configResult.ok) {
377
+ console.error('Coordinator config error:');
378
+ for (const err of configResult.errors || []) {
379
+ console.error(` - ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`);
380
+ }
381
+ process.exitCode = 1;
382
+ return;
383
+ }
384
+
385
+ const state = loadCoordinatorState(workspacePath);
386
+ if (!state) {
387
+ console.error('No coordinator state found. Run `agentxchain multi init` first.');
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+
392
+ const result = resumeCoordinatorFromBlockedState(workspacePath, state, configResult.config);
393
+
394
+ if (!result.ok) {
395
+ console.error(result.error || 'Coordinator recovery failed.');
396
+ process.exitCode = 1;
397
+ return;
398
+ }
399
+
400
+ if (options.json) {
401
+ console.log(JSON.stringify({
402
+ ok: true,
403
+ previous_status: 'blocked',
404
+ resumed_status: result.resumed_status,
405
+ blocked_reason: result.blocked_reason,
406
+ pending_gate: result.state?.pending_gate || null,
407
+ resync: result.resync,
408
+ }, null, 2));
409
+ return;
410
+ }
411
+
412
+ console.log(`Coordinator resumed: ${result.resumed_status}`);
413
+ console.log(`Previous block: ${result.blocked_reason}`);
414
+ if (result.resumed_status === 'paused' && result.state?.pending_gate) {
415
+ console.log(`Next action: agentxchain multi approve-gate (${result.state.pending_gate.gate})`);
416
+ } else {
417
+ console.log('Next action: agentxchain multi step');
418
+ }
419
+ }
420
+
365
421
  // ── multi approve-gate ─────────────────────────────────────────────────────
366
422
 
367
423
  export async function multiApproveGateCommand(options) {
@@ -14,10 +14,12 @@
14
14
 
15
15
  import chalk from 'chalk';
16
16
  import { createInterface } from 'readline';
17
- import { readFileSync, existsSync } from 'fs';
17
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
18
18
  import { join } from 'path';
19
19
  import { loadProjectContext, loadProjectState } from '../lib/config.js';
20
20
  import { runLoop } from '../lib/run-loop.js';
21
+ import { buildRunExport } from '../lib/export.js';
22
+ import { buildGovernanceReport, formatGovernanceReportMarkdown } from '../lib/report.js';
21
23
  import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
22
24
  import {
23
25
  dispatchLocalCli,
@@ -328,6 +330,32 @@ export async function runCommand(opts) {
328
330
  }
329
331
  }
330
332
 
333
+ // ── Auto governance report ──────────────────────────────────────────────
334
+ if (opts.report !== false && result.state) {
335
+ try {
336
+ const reportsDir = join(root, '.agentxchain', 'reports');
337
+ mkdirSync(reportsDir, { recursive: true });
338
+
339
+ const exportResult = buildRunExport(root);
340
+ if (exportResult.ok) {
341
+ const runId = result.state.run_id || 'unknown';
342
+ const exportPath = join(reportsDir, `export-${runId}.json`);
343
+ writeFileSync(exportPath, JSON.stringify(exportResult.export, null, 2));
344
+
345
+ const reportResult = buildGovernanceReport(exportResult.export, { input: exportPath });
346
+ const reportPath = join(reportsDir, `report-${runId}.md`);
347
+ writeFileSync(reportPath, formatGovernanceReportMarkdown(reportResult.report));
348
+
349
+ console.log('');
350
+ console.log(chalk.dim(` Governance report: .agentxchain/reports/report-${runId}.md`));
351
+ } else {
352
+ console.log(chalk.dim(` Governance report skipped: ${exportResult.error}`));
353
+ }
354
+ } catch (err) {
355
+ console.log(chalk.dim(` Governance report failed: ${err.message}`));
356
+ }
357
+ }
358
+
331
359
  // ── Exit code ───────────────────────────────────────────────────────────
332
360
  const successReasons = new Set(['completed', 'gate_held', 'caller_stopped', 'max_turns_reached']);
333
361
  if (result.ok || successReasons.has(result.stop_reason)) {
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import chalk from 'chalk';
4
4
  import { CONFIG_FILE } from '../lib/config.js';
5
- import { loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
5
+ import { loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS, SYSTEM_SPEC_OVERLAY_SEPARATOR } from '../lib/governed-templates.js';
6
6
 
7
7
  const LEDGER_PATH = '.agentxchain/decision-ledger.jsonl';
8
8
  const PROMPT_OVERRIDE_SEPARATOR = '## Project-Type-Specific Guidance';
@@ -76,6 +76,7 @@ export async function templateSetCommand(templateId, opts) {
76
76
  prompts_missing_paths: [],
77
77
  prompts_missing_files: [],
78
78
  acceptance_hints_status: 'none',
79
+ system_spec_overlay_status: 'none',
79
80
  };
80
81
 
81
82
  // Planning artifacts
@@ -127,6 +128,22 @@ export async function templateSetCommand(templateId, opts) {
127
128
  }
128
129
  }
129
130
 
131
+ // System spec overlay
132
+ const systemSpecOverlay = manifest.system_spec_overlay;
133
+ const systemSpecPath = join(root, '.planning', 'SYSTEM_SPEC.md');
134
+ if (systemSpecOverlay && Object.keys(systemSpecOverlay).length > 0) {
135
+ if (!existsSync(systemSpecPath)) {
136
+ plan.system_spec_overlay_status = 'missing_file';
137
+ } else {
138
+ const specContent = readFileSync(systemSpecPath, 'utf8');
139
+ if (specContent.includes(SYSTEM_SPEC_OVERLAY_SEPARATOR)) {
140
+ plan.system_spec_overlay_status = 'existing_guidance';
141
+ } else {
142
+ plan.system_spec_overlay_status = 'append';
143
+ }
144
+ }
145
+ }
146
+
130
147
  // ── Dry run: print plan and exit ──────────────────────────────────────
131
148
  if (opts.dryRun) {
132
149
  console.log(chalk.bold(`\n Template: ${previousTemplate} → ${templateId}\n`));
@@ -175,6 +192,16 @@ export async function templateSetCommand(templateId, opts) {
175
192
  } else {
176
193
  console.log(` ${chalk.dim('(none)')}`);
177
194
  }
195
+ console.log('\n System spec overlay:');
196
+ if (plan.system_spec_overlay_status === 'append') {
197
+ console.log(` .planning/SYSTEM_SPEC.md: ${chalk.green('WILL APPEND template guidance')}`);
198
+ } else if (plan.system_spec_overlay_status === 'existing_guidance') {
199
+ console.log(` .planning/SYSTEM_SPEC.md: ${chalk.dim('ALREADY HAS guidance (skip)')}`);
200
+ } else if (plan.system_spec_overlay_status === 'missing_file') {
201
+ console.log(` .planning/SYSTEM_SPEC.md: ${chalk.yellow('MISSING FILE (skip)')}`);
202
+ } else {
203
+ console.log(` ${chalk.dim('(none)')}`);
204
+ }
178
205
  console.log(chalk.dim('\n No changes written. Use without --dry-run to apply.\n'));
179
206
  process.exit(0);
180
207
  }
@@ -242,7 +269,24 @@ export async function templateSetCommand(templateId, opts) {
242
269
  console.log(chalk.yellow(' Warning: .planning/acceptance-matrix.md not found. Skipping template guidance hints.'));
243
270
  }
244
271
 
245
- // 5. Decision ledger
272
+ // 5. Append system spec overlay
273
+ if (plan.system_spec_overlay_status === 'append' && existsSync(systemSpecPath)) {
274
+ const specContent = readFileSync(systemSpecPath, 'utf8');
275
+ const guidanceLines = [];
276
+ if (systemSpecOverlay.purpose_guidance) guidanceLines.push(`**Purpose:** ${systemSpecOverlay.purpose_guidance}`);
277
+ if (systemSpecOverlay.interface_guidance) guidanceLines.push(`**Interface:** ${systemSpecOverlay.interface_guidance}`);
278
+ if (systemSpecOverlay.behavior_guidance) guidanceLines.push(`**Behavior:** ${systemSpecOverlay.behavior_guidance}`);
279
+ if (systemSpecOverlay.error_cases_guidance) guidanceLines.push(`**Error Cases:** ${systemSpecOverlay.error_cases_guidance}`);
280
+ if (systemSpecOverlay.acceptance_tests_guidance) guidanceLines.push(`**Acceptance Tests:**\n${systemSpecOverlay.acceptance_tests_guidance}`);
281
+ if (systemSpecOverlay.extra_sections) guidanceLines.push(systemSpecOverlay.extra_sections);
282
+ const guidanceBlock = guidanceLines.join('\n\n');
283
+ const appended = `${specContent}\n\n${SYSTEM_SPEC_OVERLAY_SEPARATOR}\n\n${guidanceBlock}\n`;
284
+ writeFileSync(systemSpecPath, appended);
285
+ } else if (plan.system_spec_overlay_status === 'missing_file') {
286
+ console.log(chalk.yellow(' Warning: .planning/SYSTEM_SPEC.md not found. Skipping template spec overlay.'));
287
+ }
288
+
289
+ // 6. Decision ledger
246
290
  const ledgerEntry = {
247
291
  type: 'template_set',
248
292
  timestamp: new Date().toISOString(),
@@ -260,6 +304,8 @@ export async function templateSetCommand(templateId, opts) {
260
304
  prompt_missing_files: plan.prompts_missing_files,
261
305
  acceptance_hints_appended: plan.acceptance_hints_status === 'append',
262
306
  acceptance_hints_skipped_reason: plan.acceptance_hints_status === 'append' ? null : plan.acceptance_hints_status,
307
+ system_spec_overlay_appended: plan.system_spec_overlay_status === 'append',
308
+ system_spec_overlay_skipped_reason: plan.system_spec_overlay_status === 'append' ? null : plan.system_spec_overlay_status,
263
309
  operator: 'human',
264
310
  };
265
311
  appendJsonl(root, LEDGER_PATH, ledgerEntry);
@@ -275,5 +321,8 @@ export async function templateSetCommand(templateId, opts) {
275
321
  if (plan.acceptance_hints_status === 'append') {
276
322
  console.log(` Appended template guidance to acceptance-matrix.md`);
277
323
  }
324
+ if (plan.system_spec_overlay_status === 'append') {
325
+ console.log(` Appended template-specific guidance to SYSTEM_SPEC.md`);
326
+ }
278
327
  console.log('');
279
328
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Adapter Interface — declared public boundary for shipped adapter dispatch.
3
+ *
4
+ * This module gives external runners a stable way to reuse the first-party
5
+ * adapters without importing deep internal source paths.
6
+ */
7
+
8
+ export {
9
+ printManualDispatchInstructions,
10
+ waitForStagedResult,
11
+ readStagedResult,
12
+ } from './adapters/manual-adapter.js';
13
+
14
+ export {
15
+ dispatchLocalCli,
16
+ saveDispatchLogs,
17
+ resolvePromptTransport,
18
+ } from './adapters/local-cli-adapter.js';
19
+
20
+ export { dispatchApiProxy } from './adapters/api-proxy-adapter.js';
21
+
22
+ export {
23
+ DEFAULT_MCP_TOOL_NAME,
24
+ DEFAULT_MCP_TRANSPORT,
25
+ dispatchMcp,
26
+ resolveMcpToolName,
27
+ resolveMcpTransport,
28
+ describeMcpRuntimeTarget,
29
+ } from './adapters/mcp-adapter.js';
30
+
31
+ export const ADAPTER_INTERFACE_VERSION = '0.1';