create-quiver 0.13.0 → 0.14.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 (46) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -2
  3. package/README_FOR_AI.md +4 -0
  4. package/ROADMAP.md +6 -0
  5. package/docs/COMMANDS.md.template +3 -1
  6. package/docs/TROUBLESHOOTING.md.template +29 -0
  7. package/docs/WORKFLOW.md.template +13 -12
  8. package/package.json +1 -1
  9. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  10. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  11. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  12. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  13. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  14. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  15. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  16. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  17. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  18. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  19. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  20. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  21. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  22. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  23. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  24. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  25. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  26. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  27. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  28. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  29. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  30. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  31. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  32. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  33. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  34. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  36. package/src/create-quiver/commands/ai.js +481 -14
  37. package/src/create-quiver/commands/spec.js +10 -0
  38. package/src/create-quiver/index.js +42 -4
  39. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  40. package/src/create-quiver/lib/ai/export-state.js +52 -5
  41. package/src/create-quiver/lib/ai/github.js +14 -2
  42. package/src/create-quiver/lib/ai/plan-review.js +159 -0
  43. package/src/create-quiver/lib/ai/run-state.js +17 -2
  44. package/src/create-quiver/lib/ai/spec-generator.js +15 -0
  45. package/src/create-quiver/lib/project-state-resolver.js +195 -1
  46. package/src/create-quiver/lib/spec-worktrees.js +50 -2
@@ -10,6 +10,7 @@ const {
10
10
  formatDoctorFixPlan,
11
11
  } = require('./lib/doctor');
