create-quiver 0.12.0 → 0.13.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 (158) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +65 -25
  3. package/README_FOR_AI.md +36 -29
  4. package/ROADMAP.md +22 -3
  5. package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
  6. package/docs/COMMANDS.md.template +53 -20
  7. package/docs/STATUS.md.template +5 -1
  8. package/docs/WORKFLOW.md.template +13 -11
  9. package/package.json +10 -3
  10. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
  11. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
  12. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
  13. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
  14. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
  15. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
  16. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
  17. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
  18. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
  19. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
  20. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
  21. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
  22. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
  23. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
  24. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
  25. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
  26. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
  27. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
  28. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
  29. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
  30. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
  31. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
  32. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
  33. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
  34. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
  36. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
  37. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
  38. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
  39. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
  40. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
  41. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
  42. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
  43. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
  44. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
  45. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
  46. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
  47. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
  48. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
  49. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
  50. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
  51. package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
  52. package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
  53. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
  54. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
  55. package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
  56. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
  57. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
  58. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
  59. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
  60. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
  61. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
  62. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
  63. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
  64. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
  65. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
  66. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
  67. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
  68. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
  69. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
  70. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
  71. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
  72. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
  73. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
  74. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
  75. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
  76. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
  77. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
  78. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
  79. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
  80. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  81. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  82. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  83. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  84. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  85. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  86. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  87. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  88. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  89. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  90. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  91. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  92. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  93. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  94. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  95. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  96. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  97. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  98. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  99. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  100. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  101. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  102. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  103. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  104. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  105. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  106. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  107. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  108. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  109. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  110. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  111. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  112. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  113. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  114. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  115. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  116. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  117. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  118. package/src/create-quiver/commands/ai.js +652 -27
  119. package/src/create-quiver/commands/flow.js +58 -9
  120. package/src/create-quiver/commands/graph.js +11 -9
  121. package/src/create-quiver/commands/plan.js +7 -16
  122. package/src/create-quiver/commands/spec.js +282 -0
  123. package/src/create-quiver/index.js +409 -31
  124. package/src/create-quiver/lib/actionable-error.js +27 -0
  125. package/src/create-quiver/lib/agent-profiles.js +16 -4
  126. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  127. package/src/create-quiver/lib/ai/context-packs.js +4 -0
  128. package/src/create-quiver/lib/ai/execution-plan.js +16 -1
  129. package/src/create-quiver/lib/ai/executor.js +272 -21
  130. package/src/create-quiver/lib/ai/export-state.js +679 -0
  131. package/src/create-quiver/lib/ai/github.js +162 -2
  132. package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
  133. package/src/create-quiver/lib/ai/plan-review.js +7 -2
  134. package/src/create-quiver/lib/ai/providers.js +4 -3
  135. package/src/create-quiver/lib/ai/run-state.js +414 -0
  136. package/src/create-quiver/lib/ai/spec-generator.js +84 -13
  137. package/src/create-quiver/lib/ai/spec-templates.js +150 -21
  138. package/src/create-quiver/lib/analyze.js +2 -2
  139. package/src/create-quiver/lib/approvals.js +36 -5
  140. package/src/create-quiver/lib/demo.js +189 -14
  141. package/src/create-quiver/lib/doctor.js +154 -0
  142. package/src/create-quiver/lib/git.js +40 -1
  143. package/src/create-quiver/lib/handoff.js +123 -12
  144. package/src/create-quiver/lib/init-docs.js +35 -13
  145. package/src/create-quiver/lib/init-layout.js +9 -0
  146. package/src/create-quiver/lib/json.js +53 -3
  147. package/src/create-quiver/lib/lifecycle.js +52 -3
  148. package/src/create-quiver/lib/locks.js +134 -0
  149. package/src/create-quiver/lib/package-safety.js +7 -0
  150. package/src/create-quiver/lib/paths.js +74 -0
  151. package/src/create-quiver/lib/project-scan.js +74 -0
  152. package/src/create-quiver/lib/project-state-resolver.js +236 -0
  153. package/src/create-quiver/lib/readiness.js +66 -10
  154. package/src/create-quiver/lib/scope.js +52 -8
  155. package/src/create-quiver/lib/slice-graph.js +138 -38
  156. package/src/create-quiver/lib/slice.js +14 -5
  157. package/src/create-quiver/lib/spec-worktrees.js +129 -32
  158. package/src/create-quiver/lib/statuses.js +115 -0
@@ -1,14 +1,31 @@
1
1
  const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
 
4
+ const { redactSecrets } = require('../lib/evidence');
5
+ const { formatActionableError } = require('../lib/actionable-error');
6
+ const {
7
+ assertProviderPromptWithinLimit,
8
+ compactRevisionInput,
9
+ extractCleanProviderOutput,
10
+ writeRawProviderArtifact,
11
+ } = require('../lib/ai/artifacts');
4
12
  const { buildContextPackMetadata, normalizeRole } = require('../lib/ai/context-packs');
5
13
  const { runExecuteSlice, runPromptSlice } = require('../lib/ai/executor');
6
14
  const { runExecutePlan } = require('../lib/ai/execution-plan');
7
15
  const { buildPrCreatePlan, formatPreflightReport, formatPrCreateReport, preflightGitHubPr, runGhPrCreate } = require('../lib/ai/github');
8
16
  const { buildContextPreparationDrafts, buildPlannerOnboardingPrompt } = require('../lib/ai/onboarding-template');
