create-quiver 0.12.1 → 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 (79) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +16 -8
  3. package/README_FOR_AI.md +11 -6
  4. package/ROADMAP.md +9 -2
  5. package/docs/COMMANDS.md.template +9 -2
  6. package/package.json +2 -1
  7. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
  8. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
  9. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  10. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  11. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  12. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  13. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  14. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  15. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  16. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  17. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  18. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  19. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  20. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  21. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  22. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  23. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  24. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  25. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  26. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  27. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  28. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  29. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  30. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  31. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  32. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  33. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  34. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  35. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  36. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  37. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  38. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  39. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  40. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  41. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  42. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  43. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  44. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  45. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  46. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  47. package/src/create-quiver/commands/ai.js +84 -9
  48. package/src/create-quiver/commands/flow.js +52 -4
  49. package/src/create-quiver/commands/graph.js +7 -7
  50. package/src/create-quiver/commands/plan.js +6 -15
  51. package/src/create-quiver/commands/spec.js +282 -0
  52. package/src/create-quiver/index.js +83 -21
  53. package/src/create-quiver/lib/agent-profiles.js +15 -3
  54. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  55. package/src/create-quiver/lib/ai/execution-plan.js +9 -0
  56. package/src/create-quiver/lib/ai/executor.js +3 -2
  57. package/src/create-quiver/lib/ai/export-state.js +242 -97
  58. package/src/create-quiver/lib/ai/github.js +80 -3
  59. package/src/create-quiver/lib/ai/plan-review.js +2 -0
  60. package/src/create-quiver/lib/ai/spec-generator.js +72 -13
  61. package/src/create-quiver/lib/ai/spec-templates.js +72 -12
  62. package/src/create-quiver/lib/analyze.js +2 -2
  63. package/src/create-quiver/lib/approvals.js +14 -2
  64. package/src/create-quiver/lib/doctor.js +79 -0
  65. package/src/create-quiver/lib/git.js +40 -1
  66. package/src/create-quiver/lib/handoff.js +43 -1
  67. package/src/create-quiver/lib/init-docs.js +11 -7
  68. package/src/create-quiver/lib/init-layout.js +1 -0
  69. package/src/create-quiver/lib/lifecycle.js +52 -3
  70. package/src/create-quiver/lib/locks.js +134 -0
  71. package/src/create-quiver/lib/package-safety.js +7 -0
  72. package/src/create-quiver/lib/paths.js +74 -0
  73. package/src/create-quiver/lib/project-scan.js +74 -0
  74. package/src/create-quiver/lib/project-state-resolver.js +236 -0
  75. package/src/create-quiver/lib/readiness.js +48 -7
  76. package/src/create-quiver/lib/scope.js +2 -1
  77. package/src/create-quiver/lib/slice.js +8 -4
  78. package/src/create-quiver/lib/spec-worktrees.js +121 -38
  79. package/src/create-quiver/lib/statuses.js +115 -0
@@ -39,9 +39,14 @@ const { runFlow } = require('./commands/flow');
39
39
  const { runGraph } = require('./commands/graph');
40
40
  const { runNext } = require('./commands/next');
41
41
  const { runPlan } = require('./commands/plan');
42
- const { runCreateSpec } = require('./commands/spec');
42
+ const { runCreateSpec, runValidateSpec } = require('./commands/spec');
43
43
  const { buildInitLayout, formatInitLayoutPlan } = require('./lib/init-layout');
44
- const { initializeProjectDocs, installSelfAsDevDep, refreshAiContextDoc } = require('./lib/init-docs');
44
+ const {
45
+ formatInstallSelfCommand,
46
+ initializeProjectDocs,
47
+ installSelfAsDevDep,
48
+ refreshAiContextDoc,
49
+ } = require('./lib/init-docs');
45
50
  const { checkPrReadiness, checkScope, checkSliceReadiness } = require('./lib/readiness');
46
51
  const { cleanupSlice, refreshActiveSlicesBoard, startSlice } = require('./lib/lifecycle');
47
52
  const { buildSpecStatus, closeSpecWorktree, formatSpecCloseResult, formatSpecStartResult, formatSpecStatus, startSpecWorktree } = require('./lib/spec-worktrees');
@@ -119,7 +124,7 @@ const SUPPORTED_AI_COMMANDS = new Set([
119
124
  'trace',
120
125
  ]);
121
126
 
122
- const SUPPORTED_SPEC_COMMANDS = new Set(['close', 'create', 'start', 'status']);
127
+ const SUPPORTED_SPEC_COMMANDS = new Set(['close', 'create', 'start', 'status', 'validate']);
123
128
  const SUPPORTED_DEMO_COMMANDS = new Set(['create']);
124
129
 
