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.
- package/CHANGELOG.md +19 -0
- package/README.md +9 -2
- package/README_FOR_AI.md +4 -0
- package/ROADMAP.md +6 -0
- package/docs/COMMANDS.md.template +3 -1
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- package/package.json +1 -1
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
- package/src/create-quiver/commands/ai.js +481 -14
- package/src/create-quiver/commands/spec.js +10 -0
- package/src/create-quiver/index.js +42 -4
- package/src/create-quiver/lib/ai/context-packs.js +2 -2
- package/src/create-quiver/lib/ai/export-state.js +52 -5
- package/src/create-quiver/lib/ai/github.js +14 -2
- package/src/create-quiver/lib/ai/plan-review.js +159 -0
- package/src/create-quiver/lib/ai/run-state.js +17 -2
- package/src/create-quiver/lib/ai/spec-generator.js +15 -0
- package/src/create-quiver/lib/project-state-resolver.js +195 -1
- 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
|
|
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
|
|
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
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|