17
+ const {
18
+ collectLifecycleExport,
19
+ formatLifecycleExportMarkdown,
20
+ formatLifecycleInspect,
21
+ formatSlicesList,
22
+ formatSpecsList,
23
+ formatTraceReport,
24
+ } = require('../lib/ai/export-state');
9
25
  const {
10
26
  PLAN_REVIEW_PROMPT_SOURCE,
11
27
  buildPlanReviewPrompt,
28
+ readPlanReview,
12
29
  resolveReviewedTechnicalPlanInput,
13
30
  resolveTechnicalPlanReviewInput,
14
31
  savePlanReview,
@@ -16,8 +33,18 @@ const {
16
33
  } = require('../lib/ai/plan-review');
17
34
  const { buildSpecGenerationManifest, describeSpecGeneration, generateSpecArtifacts } = require('../lib/ai/spec-generator');
18
35
  const { buildProviderInvocation, runProvider } = require('../lib/ai/providers');
36
+ const {
37
+ createAiRun,
38
+ ensureAiRun,
39
+ formatAiRunResume,
40
+ formatAiRunStatus,
41
+ recordAiRunApproval,
42
+ resolveAiRun,
43
+ updateAiRunPhase,
44
+ } = require('../lib/ai/run-state');
19
45
  const {
20
46
  agentProfilesPath,
47
+ buildAgentProfileState,
21
48
  getAgentProfile,
22
49
  listAgentProfiles,
23
50
  resolveProfileProvider,
@@ -26,6 +53,7 @@ const {
26
53
  const {
27
54
  PLANNER_APPROVAL_PHASES,
28
55
  approvePlannerPhase,
56
+ readPhaseApproval,
29
57
  resolveApprovedPlannerInput,
30
58
  savePlannerDraft,
31
59
  summarizePlannerApproval,
@@ -39,6 +67,8 @@ const DEFAULT_PLAN_PROVIDER = 'codex';
39
67
  const DEFAULT_PLAN_ROLE = 'planner';
40
68
  const DEFAULT_PLAN_CONTEXT = 'planning';
41
69
  const DEFAULT_PLAN_PHASE = 'acceptance';
70
+ const CONTEXT_PREP_START = '<!-- quiver:context-prep:start -->';
71
+ const CONTEXT_PREP_END = '<!-- quiver:context-prep:end -->';
42
72
 
43
73
  function formatError(message) {
44
74
  return `create-quiver: ${message}`;
@@ -85,7 +115,7 @@ function resolveProviderForProfile(repoRoot, role, provider, providerExplicit, f
85
115
  return resolveProfileProvider(repoRoot, role, fallbackProvider);
86
116
  }
87
117
 
88
- function buildPlanContext({ role, context, phase, inputText, inputPath, repoRoot }) {
118
+ function buildPlanContext({ role, context, phase, inputText, inputPath, repoRoot, revise = false }) {
89
119
  const phaseDetails = getPlannerPhaseDetails(phase);
90
120
  const pack = buildContextPackMetadata({
91
121
  role,
@@ -96,7 +126,9 @@ function buildPlanContext({ role, context, phase, inputText, inputPath, repoRoot
96
126
  const sections = [
97
127
  pack.prompt,
98
128
  `Phase: ${phaseDetails.phase}`,
99
- phaseDetails.phase === 'acceptance'
129
+ revise
130
+ ? 'Task: revise the current draft and produce a new version only. Do not advance phase, approve, create specs, or modify product code.'
131
+ : phaseDetails.phase === 'acceptance'
100
132
  ? 'Task: produce acceptance criteria only. Do not create files or modify product code.'
101
133
  : 'Task: produce a technical plan only. Do not create files or modify product code.',
102
134
  ];
@@ -167,6 +199,52 @@ function formatDryRunReport({ task, provider, role, contextPack, phase, invocati
167
199
  return `${lines.join('\n')}\n`;
168
200
  }
169
201
 
202
+ function formatPromptOnlyReport({ task, provider, role, contextPack, phase, invocation, prompt, onboardingPlan, promptSource, inputPath, inputKind, inputVersion }) {
203
+ const lines = [
204
+ `AI ${task} prompt-only`,
205
+ `Provider: ${provider}`,
206
+ `Role: ${role}`,
207
+ `Context pack: ${contextPack}`,
208
+ ];
209
+
210
+ if (phase) {
211
+ lines.push(`Phase: ${phase}`);
212
+ }
213
+
214
+ lines.push(`Command: ${invocation.command} ${invocation.args.join(' ')}`);
215
+ lines.push(`Timeout: ${invocation.timeoutMs}ms`);
216
+ lines.push(`Prompt transport: ${invocation.promptTransport.mode}`);
217
+ lines.push(`Prompt length: ${invocation.promptLength} bytes`);
218
+
219
+ if (onboardingPlan) {
220
+ lines.push(`Prompt source: ${onboardingPlan.promptSource}`);
221
+ lines.push(`Selected docs: ${onboardingPlan.selectedDocs.length}`);
222
+ lines.push(`Documentation debt: ${onboardingPlan.missingDocs.length}`);
223
+ }
224
+
225
+ if (promptSource) {
226
+ lines.push(`Prompt source: ${promptSource}`);
227
+ }
228
+
229
+ if (inputPath) {
230
+ lines.push(`Input file: ${inputPath}`);
231
+ }
232
+
233
+ if (inputKind) {
234
+ lines.push(`Input kind: ${inputKind}`);
235
+ }
236
+
237
+ if (inputVersion) {
238
+ lines.push(`Input version: v${inputVersion}`);
239
+ }
240
+
241
+ lines.push('--- PROMPT START ---');
242
+ lines.push(String(prompt || '').trimEnd());
243
+ lines.push('--- PROMPT END ---');
244
+
245
+ return `${lines.join('\n')}\n`;
246
+ }
247
+
170
248
  function formatPathList(items, emptyLabel = 'none') {
171
249
  if (!Array.isArray(items) || items.length === 0) {
172
250
  return [`- ${emptyLabel}`];
@@ -175,28 +253,41 @@ function formatPathList(items, emptyLabel = 'none') {
175
253
  return items.map((item) => `- ${item}`);
176
254
  }
177
255
 
178
- function formatContextPreparationReport({ dryRun, plan, docs, writtenDocs }) {
256
+ function formatContextPreparationReport({ dryRun, plan, writePlan, writtenDocs, snapshot, completed = false }) {
179
257
  const lines = [
180
- dryRun ? 'AI prepare-context dry-run' : 'AI prepare-context completed',
258
+ dryRun ? 'AI prepare-context dry-run' : completed ? 'AI prepare-context completed' : 'AI prepare-context write plan',
181
259
  `Mode: ${dryRun ? 'dry-run' : 'live'}`,
182
260
  `Project: ${plan.projectName}`,
183
261
  `Project slug: ${plan.projectSlug}`,
184
262
  'Writes: docs-only',
185
263
  'Product code: untouched',
186
- `Proposed docs: ${docs.length > 0 ? docs.map((doc) => doc.path).join(', ') : 'none'}`,
264
+ `Proposed docs: ${writePlan.length > 0 ? writePlan.map((item) => item.path).join(', ') : 'none'}`,
187
265
  ];
188
266
 
189
267
  if (!dryRun) {
190
- lines.push(`Written docs: ${writtenDocs.length > 0 ? writtenDocs.join(', ') : 'none'}`);
268
+ lines.push(`${completed ? 'Written docs' : 'Planned writes'}: ${writtenDocs.length > 0 ? writtenDocs.join(', ') : 'none'}`);
269
+ if (snapshot) {
270
+ lines.push(`Snapshot: ${snapshot.root}`);
271
+ }
272
+ }
273
+
274
+ if (completed) {
275
+ return `${lines.join('\n')}\n`;
191
276
  }
192
277
 
193
278
  lines.push(
279
+ 'Proposed changes:',
280
+ ...writePlan.map((item) => `- ${item.path}: ${item.action}${item.reason ? ` (${item.reason})` : ''}`),
281
+ 'Diff preview:',
282
+ ...formatDiffPreview(writePlan),
194
283
  'Files considered:',
195
284
  ...plan.filesConsidered.map((item) => `- ${item.path}: ${item.present ? 'present' : 'absent'}${item.reason ? ` (${item.reason})` : ''}`),
196
285
  'Assumptions:',
197
286
  ...formatPathList(plan.assumptions),
198
287
  'Risks:',
199
288
  ...formatPathList(plan.risks),
289
+ 'Contradictions:',
290
+ ...formatPathList(plan.contradictions),
200
291
  'Omitted paths:',
201
292
  ...formatPathList(plan.omittedPaths),
202
293
  'Uncertainty markers: TODO | Assumption | Pending confirmation',
@@ -207,20 +298,188 @@ function formatContextPreparationReport({ dryRun, plan, docs, writtenDocs }) {
207
298
 
208
299
  function writeProviderOutput(result) {
209
300
  if (result.stdout) {
210
- process.stdout.write(result.stdout);
301
+ process.stdout.write(redactSecrets(result.stdout));
211
302
  }
212
303
  if (result.stderr) {
213
- process.stderr.write(result.stderr);
304
+ process.stderr.write(redactSecrets(result.stderr));
214
305
  }
215
306
  }
216
307
 
217
- function writeDraftDocs(repoRoot, drafts) {
218
- const writtenDocs = [];
219
- for (const draft of drafts) {
308
+ function normalizeText(value) {
309
+ return String(value || '').replace(/\r\n/g, '\n');
310
+ }
311
+
312
+ function buildRevisionInput({ phase, feedbackPath, feedbackText, repoRoot, compactionOptions = {} }) {
313
+ const current = readPhaseApproval(repoRoot, phase);
314
+ if (!current.draft) {
315
+ throw new Error(formatError(`ai revise --phase ${phase} requires an existing draft; current status is ${current.status}. Run \`npx create-quiver ai plan --phase ${phase} --input <file>\` first.`));
316
+ }
317
+
318
+ const sections = [];
319
+
320
+ if (phase === 'technical-plan') {
321
+ const acceptance = resolveApprovedPlannerInput(repoRoot, phase, undefined);
322
+ const acceptanceText = readTextFile(acceptance.inputPath, repoRoot);
323
+ sections.push(`Approved acceptance input (${acceptance.inputPath}):`, acceptanceText.trimEnd());
324
+ }
325
+
326
+ sections.push(
327
+ `Current ${phase} draft (${current.draft.path}):`,
328
+ current.draft.contents.trimEnd(),
329
+ `Human feedback (${feedbackPath}):`,
330
+ feedbackText.trimEnd(),
331
+ );
332
+
333
+ return compactRevisionInput(sections.join('\n\n'), compactionOptions);
334
+ }
335
+
336
+ function buildManagedContextBlock(content) {
337
+ return `${CONTEXT_PREP_START}\n${String(content || '').trimEnd()}\n${CONTEXT_PREP_END}\n`;
338
+ }
339
+
340
+ function mergeContextDraft(existingContent, draftContent) {
341
+ const existing = normalizeText(existingContent);
342
+ const block = buildManagedContextBlock(draftContent);
343
+ const startIndex = existing.indexOf(CONTEXT_PREP_START);
344
+ const endIndex = existing.indexOf(CONTEXT_PREP_END);
345
+
346
+ if (startIndex >= 0 && endIndex > startIndex) {
347
+ const before = existing.slice(0, startIndex).trimEnd();
348
+ const after = existing.slice(endIndex + CONTEXT_PREP_END.length).trimStart();
349
+ return `${before}\n\n${block}${after ? `\n${after}` : ''}`;
350
+ }
351
+
352
+ return `${existing.trimEnd()}\n\n${block}`;
353
+ }
354
+
355
+ function firstChangedLineIndex(beforeLines, afterLines) {
356
+ const max = Math.max(beforeLines.length, afterLines.length);
357
+ for (let index = 0; index < max; index += 1) {
358
+ if (beforeLines[index] !== afterLines[index]) {
359
+ return index;
360
+ }
361
+ }
362
+ return -1;
363
+ }
364
+
365
+ function buildDiffSnippet(pathLabel, beforeContent, afterContent, maxLines = 10) {
366
+ const beforeLines = normalizeText(beforeContent).split('\n');
367
+ const afterLines = normalizeText(afterContent).split('\n');
368
+ const changedAt = firstChangedLineIndex(beforeLines, afterLines);
369
+
370
+ if (changedAt === -1) {
371
+ return [`diff -- ${pathLabel}`, ' no changes'];
372
+ }
373
+
374
+ const start = Math.max(0, changedAt - 2);
375
+ const beforeSnippet = beforeLines.slice(start, start + maxLines);
376
+ const afterSnippet = afterLines.slice(start, start + maxLines);
377
+ const lines = [
378
+ `--- ${pathLabel} (current)`,
379
+ `+++ ${pathLabel} (proposed)`,
380
+ ];
381
+
382
+ for (const line of beforeSnippet) {
383
+ if (line) {
384
+ lines.push(`- ${line}`);
385
+ }
386
+ }
387
+
388
+ for (const line of afterSnippet) {
389
+ if (line) {
390
+ lines.push(`+ ${line}`);
391
+ }
392
+ }
393
+
394
+ return lines;
395
+ }
396
+
397
+ function buildContextWritePlan(repoRoot, drafts) {
398
+ return drafts.map((draft) => {
220
399
  const destinationPath = path.join(repoRoot, draft.path);
221
- fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
222
- fs.writeFileSync(destinationPath, `${draft.content.replace(/\s+$/g, '')}\n`);
223
- writtenDocs.push(draft.path);
400
+ const exists = fs.existsSync(destinationPath);
401
+ const currentContent = exists ? fs.readFileSync(destinationPath, 'utf8') : '';
402
+ const proposedContent = exists
403
+ ? mergeContextDraft(currentContent, draft.content)
404
+ : `${String(draft.content || '').replace(/\s+$/g, '')}\n`;
405
+ const changed = normalizeText(currentContent) !== normalizeText(proposedContent);
406
+
407
+ return {
408
+ path: draft.path,
409
+ destinationPath,
410
+ action: changed ? (exists ? 'update' : 'create') : 'skip',
411
+ reason: changed ? (exists ? 'human content preserved; Quiver block appended or refreshed' : 'missing approved context doc') : 'already up to date',
412
+ exists,
413
+ currentContent,
414
+ proposedContent,
415
+ diff: buildDiffSnippet(draft.path, currentContent, proposedContent),
416
+ };
417
+ });
418
+ }
419
+
420
+ function formatDiffPreview(writePlan) {
421
+ const lines = [];
422
+ for (const item of writePlan) {
423
+ if (item.action === 'skip') {
424
+ continue;
425
+ }
426
+ lines.push(...item.diff);
427
+ }
428
+ return lines.length > 0 ? lines : ['- no changes'];
429
+ }
430
+
431
+ function createContextSnapshots(repoRoot, run, writePlan, now = new Date()) {
432
+ const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
433
+ const snapshotRoot = path.join(repoRoot, '.quiver', 'runs', run.run_id, 'snapshots', stamp);
434
+ const manifest = {
435
+ schema_version: 1,
436
+ run_id: run.run_id,
437
+ created_at: now.toISOString(),
438
+ entries: [],
439
+ };
440
+
441
+ fs.mkdirSync(snapshotRoot, { recursive: true });
442
+
443
+ for (const item of writePlan) {
444
+ if (item.action === 'skip') {
445
+ continue;
446
+ }
447
+ const entry = {
448
+ path: item.path,
449
+ action: item.action,
450
+ existed: item.exists,
451
+ snapshot_path: null,
452
+ };
453
+
454
+ if (item.exists) {
455
+ const snapshotPath = path.join(snapshotRoot, item.path);
456
+ fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
457
+ fs.copyFileSync(item.destinationPath, snapshotPath);
458
+ entry.snapshot_path = path.relative(repoRoot, snapshotPath).split(path.sep).join('/');
459
+ }
460
+
461
+ manifest.entries.push(entry);
462
+ }
463
+
464
+ const manifestPath = path.join(snapshotRoot, 'manifest.json');
465
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
466
+
467
+ return {
468
+ root: path.relative(repoRoot, snapshotRoot).split(path.sep).join('/'),
469
+ manifestPath: path.relative(repoRoot, manifestPath).split(path.sep).join('/'),
470
+ entries: manifest.entries,
471
+ };
472
+ }
473
+
474
+ function writeDraftDocs(writePlan) {
475
+ const writtenDocs = [];
476
+ for (const item of writePlan) {
477
+ if (item.action === 'skip') {
478
+ continue;
479
+ }
480
+ fs.mkdirSync(path.dirname(item.destinationPath), { recursive: true });
481
+ fs.writeFileSync(item.destinationPath, item.proposedContent);
482
+ writtenDocs.push(item.path);
224
483
  }
225
484
  return writtenDocs;
226
485
  }
@@ -350,6 +609,20 @@ async function runOnboard(repoRoot, options = {}) {
350
609
  return report;
351
610
  }
352
611
 
612
+ if (options.printPrompt) {
613
+ const report = {
614
+ task: 'onboard',
615
+ provider,
616
+ role,
617
+ contextPack: context,
618
+ invocation,
619
+ onboardingPlan: contextInfo.plan,
620
+ prompt,
621
+ };
622
+ process.stdout.write(formatPromptOnlyReport(report));
623
+ return report;
624
+ }
625
+
353
626
  let result;
354
627
  try {
355
628
  result = await (options.runProviderFn || runProvider)(provider, {
@@ -386,33 +659,62 @@ async function runOnboard(repoRoot, options = {}) {
386
659
 
387
660
  async function runPrepareContext(repoRoot, options = {}) {
388
661
  const draftPack = buildContextPreparationDrafts(repoRoot);
662
+ const writePlan = buildContextWritePlan(repoRoot, draftPack.docs);
389
663
  const report = {
390
664
  task: 'prepare-context',
391
665
  dryRun: options.dryRun === true,
392
666
  docs: draftPack.docs.map((doc) => doc.path),
393
667
  plan: draftPack.plan,
668
+ writePlan: writePlan.map((item) => ({
669
+ path: item.path,
670
+ action: item.action,
671
+ reason: item.reason,
672
+ })),
394
673
  };
395
674
 
396
675
  if (options.dryRun) {
397
676
  process.stdout.write(formatContextPreparationReport({
398
677
  dryRun: true,
399
- docs: draftPack.docs,
400
678
  plan: draftPack.plan,
679
+ writePlan,
401
680
  writtenDocs: [],
402
681
  }));
403
682
  return report;
404
683
  }
405
684
 
406
- const writtenDocs = writeDraftDocs(repoRoot, draftPack.docs);
685
+ const lifecycleRun = ensureAiRun(repoRoot, {
686
+ command: 'ai prepare-context',
687
+ input: options.input || '',
688
+ runId: options.runId,
689
+ phase: 'created',
690
+ });
691
+ const snapshot = createContextSnapshots(repoRoot, lifecycleRun, writePlan, options.now || new Date());
692
+ const plannedDocs = writePlan.filter((item) => item.action !== 'skip').map((item) => item.path);
693
+ process.stdout.write(formatContextPreparationReport({
694
+ dryRun: false,
695
+ plan: draftPack.plan,
696
+ writePlan,
697
+ writtenDocs: plannedDocs,
698
+ snapshot,
699
+ }));
700
+ const writtenDocs = writeDraftDocs(writePlan);
701
+ updateAiRunPhase(repoRoot, lifecycleRun.run_id, 'onboarding-ready', {
702
+ artifact: snapshot.manifestPath,
703
+ command: 'ai prepare-context',
704
+ });
407
705
  process.stdout.write(formatContextPreparationReport({
408
706
  dryRun: false,
409
- docs: draftPack.docs,
410
707
  plan: draftPack.plan,
708
+ writePlan,
411
709
  writtenDocs,
710
+ snapshot,
711
+ completed: true,
412
712
  }));
413
713
 
414
714
  return {
415
715
  ...report,
716
+ runId: lifecycleRun.run_id,
717
+ snapshot,
416
718
  writtenDocs,
417
719
  };
418
720
  }
@@ -424,6 +726,7 @@ async function runPlan(repoRoot, options = {}) {
424
726
  const context = options.context || DEFAULT_PLAN_CONTEXT;
425
727
  const timeoutMs = normalizeTimeout(options.timeout);
426
728
  let inputPath = options.input || '';
729
+ let inputCompaction = null;
427
730
 
428
731
  if (phase === 'spec') {
429
732
  const resolved = resolveReviewedTechnicalPlanInput(repoRoot, inputPath || undefined);
@@ -436,6 +739,17 @@ async function runPlan(repoRoot, options = {}) {
436
739
  specSlug: options.specSlug,
437
740
  });
438
741
 
742
+ if (options.printPrompt) {
743
+ const report = {
744
+ task: 'plan',
745
+ phase,
746
+ manifest,
747
+ };
748
+ process.stdout.write('AI plan prompt-only\nPhase: spec\nNo provider prompt is used for spec generation; showing the local generation plan instead.\n');
749
+ process.stdout.write(formatSpecDryRunReport({ manifest, repoRoot }));
750
+ return report;
751
+ }
752
+
439
753
  if (options.dryRun) {
440
754
  const report = {
441
755
  task: 'plan',
@@ -464,7 +778,23 @@ async function runPlan(repoRoot, options = {}) {
464
778
 
465
779
  assertPlannerPhaseReady(phase);
466
780
 
467
- if (phase === 'technical-plan') {
781
+ let inputText = '';
782
+
783
+ if (options.revise === true) {
784
+ if (!inputPath) {
785
+ throw new Error(formatError(`missing feedback input file for ai revise phase '${phase}'`));
786
+ }
787
+ const feedbackText = readTextFile(inputPath, repoRoot);
788
+ const revisionInput = buildRevisionInput({
789
+ phase,
790
+ feedbackPath: inputPath,
791
+ feedbackText,
792
+ repoRoot,
793
+ compactionOptions: options,
794
+ });
795
+ inputText = revisionInput.text;
796
+ inputCompaction = revisionInput.compaction;
797
+ } else if (phase === 'technical-plan') {
468
798
  const resolved = resolveApprovedPlannerInput(repoRoot, phase, inputPath || undefined);
469
799
  inputPath = resolved.inputPath;
470
800
  }
@@ -473,7 +803,9 @@ async function runPlan(repoRoot, options = {}) {
473
803
  throw new Error(formatError(`missing input file for ai plan phase '${phase}'`));
474
804
  }
475
805
 
476
- const inputText = readTextFile(inputPath, repoRoot);
806
+ if (!inputText) {
807
+ inputText = readTextFile(inputPath, repoRoot);
808
+ }
477
809
  const contextInfo = buildPlanContext({
478
810
  role,
479
811
  context,
@@ -481,8 +813,10 @@ async function runPlan(repoRoot, options = {}) {
481
813
  inputText,
482
814
  inputPath,
483
815
  repoRoot,
816
+ revise: options.revise === true,
484
817
  });
485
818
  const prompt = contextInfo.prompt;
819
+ assertProviderPromptWithinLimit(prompt, options);
486
820
  let invocation;
487
821
 
488
822
  try {
@@ -508,6 +842,20 @@ async function runPlan(repoRoot, options = {}) {
508
842
  return report;
509
843
  }
510
844
 
845
+ if (options.printPrompt) {
846
+ const report = {
847
+ task: 'plan',
848
+ provider,
849
+ role,
850
+ contextPack: contextInfo.pack.packName,
851
+ phase,
852
+ invocation,
853
+ prompt,
854
+ };
855
+ process.stdout.write(formatPromptOnlyReport(report));
856
+ return report;
857
+ }
858
+
511
859
  let result;
512
860
  try {
513
861
  result = await (options.runProviderFn || runProvider)(provider, {
@@ -531,7 +879,31 @@ async function runPlan(repoRoot, options = {}) {
531
879
  throw annotateProviderError(result.error || new Error('provider run failed'), 'plan', phase);
532
880
  }
533
881
 
534
- savePlannerDraft(repoRoot, phase, inputPath, [result.stdout, result.stderr].filter(Boolean).join(''));
882
+ const lifecycleRun = ensureAiRun(repoRoot, {
883
+ command: `ai plan --phase ${phase}`,
884
+ input: inputPath,
885
+ runId: options.runId,
886
+ });
887
+ const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
888
+ const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, `ai-plan-${phase}`, result, {
889
+ metadata: {
890
+ phase,
891
+ input_path: inputPath,
892
+ prompt_bytes: invocation.promptLength,
893
+ clean_output_source: clean.source,
894
+ stripped_prompt_echo: clean.strippedPromptEcho,
895
+ input_compaction: inputCompaction,
896
+ },
897
+ });
898
+ const draft = savePlannerDraft(repoRoot, phase, inputPath, clean.cleanOutput, {
899
+ rawArtifactPath: rawArtifact.path,
900
+ outputSource: clean.source,
901
+ inputCompaction,
902
+ });
903
+ updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-draft' : 'technical-plan-draft', {
904
+ artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
905
+ command: `ai plan --phase ${phase}`,
906
+ });
535
907
 
536
908
  return {
537
909
  task: 'plan',
@@ -562,6 +934,7 @@ async function runReviewPlan(repoRoot, options = {}) {
562
934
  inputText,
563
935
  inputPath,
564
936
  });
937
+ assertProviderPromptWithinLimit(built.prompt, options);
565
938
  let invocation;
566
939
 
567
940
  try {
@@ -603,6 +976,24 @@ async function runReviewPlan(repoRoot, options = {}) {
603
976
  return report;
604
977
  }
605
978
 
979
+ if (options.printPrompt) {
980
+ const report = {
981
+ task: 'review-plan',
982
+ provider,
983
+ role: 'reviewer',
984
+ contextPack: pack.packName,
985
+ phase: 'plan-review',
986
+ invocation,
987
+ prompt: built.prompt,
988
+ promptSource: built.promptSource,
989
+ inputPath,
990
+ inputKind: resolved.kind,
991
+ inputVersion: resolved.version,
992
+ };
993
+ process.stdout.write(formatPromptOnlyReport(report));
994
+ return report;
995
+ }
996
+
606
997
  let result;
607
998
  try {
608
999
  result = await (options.runProviderFn || runProvider)(provider, {
@@ -626,11 +1017,31 @@ async function runReviewPlan(repoRoot, options = {}) {
626
1017
  throw annotateProviderError(result.error || new Error('provider run failed'), 'review-plan');
627
1018
  }
628
1019
 
1020
+ const lifecycleRun = ensureAiRun(repoRoot, {
1021
+ command: 'ai review-plan',
1022
+ input: inputPath,
1023
+ runId: options.runId,
1024
+ phase: 'technical-plan-reviewed',
1025
+ });
1026
+ const clean = extractCleanProviderOutput(result, { prompt: built.prompt, projectRoot: repoRoot });
1027
+ const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, 'ai-review-plan', result, {
1028
+ metadata: {
1029
+ phase: 'plan-review',
1030
+ input_path: inputPath,
1031
+ input_kind: resolved.kind,
1032
+ input_version: resolved.version || null,
1033
+ prompt_bytes: invocation.promptLength,
1034
+ clean_output_source: clean.source,
1035
+ stripped_prompt_echo: clean.strippedPromptEcho,
1036
+ },
1037
+ });
629
1038
  const saved = savePlanReview(repoRoot, {
630
- contents: [result.stdout, result.stderr].filter(Boolean).join(''),
1039
+ contents: clean.cleanOutput,
631
1040
  inputPath,
632
1041
  inputKind: resolved.kind,
633
1042
  inputVersion: resolved.version,
1043
+ outputSource: clean.source,
1044
+ rawArtifactPath: rawArtifact.path,
634
1045
  });
635
1046
  const relativePath = path.relative(repoRoot, saved.filePath).split(path.sep).join('/');
636
1047
  process.stdout.write(`AI plan review saved\nArtifact: ${relativePath}\nPrompt source: ${PLAN_REVIEW_PROMPT_SOURCE}\n`);
@@ -649,17 +1060,46 @@ async function runReviewPlan(repoRoot, options = {}) {
649
1060
  };
650
1061
  }
651
1062
 
1063
+ async function runRevise(repoRoot, options = {}) {
1064
+ const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
1065
+ if (phase === 'spec') {
1066
+ throw new Error(formatError(`ai revise does not support phase '${phase}'`));
1067
+ }
1068
+
1069
+ const approval = readPhaseApproval(repoRoot, phase);
1070
+ if (approval.status !== 'draft' && approval.status !== 'stale') {
1071
+ throw new Error(formatError(`ai revise --phase ${phase} requires an existing draft; current status is ${approval.status}. Run \`npx create-quiver ai plan --phase ${phase} --input <file>\` first.`));
1072
+ }
1073
+
1074
+ return runPlan(repoRoot, {
1075
+ ...options,
1076
+ phase,
1077
+ revise: true,
1078
+ });
1079
+ }
1080
+
652
1081
  async function runApprove(repoRoot, options = {}) {
653
1082
  const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
654
1083
  if (phase === 'spec') {
655
1084
  throw new Error(formatError(`ai approve does not support phase '${phase}'`));
656
1085
  }
657
1086
 
658
- if (!options.input && !options.version) {
659
- throw new Error(formatError(`missing input file for ai approve phase '${phase}'`));
1087
+ if (!options.version) {
1088
+ throw new Error(formatError(`ai approve --phase ${phase} requires --version <n>. Review drafts with \`npx create-quiver ai approvals\`.`));
1089
+ }
1090
+
1091
+ if (options.input) {
1092
+ throw new Error(formatError(`ai approve --phase ${phase} approves saved draft versions only. Use \`npx create-quiver ai revise --phase ${phase} --input ${options.input}\` to create a new draft first.`));
660
1093
  }
661
1094
 
662
- const inputText = options.version ? '' : readTextFile(options.input, repoRoot);
1095
+ if (phase === 'technical-plan') {
1096
+ const review = readPlanReview(repoRoot);
1097
+ if (review.status !== 'unapproved' && review.status !== 'reviewed') {
1098
+ throw new Error(formatError(`ai approve --phase technical-plan requires a production review for the current draft; current review status is ${review.status}. Run \`npx create-quiver ai review-plan\`.`));
1099
+ }
1100
+ }
1101
+
1102
+ const inputText = '';
663
1103
 
664
1104
  if (options.dryRun) {
665
1105
  process.stdout.write(formatApprovalDryRunResult({ phase, input: options.input, version: options.version }));
@@ -675,6 +1115,21 @@ async function runApprove(repoRoot, options = {}) {
675
1115
  const result = approvePlannerPhase(repoRoot, phase, options.input || '', inputText, {
676
1116
  version: options.version || undefined,
677
1117
  });
1118
+ const lifecycleRun = ensureAiRun(repoRoot, {
1119
+ command: `ai approve --phase ${phase}`,
1120
+ input: options.input || result.filePath,
1121
+ runId: options.runId,
1122
+ });
1123
+ recordAiRunApproval(repoRoot, lifecycleRun.run_id, {
1124
+ artifact: path.relative(repoRoot, result.filePath).split(path.sep).join('/'),
1125
+ phase,
1126
+ source_file: options.input || `draft version ${options.version}`,
1127
+ version: result.version || null,
1128
+ });
1129
+ updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-approved' : 'technical-plan-approved', {
1130
+ artifact: path.relative(repoRoot, result.filePath).split(path.sep).join('/'),
1131
+ command: `ai approve --phase ${phase}`,
1132
+ });
678
1133
  process.stdout.write(formatApprovalResult({
679
1134
  ...result,
680
1135
  sourceFile: options.input || `draft version ${options.version}`,
@@ -699,6 +1154,131 @@ async function runApprovalStatus(repoRoot) {
699
1154
  };
700
1155
  }
701
1156
 
1157
+ function runLifecycleStatus(repoRoot, options = {}) {
1158
+ const run = resolveAiRun(repoRoot, options.runId || '');
1159
+ const report = formatAiRunStatus(repoRoot, run);
1160
+ process.stdout.write(report);
1161
+ return {
1162
+ task: 'status',
1163
+ run,
1164
+ report,
1165
+ };
1166
+ }
1167
+
1168
+ function runLifecycleResume(repoRoot, options = {}) {
1169
+ const run = resolveAiRun(repoRoot, options.runId || '');
1170
+ const report = formatAiRunResume(repoRoot, run);
1171
+ process.stdout.write(report);
1172
+ return {
1173
+ task: 'resume',
1174
+ run,
1175
+ report,
1176
+ };
1177
+ }
1178
+
1179
+ function runInspect(repoRoot, options = {}) {
1180
+ const report = collectLifecycleExport(repoRoot, {
1181
+ includeCompleted: options.includeCompleted === true,
1182
+ });
1183
+ process.stdout.write(formatLifecycleInspect(report));
1184
+ return {
1185
+ task: 'inspect',
1186
+ report,
1187
+ };
1188
+ }
1189
+
1190
+ function runExport(repoRoot, options = {}) {
1191
+ const report = collectLifecycleExport(repoRoot, {
1192
+ includeCompleted: options.includeCompleted === true,
1193
+ });
1194
+ const format = String(options.format || 'json').trim().toLowerCase();
1195
+
1196
+ if (format === 'json') {
1197
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
1198
+ return {
1199
+ task: 'export',
1200
+ format,
1201
+ report,
1202
+ };
1203
+ }
1204
+
1205
+ if (format === 'markdown' || format === 'md') {
1206
+ process.stdout.write(formatLifecycleExportMarkdown(report));
1207
+ return {
1208
+ task: 'export',
1209
+ format: 'markdown',
1210
+ report,
1211
+ };
1212
+ }
1213
+
1214
+ throw new Error(formatError(`unsupported ai export format: ${format}. Supported formats: json, markdown`));
1215
+ }
1216
+
1217
+ function runSpecsList(repoRoot, options = {}) {
1218
+ const report = collectLifecycleExport(repoRoot, {
1219
+ includeCompleted: options.includeCompleted === true,
1220
+ });
1221
+ if (options.json === true) {
1222
+ process.stdout.write(`${JSON.stringify({ specs: report.specs }, null, 2)}\n`);
1223
+ } else {
1224
+ process.stdout.write(formatSpecsList(report));
1225
+ }
1226
+ return {
1227
+ task: 'specs',
1228
+ specs: report.specs,
1229
+ };
1230
+ }
1231
+
1232
+ function runSlicesList(repoRoot, options = {}) {
1233
+ const report = collectLifecycleExport(repoRoot, {
1234
+ includeCompleted: options.includeCompleted === true,
1235
+ });
1236
+ if (options.json === true) {
1237
+ process.stdout.write(`${JSON.stringify({ slices: report.slices }, null, 2)}\n`);
1238
+ } else {
1239
+ process.stdout.write(formatSlicesList(report));
1240
+ }
1241
+ return {
1242
+ task: 'slices',
1243
+ slices: report.slices,
1244
+ };
1245
+ }
1246
+
1247
+ function runTraceReport(repoRoot, options = {}) {
1248
+ const report = collectLifecycleExport(repoRoot, {
1249
+ includeCompleted: options.includeCompleted === true,
1250
+ });
1251
+ process.stdout.write(formatTraceReport(report));
1252
+ return {
1253
+ task: 'trace',
1254
+ report,
1255
+ };
1256
+ }
1257
+
1258
+ function runLifecycleRun(repoRoot, options = {}) {
1259
+ const command = String(options.command || '').trim().toLowerCase();
1260
+ if (command !== 'create') {
1261
+ throw new Error(formatError(`unsupported ai run subcommand: ${command}. Supported tasks: create`));
1262
+ }
1263
+ if (!options.input) {
1264
+ throw new Error(formatError('ai run create requires --input <requirements.md>'));
1265
+ }
1266
+ const run = createAiRun(repoRoot, {
1267
+ command: 'ai run create',
1268
+ input: options.input,
1269
+ runId: options.runId,
1270
+ specSlug: options.specSlug,
1271
+ });
1272
+ const report = formatAiRunStatus(repoRoot, run);
1273
+ process.stdout.write(report);
1274
+ return {
1275
+ task: 'run',
1276
+ command,
1277
+ run,
1278
+ report,
1279
+ };
1280
+ }
1281
+
702
1282
  function formatAgentProfile(profile) {
703
1283
  const lines = [
704
1284
  `Role: ${profile.role}`,
@@ -725,16 +1305,47 @@ function formatAgentProfileList(profiles) {
725
1305
  return `${lines.join('\n')}\n`;
726
1306
  }
727
1307
 
1308
+ function formatAgentProfileDryRun(repoRoot, result) {
1309
+ const relativePath = path.relative(repoRoot, result.filePath).split(path.sep).join('/');
1310
+ const verb = result.action === 'update' ? 'update' : 'create';
1311
+ return [
1312
+ 'AI agent profile dry-run',
1313
+ '- Writes: none',
1314
+ `- Would ${verb}: ${relativePath}`,
1315
+ '',
1316
+ formatAgentProfile(result.profile).trimEnd(),
1317
+ '',
1318
+ 'No secrets or provider credentials are stored in agent profiles.',
1319
+ '',
1320
+ ].join('\n');
1321
+ }
1322
+
728
1323
  function runAgent(repoRoot, options = {}) {
729
1324
  const command = String(options.command || '').trim().toLowerCase();
730
1325
 
731
1326
  if (command === 'set') {
732
1327
  if (!options.role) {
733
- throw new Error(formatError('missing agent role. Use: npx create-quiver ai agent set <planner|executor|reviewer|researcher> --provider <provider>'));
1328
+ throw new Error(formatError('missing agent role. Use: npx create-quiver ai agent set <planner|executor|reviewer|doctor> --provider <provider>'));
734
1329
  }
735
1330
  if (!options.provider) {
736
1331
  throw new Error(formatError('ai agent set requires --provider. Supported providers: codex, claude, gemini.'));
737
1332
  }
1333
+ if (options.dryRun) {
1334
+ const preview = buildAgentProfileState(repoRoot, options.role, {
1335
+ context: options.context,
1336
+ label: options.label,
1337
+ model: options.model,
1338
+ provider: options.provider,
1339
+ });
1340
+ process.stdout.write(formatAgentProfileDryRun(repoRoot, preview));
1341
+ return {
1342
+ task: 'agent',
1343
+ command,
1344
+ dryRun: true,
1345
+ profile: preview.profile,
1346
+ filePath: path.relative(repoRoot, preview.filePath).split(path.sep).join('/'),
1347
+ };
1348
+ }
738
1349
  const result = setAgentProfile(repoRoot, options.role, {
739
1350
  context: options.context,
740
1351
  label: options.label,
@@ -754,11 +1365,16 @@ function runAgent(repoRoot, options = {}) {
754
1365
 
755
1366
  if (command === 'show') {
756
1367
  if (!options.role) {
757
- throw new Error(formatError('missing agent role. Use: npx create-quiver ai agent show <planner|executor|reviewer|researcher>'));
1368
+ throw new Error(formatError('missing agent role. Use: npx create-quiver ai agent show <planner|executor|reviewer|doctor>'));
758
1369
  }
759
1370
  const profile = getAgentProfile(repoRoot, options.role);
760
1371
  if (!profile) {
761
- throw new Error(formatError(`agent profile '${options.role}' is not configured. Run: npx create-quiver ai agent set ${options.role} --provider <provider> --model <label>`));
1372
+ throw new Error(formatActionableError({
1373
+ failure: `agent profile '${options.role}' is not configured.`,
1374
+ impact: 'Quiver will fall back to default provider behavior and may use the wrong model/cost profile.',
1375
+ fix: `Configure the ${options.role} profile with a supported provider and optional model label.`,
1376
+ nextCommand: `npx create-quiver ai agent set ${options.role} --provider <provider> --model <label>`,
1377
+ }));
762
1378
  }
763
1379
  process.stdout.write(formatAgentProfile(profile));
764
1380
  return {
@@ -903,12 +1519,21 @@ module.exports = {
903
1519
  runDoctor,
904
1520
  runExecutePlan,
905
1521
  runExecuteSlice,
1522
+ runLifecycleResume,
1523
+ runLifecycleRun,
1524
+ runLifecycleStatus,
1525
+ runExport,
1526
+ runInspect,
906
1527
  runPromptSlice,
907
1528
  runApprove,
908
1529
  runApprovalStatus,
909
1530
  runPrepareContext,
910
1531
  runReviewPlan,
1532
+ runRevise,
911
1533
  runPr,
1534
+ runSlicesList,
1535
+ runSpecsList,
1536
+ runTraceReport,
912
1537
  runOnboard,
913
1538
  runPlan,
914
1539
  writeProviderOutput,