125
130
  function unsupportedCommandMessage(commandName) {
@@ -159,7 +164,7 @@ const COMMAND_HELP_GROUPS = [
159
164
  ['ai resume', 'Resume guidance from the last valid lifecycle phase without chat memory.'],
160
165
  ['ai onboard', 'Run or print the planner onboarding prompt with a token-aware context pack.'],
161
166
  ['ai prepare-context', 'Preview or write docs-only AI context updates with assumptions and risks.'],
162
- ['ai agent set|list|show', 'Manage planner, executor, reviewer, and doctor provider profiles without secrets.'],
167
+ ['ai agent set|list|show', 'Manage planner, executor, reviewer, and doctor provider profiles without secrets; use set --dry-run to preview.'],
163
168
  ['ai plan', 'Generate versioned planner drafts for acceptance criteria, technical plan, or spec phase.'],
164
169
  ['ai revise', 'Create a new planner draft from human feedback without approving it.'],
165
170
  ['ai review-plan', 'Review the technical-plan draft for production readiness before approval.'],
@@ -188,6 +193,7 @@ const COMMAND_HELP_GROUPS = [
188
193
  ['spec create', 'Create the real spec tree from a reviewed approved technical plan.'],
189
194
  ['spec start', 'Create or reuse the dedicated worktree and branch for one spec.'],
190
195
  ['spec status', 'Show spec worktree, branch, slice-00 state, and pending slices.'],
196
+ ['spec validate', 'Validate spec docs, slices, briefs, evidence, status, dependencies, and safe paths.'],
191
197
  ['spec close', 'Close a merged clean spec worktree and guide local sync.'],
192
198
  ['start-slice', 'Start work on one slice and mark it active.'],
193
199
  ['check-slice', 'Validate slice structure, dependencies, scope, and readiness.'],
@@ -263,6 +269,7 @@ function printUsage() {
263
269
  npx create-quiver spec create [options]
264
270
  npx create-quiver spec start <spec-dir>
265
271
  npx create-quiver spec status <spec-dir>
272
+ npx create-quiver spec validate <spec-dir>
266
273
  npx create-quiver spec close <spec-dir>
267
274
  npx create-quiver evidence run [options] -- <command>
268
275
  npx create-quiver demo create spec-viewer [options]
@@ -282,12 +289,13 @@ Options:
282
289
  --all-ready List every ready slice returned by next
283
290
  --auto-start Prompt for confirmation and run start-slice on next
284
291
  --local For check-slice, run structural validation without remote/base checks
292
+ --strict Treat supported validation warnings as failures
285
293
  --unicode Prefer Unicode output when supported
286
294
  --minimal Plan or run the minimal init profile
287
295
  --full Plan or run the full compatibility init profile
288
296
  --legacy-scripts Include legacy Bash wrappers in init profile
289
297
  --include-templates Export packaged templates in init profile
290
- --dry-run Preview init, migrate, prepare, spec create/start/close, demo, or AI work without executing writes/providers
298
+ --dry-run Preview init, analyze, migrate, prepare, spec create/start/close, demo, ai agent set, or AI work without executing writes/providers
291
299
  --print-prompt Print the exact AI prompt and exit without executing provider CLIs
292
300
  --fix For doctor, apply safe non-destructive repairs
293
301
  --execute For ai execute-plan, run the planned slices instead of printing commands
@@ -302,7 +310,7 @@ Options:
302
310
  --ssh-host-alias <name> SSH host alias to validate for prepare or AI commands
303
311
  --identity-file <path> SSH identity file to validate for prepare or AI commands
304
312
  --remote <name> Git remote name for check-slice or AI PR checks
305
- --base <branch> Base branch for check-slice, ai pr, or spec close (default: main)
313
+ --base <branch> Base branch for check-slice, check-scope, ai pr, or spec close (default: main)
306
314
  --output <file> Output file for evidence run
307
315
  --max-output <n> Maximum stdout/stderr chars per evidence section
308
316
  --title <text> Override PR title for ai pr create
@@ -330,6 +338,7 @@ Examples:
330
338
  cd ./my-project && npx create-quiver ai specs list
331
339
  cd ./my-project && npx create-quiver ai slices list --json
332
340
  cd ./my-project && npx create-quiver ai trace report
341
+ cd ./my-project && npx create-quiver ai agent set planner --provider codex --model gpt-5.5 --dry-run
333
342
  cd ./my-project && npx create-quiver ai agent set planner --provider codex --model gpt-5.5
334
343
  cd ./my-project && npx create-quiver ai agent list
335
344
  cd ./my-project && npx create-quiver ai plan --phase acceptance --input requirements.md --dry-run
@@ -368,6 +377,7 @@ Examples:
368
377
  cd ./my-project && npx create-quiver refresh-active-slices
369
378
  cd ./my-project && npx create-quiver spec start specs/my-project
370
379
  cd ./my-project && npx create-quiver spec status specs/my-project
380
+ cd ./my-project && npx create-quiver spec validate specs/my-project
371
381
  cd ./my-project && npx create-quiver spec close specs/my-project --dry-run
372
382
  cd ./my-project && npx create-quiver evidence run -- npm test
373
383
  cd ./my-project && npx create-quiver demo create spec-viewer --dry-run
@@ -951,7 +961,7 @@ function parseArgs(argv) {
951
961
  result.specCommand = positional.shift();
952
962
  }
953
963
  if (!result.specCommand) {
954
- throw new Error(formatError('missing spec subcommand. Use: npx create-quiver spec <create|start|status|close>'));
964
+ throw new Error(formatError('missing spec subcommand. Use: npx create-quiver spec <create|start|status|validate|close>'));
955
965
  }
956
966
  if (result.specCommand !== 'create' && positional.length > 0) {
957
967
  result.targetDir = positional.shift();
@@ -1574,8 +1584,8 @@ function detectFrameworks(projectRoot, files, rootEntries, packageJson) {
1574
1584
  },
1575
1585
  {
1576
1586
  name: 'vue',
1577
- matches: () => dependencies.has('vue') || rootFileSet.has('vue.config.js') || rootFileSet.has('vite.config.js') || rootFileSet.has('vite.config.ts'),
1578
- signals: ['vue', 'vue.config.*', 'vite.config.*'],
1587
+ matches: () => dependencies.has('vue') || rootFileSet.has('vue.config.js'),
1588
+ signals: ['vue', 'vue.config.*'],
1579
1589
  },
1580
1590
  {
1581
1591
  name: 'react',
@@ -1998,7 +2008,7 @@ function writeProjectScanArtifacts(projectRoot, scan) {
1998
2008
  return { jsonPath, mdPath: scanPaths.projectMapPath };
1999
2009
  }
2000
2010
 
2001
- function runAnalyze(targetDir) {
2011
+ function runAnalyze(targetDir, options = {}) {
2002
2012
  const projectRoot = resolveTargetRoot(process.cwd(), targetDir);
2003
2013
 
2004
2014
  if (!fs.existsSync(projectRoot)) {
@@ -2006,6 +2016,26 @@ function runAnalyze(targetDir) {
2006
2016
  }
2007
2017
 
2008
2018
  const scan = buildProjectScan(projectRoot);
2019
+
2020
+ if (options.dryRun) {
2021
+ console.log(`Project analysis dry-run for ${projectRoot}`);
2022
+ console.log('Writes: none');
2023
+ console.log(`Would write ${CURRENT_SCAN_RELATIVE_PATH}`);
2024
+ console.log(`Would write ${PROJECT_MAP_RELATIVE_PATH}`);
2025
+ console.log('Would refresh docs/AI_CONTEXT.md');
2026
+ console.log(`Detected primary stack: ${scan.stack.primary}`);
2027
+ console.log(`Detected frameworks: ${scan.stack.frameworks.length > 0 ? scan.stack.frameworks.join(', ') : 'none detected'}`);
2028
+ console.log(`Detected package manager: ${scan.project.package_manager}`);
2029
+ return {
2030
+ artifacts: {
2031
+ jsonPath: path.join(projectRoot, CURRENT_SCAN_RELATIVE_PATH),
2032
+ mdPath: path.join(projectRoot, PROJECT_MAP_RELATIVE_PATH),
2033
+ },
2034
+ dryRun: true,
2035
+ scan,
2036
+ };
2037
+ }
2038
+
2009
2039
  const artifacts = writeProjectScanArtifacts(projectRoot, scan);
2010
2040
  const aiContextPath = refreshAiContextDoc(projectRoot, scan);
2011
2041
  updateStateForAnalyze(projectRoot, CLI_VERSION);
@@ -2016,6 +2046,13 @@ function runAnalyze(targetDir) {
2016
2046
  console.log(`Wrote ${relativePosixPath(projectRoot, aiContextPath)}`);
2017
2047
  console.log(`Detected primary stack: ${scan.stack.primary}`);
2018
2048
  console.log(`Detected package manager: ${scan.project.package_manager}`);
2049
+
2050
+ return {
2051
+ artifacts,
2052
+ aiContextPath,
2053
+ dryRun: false,
2054
+ scan,
2055
+ };
2019
2056
  }
2020
2057
 
2021
2058
  function runMigrate(targetDir, options = {}) {
@@ -2079,7 +2116,7 @@ function runMigrate(targetDir, options = {}) {
2079
2116
  if (installResult === 'installed') {
2080
2117
  console.log(`Added create-quiver@${CLI_VERSION} as dev dependency`);
2081
2118
  } else if (installResult === 'failed') {
2082
- console.warn(`Warning: could not install create-quiver automatically. Run: npm install -D create-quiver@${CLI_VERSION}`);
2119
+ console.warn(`Warning: could not install create-quiver automatically. Run: ${formatInstallSelfCommand(projectRoot, CLI_VERSION)}`);
2083
2120
  }
2084
2121
  }
2085
2122
 
@@ -2117,6 +2154,11 @@ function runDoctor(targetDir, options = {}) {
2117
2154
 
2118
2155
  const doctorReport = collectDoctorReport(projectRoot);
2119
2156
  const specSlugs = doctorReport.specSlugs;
2157
+ const doctorExampleTarget = doctorReport.exampleTarget || {
2158
+ sliceId: '<slice-id>',
2159
+ source: 'generic',
2160
+ specSlug: '<spec-slug>',
2161
+ };
2120
2162
  const specRequiredFiles = specSlugs.flatMap((projectSlug) => [
2121
2163
  `specs/${projectSlug}/SPEC.md`,
2122
2164
  `specs/${projectSlug}/STATUS.md`,
@@ -2237,16 +2279,22 @@ function runDoctor(targetDir, options = {}) {
2237
2279
  if (!hasQuiverState) {
2238
2280
  console.log('- Run migration first: npx create-quiver migrate');
2239
2281
  } else if (!hasScanArtifacts) {
2240
- console.log('- Analyze the project first: npx create-quiver analyze');
2282
+ console.log('- Analyze the project first: npx create-quiver analyze');
2241
2283
  } else {
2242
2284
  console.log('- Ask your AI agent: Read AGENTS.md, then docs/AI_ONBOARDING_PROMPT.md and execute it.');
2243
2285
  }
2244
2286
  console.log('- Check the next ready slice: npx create-quiver next');
2245
2287
  if (specSlugs.length > 0) {
2246
- const projectSlug = specSlugs[0];
2247
- console.log(`- Start a slice: npx create-quiver start-slice specs/${projectSlug}/slices/<slice-id>/slice.json`);
2248
- console.log(`- Validate a slice: npx create-quiver check-slice specs/${projectSlug}/slices/<slice-id>/slice.json`);
2249
- console.log(`- Validate the PR gate: npx create-quiver check-pr specs/${projectSlug}/slices/<slice-id>/slice.json`);
2288
+ const projectSlug = doctorExampleTarget.specSlug;
2289
+ const sliceId = doctorExampleTarget.sliceId || '<slice-id>';
2290
+ if (doctorExampleTarget.source === 'active-slice') {
2291
+ console.log(`- Example target: ${projectSlug}/${sliceId} (${doctorExampleTarget.status})`);
2292
+ } else if (doctorExampleTarget.source === 'generic-multiple-specs') {
2293
+ console.log('- Example target: specs/<spec-slug>/slices/<slice-id>/slice.json (generic because no active slice is obvious)');
2294
+ }
2295
+ console.log(`- Start a slice: npx create-quiver start-slice specs/${projectSlug}/slices/${sliceId}/slice.json`);
2296
+ console.log(`- Validate a slice: npx create-quiver check-slice specs/${projectSlug}/slices/${sliceId}/slice.json`);
2297
+ console.log(`- Validate the PR gate: npx create-quiver check-pr specs/${projectSlug}/slices/${sliceId}/slice.json`);
2250
2298
  } else {
2251
2299
  console.log('- Create real specs and slices only after acceptance criteria are approved and the technical plan is reviewed and approved.');
2252
2300
  }
@@ -2280,7 +2328,9 @@ async function run(argv) {
2280
2328
  }
2281
2329
 
2282
2330
  if (args.mode === 'analyze') {
2283
- runAnalyze(args.targetDir);
2331
+ runAnalyze(args.targetDir, {
2332
+ dryRun: args.dryRun,
2333
+ });
2284
2334
  return;
2285
2335
  }
2286
2336
 
@@ -2387,6 +2437,7 @@ async function run(argv) {
2387
2437
  model: args.aiModel || undefined,
2388
2438
  provider: args.aiProviderExplicit ? args.aiProvider : undefined,
2389
2439
  role: args.aiAgentRole || undefined,
2440
+ dryRun: args.dryRun,
2390
2441
  });
2391
2442
  return;
2392
2443
  }
@@ -2655,7 +2706,11 @@ async function run(argv) {
2655
2706
  }
2656
2707
 
2657
2708
  if (args.mode === 'check-scope') {
2658
- checkScope(args.targetDir, { strict: args.strict });
2709
+ checkScope(args.targetDir, {
2710
+ baseBranch: args.baseBranchExplicit ? args.aiBaseBranch : '',
2711
+ remote: args.aiRemote,
2712
+ strict: args.strict,
2713
+ });
2659
2714
  return;
2660
2715
  }
2661
2716
 
@@ -2676,7 +2731,7 @@ async function run(argv) {
2676
2731
  }
2677
2732
 
2678
2733
  if (!args.targetDir || args.targetDir === '.') {
2679
- throw new Error(formatError('missing spec directory. Use: npx create-quiver spec <start|status|close> <spec-dir>'));
2734
+ throw new Error(formatError('missing spec directory. Use: npx create-quiver spec <start|status|validate|close> <spec-dir>'));
2680
2735
  }
2681
2736
 
2682
2737
  if (args.specCommand === 'start') {
@@ -2693,6 +2748,13 @@ async function run(argv) {
2693
2748
  return;
2694
2749
  }
2695
2750
 
2751
+ if (args.specCommand === 'validate') {
2752
+ runValidateSpec(process.cwd(), args.targetDir, {
2753
+ strict: args.strict,
2754
+ });
2755
+ return;
2756
+ }
2757
+
2696
2758
  if (args.specCommand === 'close') {
2697
2759
  const report = closeSpecWorktree(process.cwd(), args.targetDir, {
2698
2760
  baseBranch: args.aiBaseBranch,
@@ -2705,7 +2767,7 @@ async function run(argv) {
2705
2767
  return;
2706
2768
  }
2707
2769
 
2708
- throw new Error(formatError(`unsupported spec subcommand: ${args.specCommand}. Supported tasks: create, start, status, close`));
2770
+ throw new Error(formatError(`unsupported spec subcommand: ${args.specCommand}. Supported tasks: create, start, status, validate, close`));
2709
2771
  }
2710
2772
 
2711
2773
  const packageRoot = path.resolve(__dirname, '../..');
@@ -2748,7 +2810,7 @@ async function run(argv) {
2748
2810
  if (installResult === 'installed') {
2749
2811
  console.log(`Added create-quiver@${CLI_VERSION} as dev dependency`);
2750
2812
  } else if (installResult === 'failed') {
2751
- console.warn(`Warning: could not install create-quiver automatically. Run: npm install -D create-quiver@${CLI_VERSION}`);
2813
+ console.warn(`Warning: could not install create-quiver automatically. Run: ${formatInstallSelfCommand(targetDir, CLI_VERSION)}`);
2752
2814
  }
2753
2815
  }
2754
2816
 
@@ -95,6 +95,16 @@ function listAgentProfiles(projectRoot) {
95
95
  }
96
96
 
97
97
  function setAgentProfile(projectRoot, role, options = {}) {
98
+ const next = buildAgentProfileState(projectRoot, role, options);
99
+
100
+ const filePath = writeAgentProfiles(projectRoot, next.state);
101
+ return {
102
+ filePath,
103
+ profile: next.profile,
104
+ };
105
+ }
106
+
107
+ function buildAgentProfileState(projectRoot, role, options = {}) {
98
108
  const normalizedRole = normalizeAgentProfileRole(role);
99
109
  const provider = assertSupportedProvider(options.provider);
100
110
  const model = normalizeOptionalText(options.model, 'model');
@@ -102,7 +112,7 @@ function setAgentProfile(projectRoot, role, options = {}) {
102
112
  const context = normalizeOptionalText(options.context, 'context');
103
113
  const state = readAgentProfiles(projectRoot);
104
114
  const current = state.profiles[normalizedRole] || {};
105
- const now = new Date().toISOString();
115
+ const now = options.now instanceof Date ? options.now.toISOString() : new Date().toISOString();
106
116
  const profile = {
107
117
  role: normalizedRole,
108
118
  provider,
@@ -119,10 +129,11 @@ function setAgentProfile(projectRoot, role, options = {}) {
119
129
  };
120
130
  state.updated_at = now;
121
131
 
122
- const filePath = writeAgentProfiles(projectRoot, state);
123
132
  return {
124
- filePath,
133
+ action: current.provider ? 'update' : 'create',
134
+ filePath: agentProfilesPath(projectRoot),
125
135
  profile,
136
+ state,
126
137
  };
127
138
  }
128
139
 
@@ -139,6 +150,7 @@ module.exports = {
139
150
  PROFILE_STATE_VERSION,
140
151
  agentProfilesPath,
141
152
  formatProviderList,
153
+ buildAgentProfileState,
142
154
  getAgentProfile,
143
155
  listAgentProfiles,
144
156
  normalizeAgentProfileRole,
@@ -0,0 +1,318 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+
5
+ const { redactSecrets } = require('../evidence');
6
+ const { quiverInternalPaths } = require('../init-layout');
7
+
8
+ const RAW_ARTIFACT_SCHEMA_VERSION = 1;
9
+ const DEFAULT_MAX_PROVIDER_PROMPT_BYTES = 1024 * 1024;
10
+ const DEFAULT_MAX_REVISION_INPUT_BYTES = 400 * 1024;
11
+ const DEFAULT_COMPACTED_REVISION_INPUT_BYTES = 120 * 1024;
12
+
13
+ const IMPORTANT_REVISION_LINE = /\b(acceptance|criteri[ao]s?|decision|decisi[o\u00f3]n|risk|riesgo|file|archivo|changed|cambio|scope|alcance|validation|validaci[o\u00f3]n|test|blocker|bloque|dependency|dependencia|assumption|supuesto|pending|pendiente|error|rollback|evidence|evidencia)\b/i;
14
+ const PROVIDER_LOG_LINE = /^\s*(?:\[?(?:debug|info|notice|trace|warn|warning)\]?[:\s-]|(?:codex|claude|gemini)\b.*(?:provider|model|prompt|token|running|loading|thinking)|(?:using|loading)\s+(?:model|provider)\b|prompt\s+(?:length|transport)\s*:)/i;
15
+
16
+ function formatError(message) {
17
+ return `create-quiver: ${message}`;
18
+ }
19
+
20
+ function byteLength(value) {
21
+ return Buffer.byteLength(String(value || ''), 'utf8');
22
+ }
23
+
24
+ function normalizeText(value) {
25
+ return String(value || '')
26
+ .replace(/\r\n/g, '\n')
27
+ .replace(/\r/g, '\n')
28
+ .replace(/\u001b\[[0-9;]*m/g, '');
29
+ }
30
+
31
+ function escapeRegExp(value) {
32
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
33
+ }
34
+
35
+ function redactSensitiveLocalValues(text, options = {}) {
36
+ let result = redactSecrets(text);
37
+ const replacements = [];
38
+
39
+ if (options.projectRoot) {
40
+ replacements.push({
41
+ value: path.resolve(options.projectRoot),
42
+ label: '[PROJECT_ROOT]',
43
+ });
44
+ }
45
+
46
+ if (os.homedir()) {
47
+ replacements.push({
48
+ value: os.homedir(),
49
+ label: '[HOME]',
50
+ });
51
+ }
52
+
53
+ for (const replacement of replacements) {
54
+ if (!replacement.value) {
55
+ continue;
56
+ }
57
+ result = result.replace(new RegExp(escapeRegExp(replacement.value), 'g'), replacement.label);
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ function normalizePositiveInteger(value, fallback) {
64
+ if (value === undefined || value === null || value === '') {
65
+ return fallback;
66
+ }
67
+ const parsed = Number(value);
68
+ if (!Number.isFinite(parsed) || parsed <= 0) {
69
+ return fallback;
70
+ }
71
+ return Math.floor(parsed);
72
+ }
73
+
74
+ function resolveAiArtifactLimits(options = {}) {
75
+ return {
76
+ maxProviderPromptBytes: normalizePositiveInteger(
77
+ options.maxProviderPromptBytes ?? process.env.QUIVER_AI_MAX_PROMPT_BYTES,
78
+ DEFAULT_MAX_PROVIDER_PROMPT_BYTES,
79
+ ),
80
+ maxRevisionInputBytes: normalizePositiveInteger(
81
+ options.maxRevisionInputBytes ?? process.env.QUIVER_AI_MAX_REVISION_INPUT_BYTES,
82
+ DEFAULT_MAX_REVISION_INPUT_BYTES,
83
+ ),
84
+ compactedRevisionInputBytes: normalizePositiveInteger(
85
+ options.compactedRevisionInputBytes ?? process.env.QUIVER_AI_COMPACTED_REVISION_INPUT_BYTES,
86
+ DEFAULT_COMPACTED_REVISION_INPUT_BYTES,
87
+ ),
88
+ };
89
+ }
90
+
91
+ function stripPromptEcho(text, prompt) {
92
+ const normalizedText = normalizeText(text);
93
+ const normalizedPrompt = normalizeText(prompt).trim();
94
+
95
+ if (!normalizedPrompt || normalizedPrompt.length < 80) {
96
+ return normalizedText;
97
+ }
98
+
99
+ const directIndex = normalizedText.indexOf(normalizedPrompt);
100
+ if (directIndex >= 0) {
101
+ return `${normalizedText.slice(0, directIndex)}${normalizedText.slice(directIndex + normalizedPrompt.length)}`;
102
+ }
103
+
104
+ return normalizedText;
105
+ }
106
+
107
+ function stripProviderLogEdges(text) {
108
+ const lines = normalizeText(text).split('\n');
109
+
110
+ while (lines.length > 0 && (PROVIDER_LOG_LINE.test(lines[0]) || lines[0].trim() === '')) {
111
+ lines.shift();
112
+ }
113
+
114
+ while (lines.length > 0 && (PROVIDER_LOG_LINE.test(lines[lines.length - 1]) || lines[lines.length - 1].trim() === '')) {
115
+ lines.pop();
116
+ }
117
+
118
+ return lines.join('\n').trim();
119
+ }
120
+
121
+ function normalizeDraftOutput(text, sourceText = text) {
122
+ const value = normalizeText(text).trim();
123
+ if (!value) {
124
+ return '';
125
+ }
126
+ return /\n\s*$/.test(String(sourceText || '')) ? `${value}\n` : value;
127
+ }
128
+
129
+ function extractCleanProviderOutput(result, options = {}) {
130
+ const stdout = redactSensitiveLocalValues(result?.stdout || '', options);
131
+ const stderr = redactSensitiveLocalValues(result?.stderr || '', options);
132
+ const primary = stdout.trim() ? stdout : stderr;
133
+ const cleaned = stripProviderLogEdges(stripPromptEcho(primary, options.prompt || ''));
134
+ const cleanOutput = normalizeDraftOutput(cleaned, primary);
135
+
136
+ if (cleanOutput) {
137
+ return {
138
+ cleanOutput,
139
+ source: stdout.trim() ? 'stdout' : 'stderr',
140
+ strippedPromptEcho: primary !== stripPromptEcho(primary, options.prompt || ''),
141
+ };
142
+ }
143
+
144
+ return {
145
+ cleanOutput: normalizeDraftOutput(primary || [stdout, stderr].filter(Boolean).join('\n'), primary),
146
+ source: stdout.trim() ? 'stdout' : stderr.trim() ? 'stderr' : 'empty',
147
+ strippedPromptEcho: false,
148
+ };
149
+ }
150
+
151
+ function safeArtifactName(scope, now = new Date()) {
152
+ const slug = String(scope || 'provider-output')
153
+ .trim()
154
+ .toLowerCase()
155
+ .replace(/[^a-z0-9._-]+/g, '-')
156
+ .replace(/^-+|-+$/g, '') || 'provider-output';
157
+ const stamp = now.toISOString()
158
+ .replace(/\.\d{3}Z$/, 'z')
159
+ .replace(/[^0-9a-z]+/gi, '-')
160
+ .toLowerCase()
161
+ .replace(/^-+|-+$/g, '');
162
+ return `${stamp}-${slug}.json`;
163
+ }
164
+
165
+ function toRelativePosix(root, filePath) {
166
+ return path.relative(root, filePath).split(path.sep).join('/');
167
+ }
168
+
169
+ function writeRawProviderArtifact(projectRoot, runId, scope, result, options = {}) {
170
+ if (!runId) {
171
+ throw new Error(formatError('missing AI run id for raw provider artifact'));
172
+ }
173
+
174
+ const now = options.now || new Date();
175
+ const rawDir = path.join(quiverInternalPaths(projectRoot).runsDir, String(runId), 'raw');
176
+ const rawPath = path.join(rawDir, safeArtifactName(scope, now));
177
+ const serializedError = result?.error
178
+ ? {
179
+ code: result.error.code || null,
180
+ message: result.error.message || String(result.error),
181
+ provider: result.error.provider || null,
182
+ command: result.error.command || null,
183
+ }
184
+ : null;
185
+ const artifact = {
186
+ schema_version: RAW_ARTIFACT_SCHEMA_VERSION,
187
+ kind: 'provider-output',
188
+ scope: String(scope || 'provider-output'),
189
+ created_at: now.toISOString(),
190
+ provider: result?.provider || null,
191
+ command: result?.command || null,
192
+ args: Array.isArray(result?.args) ? result.args.slice() : [],
193
+ cwd: result?.cwd ? redactSensitiveLocalValues(result.cwd, { projectRoot }) : null,
194
+ ok: Boolean(result?.ok),
195
+ dry_run: Boolean(result?.dryRun),
196
+ exit_code: typeof result?.exitCode === 'number' ? result.exitCode : null,
197
+ signal: result?.signal || null,
198
+ timeout_ms: typeof result?.timeoutMs === 'number' ? result.timeoutMs : null,
199
+ prompt_transport: result?.promptTransport || null,
200
+ stdout: redactSensitiveLocalValues(result?.stdout || '', { projectRoot }),
201
+ stderr: redactSensitiveLocalValues(result?.stderr || '', { projectRoot }),
202
+ error: serializedError ? JSON.parse(redactSensitiveLocalValues(JSON.stringify(serializedError), { projectRoot })) : null,
203
+ metadata: options.metadata || {},
204
+ };
205
+
206
+ fs.mkdirSync(rawDir, { recursive: true });
207
+ fs.writeFileSync(rawPath, `${JSON.stringify(artifact, null, 2)}\n`);
208
+
209
+ return {
210
+ filePath: rawPath,
211
+ path: toRelativePosix(projectRoot, rawPath),
212
+ artifact,
213
+ };
214
+ }
215
+
216
+ function compactTextToByteLimit(text, maxBytes) {
217
+ let value = normalizeText(text).trim();
218
+ if (byteLength(value) <= maxBytes) {
219
+ return value;
220
+ }
221
+
222
+ while (byteLength(value) > maxBytes && value.length > 0) {
223
+ value = value.slice(0, Math.max(0, value.length - Math.ceil((byteLength(value) - maxBytes) / 2) - 32)).trimEnd();
224
+ }
225
+
226
+ return value;
227
+ }
228
+
229
+ function compactRevisionInput(inputText, options = {}) {
230
+ const limits = resolveAiArtifactLimits(options);
231
+ const originalText = normalizeText(inputText);
232
+ const originalBytes = byteLength(originalText);
233
+
234
+ if (originalBytes <= limits.maxRevisionInputBytes) {
235
+ return {
236
+ text: originalText,
237
+ compaction: null,
238
+ };
239
+ }
240
+
241
+ const lines = originalText.split('\n');
242
+ const selected = [];
243
+ const seen = new Set();
244
+ const addLine = (line) => {
245
+ const key = line;
246
+ if (seen.has(key)) {
247
+ return;
248
+ }
249
+ seen.add(key);
250
+ selected.push(line);
251
+ };
252
+
253
+ lines.slice(0, 24).forEach(addLine);
254
+ for (const line of lines) {
255
+ if (/^\s*#{1,6}\s+/.test(line) || IMPORTANT_REVISION_LINE.test(line)) {
256
+ addLine(line);
257
+ }
258
+ }
259
+ lines.slice(-24).forEach(addLine);
260
+
261
+ const preface = [
262
+ `[Quiver compacted oversized revise input from ${originalBytes} bytes before provider execution.]`,
263
+ '[Preserved headings and lines mentioning decisions, risks, files, acceptance criteria, validation, blockers, dependencies, assumptions, pending work, rollback, and evidence.]',
264
+ '',
265
+ ].join('\n');
266
+ const compacted = `${preface}${selected.join('\n')}`;
267
+ const targetBytes = Math.min(limits.compactedRevisionInputBytes, limits.maxRevisionInputBytes);
268
+ const finalText = compactTextToByteLimit(compacted, targetBytes);
269
+ const compactedBytes = byteLength(finalText);
270
+
271
+ if (compactedBytes > limits.maxRevisionInputBytes) {
272
+ const error = new Error(formatError(`ai revise input is too large after compaction (${compactedBytes} bytes; limit ${limits.maxRevisionInputBytes}). Reduce feedback size and retry.`));
273
+ error.code = 'AI_INPUT_TOO_LARGE';
274
+ throw error;
275
+ }
276
+
277
+ return {
278
+ text: `${finalText.trimEnd()}\n`,
279
+ compaction: {
280
+ compacted: true,
281
+ original_bytes: originalBytes,
282
+ compacted_bytes: compactedBytes,
283
+ max_revision_input_bytes: limits.maxRevisionInputBytes,
284
+ preserved: ['headings', 'decisions', 'risks', 'files', 'acceptance criteria', 'validation', 'blockers', 'dependencies', 'assumptions', 'rollback', 'evidence'],
285
+ },
286
+ };
287
+ }
288
+
289
+ function assertProviderPromptWithinLimit(prompt, options = {}) {
290
+ const limits = resolveAiArtifactLimits(options);
291
+ const promptBytes = byteLength(prompt);
292
+
293
+ if (promptBytes <= limits.maxProviderPromptBytes) {
294
+ return {
295
+ prompt,
296
+ bytes: promptBytes,
297
+ maxProviderPromptBytes: limits.maxProviderPromptBytes,
298
+ };
299
+ }
300
+
301
+ const error = new Error(formatError(`provider prompt is too large (${promptBytes} bytes; limit ${limits.maxProviderPromptBytes}). Reduce the input, split the work, or run ai revise with focused feedback before invoking the provider.`));
302
+ error.code = 'AI_PROMPT_TOO_LARGE';
303
+ throw error;
304
+ }
305
+
306
+ module.exports = {
307
+ DEFAULT_COMPACTED_REVISION_INPUT_BYTES,
308
+ DEFAULT_MAX_PROVIDER_PROMPT_BYTES,
309
+ DEFAULT_MAX_REVISION_INPUT_BYTES,
310
+ RAW_ARTIFACT_SCHEMA_VERSION,
311
+ assertProviderPromptWithinLimit,
312
+ byteLength,
313
+ compactRevisionInput,
314
+ extractCleanProviderOutput,
315
+ redactSensitiveLocalValues,
316
+ resolveAiArtifactLimits,
317
+ writeRawProviderArtifact,
318
+ };
@@ -3,6 +3,7 @@ const path = require('node:path');
3
3
 
4
4
  const { resolveProfileProvider } = require('../agent-profiles');
5
5
  const { branchDelete, runGit, statusPorcelain, worktreeAdd, worktreePrune, worktreeRemove } = require('../git');
6
+ const { withLock } = require('../locks');
6
7
  const { safeBranchName, worktreesRootForRepo } = require('../slice');
7
8
  const { buildGraph, computeLevels, detectFileConflicts, isFoundationSliceId, readAllSlices, topoSort, SliceGraphError } = require('../slice-graph');
8
9
  const { runExecuteSlice } = require('./executor');
@@ -486,6 +487,13 @@ async function runParallelGroupInWorktrees(repoRoot, level, group, options = {})
486
487
  const slices = group.slice_refs.map((ref) => level.slices.find((item) => item.ref === ref));
487
488
  const workspaces = slices.map((slice, index) => buildDelegatedWorkspace(repoRoot, slice, runId, index, options));
488
489
 
490
+ return withLock(repoRoot, `execute-plan-${runId}`, {
491
+ command: 'ai execute-plan',
492
+ metadata: {
493
+ mode: 'delegated',
494
+ slices: group.slice_refs,
495
+ },
496
+ }, async () => {
489
497
  let runResults;
490
498
  try {
491
499
  worktreePrune(repoRoot);
@@ -542,6 +550,7 @@ async function runParallelGroupInWorktrees(repoRoot, level, group, options = {})
542
550
  workspace: item.workspace.worktreePath,
543
551
  integratedCommit: item.commit,
544
552
  }));
553
+ });
545
554
  }
546
555
 
547
556
  async function runExecutePlan(repoRoot, options = {}) {