12
12
  const {
13
+ runActiveSlice: runAiActiveSlice,
13
14
  runAgent: runAiAgent,
14
15
  runApprovalStatus: runAiApprovalStatus,
15
16
  runApprove: runAiApprove,
@@ -25,6 +26,7 @@ const {
25
26
  runPlan: runAiPlan,
26
27
  runPrepareContext: runAiPrepareContext,
27
28
  runPr: runAiPr,
29
+ runRepairPlan: runAiRepairPlan,
28
30
  runPromptSlice: runAiPromptSlice,
29
31
  runReviewPlan: runAiReviewPlan,
30
32
  runRevise: runAiRevise,
@@ -99,6 +101,7 @@ const SUPPORTED_COMMAND_MODES = new Set([
99
101
  ]);
100
102
 
101
103
  const SUPPORTED_AI_COMMANDS = new Set([
104
+ 'active-slice',
102
105
  'agent',
103
106
  'approve',
104
107
  'approval-status',
@@ -114,6 +117,7 @@ const SUPPORTED_AI_COMMANDS = new Set([
114
117
  'prepare-context',
115
118
  'pr',
116
119
  'prompt-slice',
120
+ 'repair-plan',
117
121
  'review-plan',
118
122
  'revise',
119
123
  'resume',
@@ -159,7 +163,8 @@ const COMMAND_HELP_GROUPS = [
159
163
  {
160
164
  title: 'AI lifecycle',
161
165
  commands: [
162
- ['ai run create', 'Create a durable AI lifecycle run from a requirements file.'],
166
+ ['ai run create|close', 'Create a durable AI lifecycle run or close/archive a completed or stale run without deleting evidence.'],
167
+ ['ai active-slice status|reconcile', 'Inspect or dry-run reconcile local active-slice state from every supported source.'],
163
168
  ['ai status', 'Show current AI lifecycle phase, approved versions, blockers, and next command.'],
164
169
  ['ai resume', 'Resume guidance from the last valid lifecycle phase without chat memory.'],
165
170
  ['ai onboard', 'Run or print the planner onboarding prompt with a token-aware context pack.'],
@@ -167,6 +172,7 @@ const COMMAND_HELP_GROUPS = [
167
172
  ['ai agent set|list|show', 'Manage planner, executor, reviewer, and doctor provider profiles without secrets; use set --dry-run to preview.'],
168
173
  ['ai plan', 'Generate versioned planner drafts for acceptance criteria, technical plan, or spec phase.'],
169
174
  ['ai revise', 'Create a new planner draft from human feedback without approving it.'],
175
+ ['ai repair-plan', 'Repair an approved technical plan into a new structured draft without mutating the approved artifact.'],
170
176
  ['ai review-plan', 'Review the technical-plan draft for production readiness before approval.'],
171
177
  ['ai approve', 'Approve a concrete saved draft version for the next planner phase.'],
172
178
  ['ai approvals', 'Inspect approval status and saved planner drafts.'],
@@ -243,6 +249,8 @@ function printUsage() {
243
249
  npx create-quiver plan [options]
244
250
  npx create-quiver ai <task> [options]
245
251
  npx create-quiver ai run create --input <requirements.md>
252
+ npx create-quiver ai run close --run <id>
253
+ npx create-quiver ai active-slice reconcile --dry-run
246
254
  npx create-quiver ai status [options]
247
255
  npx create-quiver ai resume [options]
248
256
  npx create-quiver ai inspect [options]
@@ -253,6 +261,7 @@ function printUsage() {
253
261
  npx create-quiver ai agent <set|list|show> [role] [options]
254
262
  npx create-quiver ai prepare-context [options]
255
263
  npx create-quiver ai revise [options]
264
+ npx create-quiver ai repair-plan [options]
256
265
  npx create-quiver graph [options]
257
266
  npx create-quiver next [options]
258
267
  npx create-quiver migrate [options]
@@ -330,6 +339,7 @@ Examples:
330
339
  cd ./my-project && npx create-quiver ai onboard --print-prompt
331
340
  cd ./my-project && npx create-quiver ai prepare-context --dry-run
332
341
  cd ./my-project && npx create-quiver ai run create --input requirements.md
342
+ cd ./my-project && npx create-quiver ai active-slice reconcile --dry-run
333
343
  cd ./my-project && npx create-quiver ai status
334
344
  cd ./my-project && npx create-quiver ai resume
335
345
  cd ./my-project && npx create-quiver ai inspect
@@ -345,12 +355,14 @@ Examples:
345
355
  cd ./my-project && npx create-quiver ai revise --phase acceptance --input feedback.md --dry-run
346
356
  cd ./my-project && npx create-quiver ai approve --phase acceptance --version 1
347
357
  cd ./my-project && npx create-quiver ai plan --phase technical-plan --dry-run
358
+ cd ./my-project && npx create-quiver ai repair-plan --dry-run
348
359
  cd ./my-project && npx create-quiver ai review-plan --dry-run
349
360
  cd ./my-project && npx create-quiver ai approve --phase technical-plan --version 1
350
361
  cd ./my-project && npx create-quiver spec create --dry-run
351
362
  cd ./my-project && npx create-quiver spec start specs/my-project --dry-run
352
363
  cd ./my-project && npx create-quiver ai approvals
353
364
  cd ./my-project && npx create-quiver ai prompt-slice --slice specs/my-project/slices/slice-01/slice.json --dry-run
365
+ cd ./my-project && npx --yes create-quiver@${CLI_VERSION} ai prompt-slice --slice specs/my-project/slices/slice-01/slice.json --dry-run
354
366
  cd ./my-project && npx create-quiver ai execute-slice --slice specs/my-project/slices/slice-01/slice.json --dry-run
355
367
  cd ./my-project && npx create-quiver ai execute-slice --slice specs/my-project/slices/slice-01/slice.json --commit
356
368
  cd ./my-project && npx create-quiver ai execute-plan --dry-run --commit
@@ -936,7 +948,7 @@ function parseArgs(argv) {
936
948
  if (result.aiCommand === 'run' && !result.aiRunCommand && positional.length > 0) {
937
949
  result.aiRunCommand = positional.shift();
938
950
  }
939
- if ((result.aiCommand === 'specs' || result.aiCommand === 'slices' || result.aiCommand === 'trace') && !result.aiSecondaryCommand && positional.length > 0) {
951
+ if ((result.aiCommand === 'specs' || result.aiCommand === 'slices' || result.aiCommand === 'trace' || result.aiCommand === 'active-slice') && !result.aiSecondaryCommand && positional.length > 0) {
940
952
  result.aiSecondaryCommand = positional.shift();
941
953
  }
942
954
  if ((result.aiCommand === 'specs' || result.aiCommand === 'slices') && result.aiSecondaryCommand && result.aiSecondaryCommand !== 'list') {
@@ -945,6 +957,9 @@ function parseArgs(argv) {
945
957
  if (result.aiCommand === 'trace' && result.aiSecondaryCommand && result.aiSecondaryCommand !== 'report') {
946
958
  throw new Error(formatError(`unsupported ai trace subcommand: ${result.aiSecondaryCommand}. Supported tasks: report`));
947
959
  }
960
+ if (result.aiCommand === 'active-slice' && result.aiSecondaryCommand && result.aiSecondaryCommand !== 'status' && result.aiSecondaryCommand !== 'reconcile') {
961
+ throw new Error(formatError(`unsupported ai active-slice subcommand: ${result.aiSecondaryCommand}. Supported tasks: status, reconcile`));
962
+ }
948
963
  if (positional.length > 0) {
949
964
  throw new Error(formatError('ai does not accept extra positional arguments'));
950
965
  }
@@ -2364,7 +2379,7 @@ async function run(argv) {
2364
2379
 
2365
2380
  if (args.mode === 'ai') {
2366
2381
  if (!args.aiCommand) {
2367
- throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai onboard | prepare-context | run | status | resume | inspect | export | specs | slices | trace | plan | revise | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
2382
+ throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai onboard | prepare-context | run | active-slice | status | resume | inspect | export | specs | slices | trace | plan | revise | repair-plan | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
2368
2383
  }
2369
2384
 
2370
2385
  if (args.aiCommand === 'run') {
@@ -2377,6 +2392,14 @@ async function run(argv) {
2377
2392
  return;
2378
2393
  }
2379
2394
 
2395
+ if (args.aiCommand === 'active-slice') {
2396
+ runAiActiveSlice(process.cwd(), {
2397
+ command: args.aiSecondaryCommand || 'status',
2398
+ dryRun: args.dryRun,
2399
+ });
2400
+ return;
2401
+ }
2402
+
2380
2403
  if (args.aiCommand === 'status') {
2381
2404
  runAiLifecycleStatus(process.cwd(), {
2382
2405
  runId: args.aiRunId || undefined,
@@ -2494,6 +2517,21 @@ async function run(argv) {
2494
2517
  return;
2495
2518
  }
2496
2519
 
2520
+ if (args.aiCommand === 'repair-plan') {
2521
+ await runAiRepairPlan(process.cwd(), {
2522
+ context: args.aiContext || undefined,
2523
+ dryRun: args.dryRun,
2524
+ input: args.aiInput || undefined,
2525
+ printPrompt: args.aiPrintPrompt,
2526
+ provider: args.aiProvider,
2527
+ providerExplicit: args.aiProviderExplicit,
2528
+ role: args.aiRole,
2529
+ runId: args.aiRunId || undefined,
2530
+ timeout: args.aiTimeout,
2531
+ });
2532
+ return;
2533
+ }
2534
+
2497
2535
  if (args.aiCommand === 'revise') {
2498
2536
  await runAiRevise(process.cwd(), {
2499
2537
  context: args.aiContext || undefined,
@@ -2590,7 +2628,7 @@ async function run(argv) {
2590
2628
  return;
2591
2629
  }
2592
2630
 
2593
- throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: onboard, prepare-context, run, status, resume, inspect, export, specs, slices, trace, plan, revise, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
2631
+ throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: onboard, prepare-context, run, active-slice, status, resume, inspect, export, specs, slices, trace, plan, revise, repair-plan, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
2594
2632
  }
2595
2633
 
2596
2634
  if (args.mode === 'graph') {
@@ -27,14 +27,14 @@ const CONTEXT_PACKS = Object.freeze({
27
27
  description: 'Executor context for a single slice handoff.',
28
28
  role: ROLES.EXECUTOR,
29
29
  tokenBudgetHint: 3200,
30
- roleGuidance: 'Use slice handoff, allowed files, acceptance criteria, and validation commands only.',
30
+ roleGuidance: 'Use the slice.json, EXECUTION_BRIEF, CLOSURE_BRIEF, allowed files, acceptance criteria, and validation commands only. Do not request the full spec unless the slice brief explicitly requires it.',
31
31
  }),
32
32
  minimal: Object.freeze({
33
33
  name: 'minimal',
34
34
  description: 'Smallest executor context for narrowly-scoped tasks.',
35
35
  role: ROLES.EXECUTOR,
36
36
  tokenBudgetHint: 1200,
37
- roleGuidance: 'Use the smallest safe set of slice details and avoid onboarding context.',
37
+ roleGuidance: 'Use the smallest safe set of slice details, avoid onboarding context, and avoid full-spec context by default.',
38
38
  }),
39
39
  });
40
40
 
@@ -5,6 +5,7 @@ const { listAgentProfiles } = require('../agent-profiles');
5
5
  const { PLANNER_APPROVAL_PHASES, readPhaseApproval } = require('../approvals');
6
6
  const { collectLayoutReport } = require('../doctor');
7
7
  const {
8
+ collectActiveSliceState,
8
9
  filterSlicesForExecution,
9
10
  groupSlicesBySpec: groupResolvedSlicesBySpec,
10
11
  isBlockedStatus: isCanonicalBlockedStatus,
@@ -296,14 +297,44 @@ function collectWarnings({ graph, layout, specs, slices }) {
296
297
  function collectNextSteps(data) {
297
298
  const activeRun = [...data.runs].reverse().find((run) => run.status !== 'closed');
298
299
  const commands = [];
300
+ const firstSpec = data.specs[0] || null;
301
+ const firstSlice = data.slices[0] || null;
302
+ const activeRunWantsSpecCreate = Boolean(activeRun?.next_command && activeRun.next_command.includes('spec create'));
299
303
 
300
- commands.push({
301
- id: activeRun ? 'continue-active-run' : 'create-ai-run',
302
- command: activeRun ? activeRun.next_command : 'npx create-quiver ai run create --input <requirements.md>',
303
- reason: activeRun ? `Continue AI run ${activeRun.run_id}.` : 'Start a new AI lifecycle run.',
304
- });
304
+ if (activeRun && activeRunWantsSpecCreate && firstSpec) {
305
+ commands.push({
306
+ id: 'validate-existing-spec',
307
+ command: `npx create-quiver spec validate ${firstSpec.path}`,
308
+ reason: `A spec already exists while run ${activeRun.run_id} points to spec creation.`,
309
+ });
310
+ commands.push({
311
+ id: 'find-ready-slice',
312
+ command: 'npx create-quiver next --all-ready',
313
+ reason: 'Find ready slices before creating another spec.',
314
+ });
315
+ if (firstSlice?.slice_json) {
316
+ commands.push({
317
+ id: 'prompt-existing-slice',
318
+ command: `npx create-quiver ai prompt-slice --slice ${firstSlice.slice_json}`,
319
+ reason: 'Prepare a minimal executor prompt for the existing spec.',
320
+ });
321
+ }
322
+ } else {
323
+ commands.push({
324
+ id: activeRun ? 'continue-active-run' : 'create-ai-run',
325
+ command: activeRun ? activeRun.next_command : 'npx create-quiver ai run create --input <requirements.md>',
326
+ reason: activeRun ? `Continue AI run ${activeRun.run_id}.` : 'Start a new AI lifecycle run.',
327
+ });
328
+ }
305
329
 
306
330
  if (data.summary.slices > 0) {
331
+ if (data.active_slice?.reconciliation?.decision && data.active_slice.reconciliation.decision !== 'preserve') {
332
+ commands.push({
333
+ id: 'reconcile-active-slice',
334
+ command: 'npx create-quiver ai active-slice reconcile --dry-run',
335
+ reason: 'Review active-slice state before assigning more execution work.',
336
+ });
337
+ }
307
338
  commands.push({
308
339
  id: 'inspect-slices',
309
340
  command: 'npx create-quiver ai slices list',
@@ -379,6 +410,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
379
410
  const approvals = normalizeApprovals(projectRoot);
380
411
  const progress = summarizeProgress(slices);
381
412
  const evidence = collectEvidenceEntries(slices);
413
+ const activeSlice = collectActiveSliceState(projectRoot, { slices: allSlices });
382
414
  const blockers = normalizedSlices
383
415
  .filter((slice) => slice.blocked_reason || String(slice.status).toLowerCase() === 'blocked')
384
416
  .map((slice) => ({ ref: slice.ref, reason: slice.blocked_reason || 'blocked' }));
@@ -389,6 +421,9 @@ function collectLifecycleExport(projectRoot, options = {}) {
389
421
  if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
390
422
  blockers.push({ ref: 'migration', reason: layout.recommendations.join(' ') });
391
423
  }
424
+ if (activeSlice.reconciliation.decision === 'blocked') {
425
+ blockers.push({ ref: 'active-slice', reason: activeSlice.reconciliation.reason });
426
+ }
392
427
 
393
428
  const exportData = {
394
429
  schema_version: EXPORT_SCHEMA_VERSION,
@@ -413,6 +448,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
413
448
  runs: runs.length,
414
449
  configured_agents: agents.filter((agent) => agent.configured).length,
415
450
  approvals: approvals.length,
451
+ active_slice_sources: activeSlice.sources.length,
416
452
  warnings: 0,
417
453
  },
418
454
  agents,
@@ -437,6 +473,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
437
473
  dry_run_command: 'npx create-quiver migrate --dry-run',
438
474
  },
439
475
  evidence,
476
+ active_slice: activeSlice,
440
477
  warnings: [],
441
478
  blockers,
442
479
  next_steps: [],
@@ -480,6 +517,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
480
517
  blocker: slice.blocked_reason,
481
518
  })),
482
519
  dependencies: graph.edges,
520
+ active_slice: activeSlice,
483
521
  },
484
522
  };
485
523
 
@@ -513,6 +551,15 @@ function formatLifecycleInspect(data) {
513
551
  lines.push(`- ${step.command}`);
514
552
  }
515
553
 
554
+ if (data.active_slice) {
555
+ lines.push(
556
+ '',
557
+ 'Active slice state',
558
+ `- Sources: ${data.active_slice.sources.length}`,
559
+ `- Reconciliation: ${data.active_slice.reconciliation.decision} (${data.active_slice.reconciliation.reason})`,
560
+ );
561
+ }
562
+
516
563
  if (data.dashboard.blockers.length > 0) {
517
564
  lines.push('', 'Blockers');
518
565
  for (const blocker of data.dashboard.blockers) {
@@ -59,7 +59,8 @@ function formatGhInstallGuidance() {
59
59
  'GitHub CLI is not installed.',
60
60
  'macOS: brew install gh',
61
61
  'Linux: follow https://github.com/cli/cli/blob/trunk/docs/install_linux.md or use your distro package manager',
62
- 'Windows: winget install GitHub.cli',
62
+ 'Windows PowerShell: winget install GitHub.cli',
63
+ 'Git Bash/WSL: install gh inside the environment where the command will run, then authenticate there',
63
64
  ].join('\n');
64
65
  }
65
66
 
@@ -89,6 +90,16 @@ function formatShellPathGuidance(optionName, examplePath) {
89
90
  ].join('\n');
90
91
  }
91
92
 
93
+ function formatSshAliasGuidance(alias = '<alias>') {
94
+ const resolvedAlias = alias || '<alias>';
95
+ return [
96
+ 'SSH alias setup:',
97
+ `- macOS/Linux/Git Bash/WSL: edit ~/.ssh/config and add a Host entry such as \`Host ${resolvedAlias}\`, \`HostName github.com\`, \`User git\`, and \`IdentityFile ~/.ssh/<key>\`.`,
98
+ `- Windows PowerShell: edit $HOME\\.ssh\\config and add the same Host entry for ${resolvedAlias}.`,
99
+ `- Verify the alias with: ssh -T ${resolvedAlias}`,
100
+ ].join('\n');
101
+ }
102
+
92
103
  function formatCommandForShell(command, args, quoter) {
93
104
  return `${command} ${args.map(quoter).join(' ')}`;
94
105
  }
@@ -326,7 +337,7 @@ function ensureSshHostAlias(sshHostAlias) {
326
337
  formatActionableError({
327
338
  failure: 'missing SSH host alias. Pass --ssh-host-alias <alias> before opening the PR.',
328
339
  impact: 'Quiver cannot verify which GitHub SSH identity should be used for this PR flow.',
329
- fix: 'macOS/Linux/Git Bash/WSL: add a Host entry in ~/.ssh/config, for example `Host github-work`. Windows PowerShell: add the Host entry in $HOME\\.ssh\\config.',
340
+ fix: formatSshAliasGuidance('github-work'),
330
341
  nextCommand: 'ssh -T <alias>',
331
342
  }),
332
343
  );
@@ -675,6 +686,7 @@ module.exports = {
675
686
  ensureWorktreeReady,
676
687
  findPrBodyCandidates,
677
688
  formatGhInstallGuidance,
689
+ formatSshAliasGuidance,
678
690
  formatPreflightReport,
679
691
  formatPrCreateReport,
680
692
  preflightGitHubPr,
@@ -5,6 +5,7 @@ const { readPhaseApproval, resolveApprovedPlannerInput } = require('../approvals
5
5
  const { quiverInternalPaths } = require('../init-layout');
6
6
 
7
7
  const PLAN_REVIEW_PROMPT_SOURCE = 'packaged production-readiness plan review template';
8
+ const PLAN_REVIEW_RECOMMENDATIONS = Object.freeze(['approve', 'approve-with-risk', 'revise']);
8
9
 
9
10
  function formatError(message) {
10
11
  return `create-quiver: ${message}`;
@@ -50,6 +51,141 @@ function samePath(projectRoot, left, right) {
50
51
  return Boolean(left && right && resolvePath(projectRoot, left) === resolvePath(projectRoot, right));
51
52
  }
52
53
 
54
+ function normalizeList(value) {
55
+ if (Array.isArray(value)) {
56
+ return value.map((item) => String(item || '').trim()).filter(Boolean);
57
+ }
58
+ if (typeof value === 'string' && value.trim()) {
59
+ return value.split(/\r?\n/).map((item) => item.replace(/^[-*]\s+/, '').trim()).filter(Boolean);
60
+ }
61
+ return [];
62
+ }
63
+
64
+ function normalizeRecommendation(value) {
65
+ const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
66
+ if (PLAN_REVIEW_RECOMMENDATIONS.includes(normalized)) {
67
+ return normalized;
68
+ }
69
+ if (normalized === 'approved' || normalized === 'approvable') {
70
+ return 'approve';
71
+ }
72
+ if (normalized === 'approved-with-risk' || normalized === 'approvable-with-risk' || normalized === 'approve-with-risks') {
73
+ return 'approve-with-risk';
74
+ }
75
+ if (normalized === 'changes-required' || normalized === 'requires-revision' || normalized === 'needs-revision') {
76
+ return 'revise';
77
+ }
78
+ return '';
79
+ }
80
+
81
+ function recommendedNextCommand(recommendation, sourceVersion) {
82
+ if (recommendation === 'revise') {
83
+ return 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run';
84
+ }
85
+ return `npx create-quiver ai approve --phase technical-plan --version ${sourceVersion || '<n>'}`;
86
+ }
87
+
88
+ function parseJsonObject(value) {
89
+ try {
90
+ const parsed = JSON.parse(value);
91
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ function extractStructuredReview(text) {
98
+ const raw = String(text || '').trim();
99
+ if (!raw) {
100
+ return null;
101
+ }
102
+
103
+ const direct = parseJsonObject(raw);
104
+ if (direct) {
105
+ return direct;
106
+ }
107
+
108
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
109
+ if (!fenced) {
110
+ return null;
111
+ }
112
+ return parseJsonObject(fenced[1].trim());
113
+ }
114
+
115
+ function normalizeStructuredReview(parsed, sourceVersion) {
116
+ const review = parsed?.review || parsed?.plan_review || parsed;
117
+ const requiredFixes = normalizeList(review.required_fixes || review.requiredFixes || review.blocking_issues || review.blockingIssues);
118
+ const optionalHardening = normalizeList(review.optional_hardening || review.optionalHardening || review.non_blocking_issues || review.nonBlockingIssues);
119
+ const risks = normalizeList(review.risks || review.remaining_risks || review.remainingRisks);
120
+ let approvalRecommendation = normalizeRecommendation(review.approval_recommendation || review.approvalRecommendation || review.recommendation);
121
+ const blocking = review.blocking === true || review.has_blockers === true || review.hasBlockers === true || requiredFixes.length > 0;
122
+
123
+ if (!approvalRecommendation) {
124
+ approvalRecommendation = blocking ? 'revise' : optionalHardening.length > 0 || risks.length > 0 ? 'approve-with-risk' : 'approve';
125
+ }
126
+
127
+ const normalizedBlocking = blocking || approvalRecommendation === 'revise';
128
+
129
+ return {
130
+ schema_version: 1,
131
+ approval_recommendation: approvalRecommendation,
132
+ blocking: normalizedBlocking,
133
+ next_command: String(review.next_command || review.nextCommand || '').trim() || recommendedNextCommand(approvalRecommendation, sourceVersion),
134
+ optional_hardening: optionalHardening,
135
+ required_fixes: requiredFixes,
136
+ risks,
137
+ source: 'structured',
138
+ };
139
+ }
140
+
141
+ function classifyReviewText(text) {
142
+ const value = String(text || '').toLowerCase();
143
+ if (/\b(revise|revision|required fix|required fixes|blocking|blocker|not approvable|not ready)\b/.test(value)) {
144
+ return 'revise';
145
+ }
146
+ if (/\b(approve with risk|approvable with risk|non-blocking|optional hardening|p1|p2|risk)\b/.test(value)) {
147
+ return 'approve-with-risk';
148
+ }
149
+ if (/\b(approve|approved|approvable|no blockers|no blocking|production ready)\b/.test(value)) {
150
+ return 'approve';
151
+ }
152
+ return 'approve-with-risk';
153
+ }
154
+
155
+ function derivePlanReviewResult(contents, options = {}) {
156
+ const structured = extractStructuredReview(contents);
157
+ if (structured) {
158
+ return normalizeStructuredReview(structured, options.inputVersion);
159
+ }
160
+
161
+ const approvalRecommendation = classifyReviewText(contents);
162
+ const fallbackNote = approvalRecommendation === 'approve-with-risk'
163
+ ? ['Review output did not include structured metadata; treat approval as risky and inspect the human review text before approving.']
164
+ : [];
165
+ const requiredFixes = approvalRecommendation === 'revise'
166
+ ? ['Review output indicates the technical plan must be revised before approval.']
167
+ : [];
168
+
169
+ return {
170
+ schema_version: 1,
171
+ approval_recommendation: approvalRecommendation,
172
+ blocking: approvalRecommendation === 'revise',
173
+ next_command: recommendedNextCommand(approvalRecommendation, options.inputVersion),
174
+ optional_hardening: fallbackNote,
175
+ required_fixes: requiredFixes,
176
+ risks: [],
177
+ source: 'heuristic',
178
+ };
179
+ }
180
+
181
+ function reviewBlocksApproval(review) {
182
+ const result = review?.meta?.review_result || review?.review_result || null;
183
+ if (!result) {
184
+ return false;
185
+ }
186
+ return result.blocking === true || result.approval_recommendation === 'revise';
187
+ }
188
+
53
189
  function latestTechnicalPlanDraft(approval) {
54
190
  const version = Number(approval.meta?.draft?.version || 0);
55
191
  if (!version) {
@@ -184,6 +320,9 @@ function savePlanReview(projectRoot, options = {}) {
184
320
  const now = new Date().toISOString();
185
321
  const contents = String(options.contents || '');
186
322
  const inputPath = options.inputPath || '';
323
+ const reviewResult = derivePlanReviewResult(contents, {
324
+ inputVersion: options.inputVersion,
325
+ });
187
326
 
188
327
  fs.writeFileSync(reviewPath, contents);
189
328
  const meta = {
@@ -194,6 +333,7 @@ function savePlanReview(projectRoot, options = {}) {
194
333
  path: toRelativePosix(projectRoot, reviewPath),
195
334
  raw_artifact_path: options.rawArtifactPath || null,
196
335
  output_source: options.outputSource || null,
336
+ review_result: reviewResult,
197
337
  reviewed_at: now,
198
338
  };
199
339
  fs.writeFileSync(planReviewMetaPath(projectRoot), `${JSON.stringify(meta, null, 2)}\n`);
@@ -216,6 +356,10 @@ function assertPlanReviewed(projectRoot) {
216
356
  : ' Preview the review first, then run `npx create-quiver ai review-plan` to persist it.';
217
357
  throw new Error(formatError(`ai plan phase 'spec' requires a reviewed and approved technical-plan input; current review status: ${review.status}. Run \`${nextCommand}\`.${followUp}`));
218
358
  }
359
+ if (reviewBlocksApproval(review)) {
360
+ const result = review.meta.review_result;
361
+ throw new Error(formatError(`ai plan phase 'spec' requires an approvable production review; current approval recommendation is ${result.approval_recommendation}. Run \`${result.next_command || recommendedNextCommand('revise')}\`.`));
362
+ }
219
363
  return review;
220
364
  }
221
365
 
@@ -243,6 +387,8 @@ function buildPlanReviewPrompt({ pack, inputText, inputPath }) {
243
387
  '- operational risks',
244
388
  '- recommended fixes to the plan',
245
389
  'If ambiguity is not blocking, state the safest assumption and continue.',
390
+ 'Required output contract: include a fenced json block with `{ "review": { "blocking": boolean, "approvalRecommendation": "approve|approve-with-risk|revise", "requiredFixes": [], "optionalHardening": [], "risks": [], "nextCommand": "" } }`.',
391
+ 'Use `approve` only when no required fixes remain. Use `approve-with-risk` when only optional hardening or accepted risks remain. Use `revise` when required fixes or blocking ambiguity remain.',
246
392
  ];
247
393
 
248
394
  if (inputPath) {
@@ -261,6 +407,7 @@ function buildPlanReviewPrompt({ pack, inputText, inputPath }) {
261
407
 
262
408
  function summarizePlanReview(projectRoot) {
263
409
  const review = readPlanReview(projectRoot);
410
+ const result = review.meta?.review_result || null;
264
411
  const lines = [
265
412
  'Phase: plan-review',
266
413
  `Status: ${review.status}`,
@@ -271,16 +418,28 @@ function summarizePlanReview(projectRoot) {
271
418
  if (review.meta?.source_file) {
272
419
  lines.push(`Source file: ${review.meta.source_file}`);
273
420
  }
421
+ if (result) {
422
+ const requiredFixes = normalizeList(result.required_fixes);
423
+ const optionalHardening = normalizeList(result.optional_hardening);
424
+ lines.push(`Approval recommendation: ${result.approval_recommendation}`);
425
+ lines.push(`Blocking: ${result.blocking ? 'yes' : 'no'}`);
426
+ lines.push(`Required fixes: ${requiredFixes.length}`);
427
+ lines.push(`Optional hardening: ${optionalHardening.length}`);
428
+ lines.push(`Next command: ${result.next_command}`);
429
+ }
274
430
  return `${lines.join('\n')}\n`;
275
431
  }
276
432
 
277
433
  module.exports = {
278
434
  PLAN_REVIEW_PROMPT_SOURCE,
435
+ PLAN_REVIEW_RECOMMENDATIONS,
279
436
  assertPlanReviewed,
280
437
  buildPlanReviewPrompt,
438
+ derivePlanReviewResult,
281
439
  planReviewMetaPath,
282
440
  planReviewPath,
283
441
  readPlanReview,
442
+ reviewBlocksApproval,
284
443
  resolveTechnicalPlanReviewInput,
285
444
  resolveReviewedTechnicalPlanInput,
286
445
  savePlanReview,
@@ -357,7 +357,9 @@ function formatAiRunStatus(projectRoot, run) {
357
357
  ].join('\n');
358
358
  }
359
359
 
360
- return [
360
+ const openRuns = listAiRuns(projectRoot).filter((item) => item.status !== 'closed');
361
+ const otherOpenRuns = openRuns.filter((item) => item.run_id !== run.run_id);
362
+ const lines = [
361
363
  'AI run status',
362
364
  `Run: ${run.run_id}`,
363
365
  `Status: ${run.status}`,
@@ -366,9 +368,22 @@ function formatAiRunStatus(projectRoot, run) {
366
368
  `Requirement: ${run.requirement?.path || '(missing)'}`,
367
369
  `State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
368
370
  `Approvals: ${run.approvals_path}`,
371
+ `Open runs: ${openRuns.length}`,
372
+ ];
373
+
374
+ if (otherOpenRuns.length > 0) {
375
+ lines.push('Other open runs:');
376
+ for (const item of otherOpenRuns) {
377
+ lines.push(`- ${item.run_id}: ${item.phase} (${item.status}) -> ${nextCommandForPhase(item.phase)}`);
378
+ }
379
+ }
380
+
381
+ lines.push(
369
382
  `Next safe command: ${nextCommandForPhase(run.phase)}`,
370
383
  '',
371
- ].join('\n');
384
+ );
385
+
386
+ return lines.join('\n');
372
387
  }
373
388
 
374
389
  function formatAiRunResume(projectRoot, run) {
@@ -278,6 +278,20 @@ function buildSpecGenerationManifest({ inputText, inputPath, repoRoot, specSlug
278
278
  return manifest;
279
279
  }
280
280
 
281
+ function validateTechnicalPlanSpecContract(repoRoot, options = {}) {
282
+ const inputPath = options.inputPath || options.input;
283
+ const inputText = typeof options.inputText === 'string'
284
+ ? options.inputText
285
+ : readSourceText(inputPath, repoRoot);
286
+
287
+ return buildSpecGenerationManifest({
288
+ inputPath,
289
+ inputText,
290
+ repoRoot,
291
+ specSlug: options.specSlug,
292
+ });
293
+ }
294
+
281
295
  function validateSpecCollision(specDir) {
282
296
  if (fs.existsSync(specDir)) {
283
297
  throw new Error(formatError(`spec directory already exists: ${path.relative(process.cwd(), specDir)}`));
@@ -381,5 +395,6 @@ module.exports = {
381
395
  generateSpecArtifacts,
382
396
  parseApprovedManifest,
383
397
  readSourceText,
398
+ validateTechnicalPlanSpecContract,
384
399
  validateGeneratedSliceJson,
385
400
  };