create-quiver 0.15.1 → 0.15.2
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/.github/workflows/ci.yml +3 -0
- package/README.md +2 -0
- package/README_FOR_AI.md +5 -3
- package/docs/CLI_UX_GUIDE.md +15 -1
- package/docs/COMMANDS.md.template +2 -1
- package/docs/WORKFLOW.md.template +2 -2
- package/docs/reference/commands.md +10 -4
- package/package.json +1 -1
- package/specs/quiver-v33-approval-ux-and-planner-progress/EVIDENCE_REPORT.md +93 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/EXECUTION_PLAN.md +83 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/SPEC.md +158 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/STATUS.md +31 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/pr.md +109 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/CLOSURE_BRIEF.md +30 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/slice.json +65 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/slice.json +74 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/slice.json +79 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/slice.json +75 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/slice.json +73 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/slice.json +91 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/slice.json +87 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/CLOSURE_BRIEF.md +21 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/slice.json +97 -0
- package/src/create-quiver/commands/ai.js +112 -12
- package/src/create-quiver/commands/flow.js +74 -12
- package/src/create-quiver/commands/spec.js +8 -1
- package/src/create-quiver/index.js +10 -2
- package/src/create-quiver/lib/ai/approval-candidates.js +86 -0
- package/src/create-quiver/lib/ai/plan-review.js +77 -1
- package/src/create-quiver/lib/ai/run-state.js +21 -4
- package/src/create-quiver/lib/approvals.js +103 -0
- package/src/create-quiver/lib/git.js +13 -4
- package/src/create-quiver/lib/paths.js +45 -3
|
@@ -85,6 +85,10 @@ const {
|
|
|
85
85
|
savePlannerDraft,
|
|
86
86
|
summarizePlannerApproval,
|
|
87
87
|
} = require('../lib/approvals');
|
|
88
|
+
const {
|
|
89
|
+
buildApprovalCandidateReport,
|
|
90
|
+
formatApprovalDecisionLines,
|
|
91
|
+
} = require('../lib/ai/approval-candidates');
|
|
88
92
|
const { assertPlannerPhaseReady, getPlannerPhaseDetails, normalizePlannerPhase, PlannerPhaseError } = require('../lib/ai/phase-gates');
|
|
89
93
|
const { collectActiveSliceState, resolveProjectState } = require('../lib/project-state-resolver');
|
|
90
94
|
|
|
@@ -1224,7 +1228,11 @@ function formatApprovalStatusReport(repoRoot) {
|
|
|
1224
1228
|
for (const phase of PLANNER_APPROVAL_PHASES) {
|
|
1225
1229
|
const summary = summarizePlannerApproval(repoRoot, phase).trimEnd();
|
|
1226
1230
|
const relation = classifyGlobalApprovalRelation(readPhaseApproval(repoRoot, phase), runApprovalRows);
|
|
1227
|
-
|
|
1231
|
+
const candidates = buildApprovalCandidateReport(repoRoot, phase);
|
|
1232
|
+
const decisionLines = formatApprovalDecisionLines(candidates)
|
|
1233
|
+
.map((line) => `- ${line}`)
|
|
1234
|
+
.join('\n');
|
|
1235
|
+
sections.push(`${summary}\nRun relation: ${relation}${decisionLines ? `\nApproval candidates:\n${decisionLines}` : ''}`);
|
|
1228
1236
|
}
|
|
1229
1237
|
sections.push(summarizePlanReview(repoRoot).trimEnd());
|
|
1230
1238
|
return `${sections.join('\n\n')}\n`;
|
|
@@ -1657,7 +1665,7 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
1657
1665
|
|
|
1658
1666
|
if (options.revise === true) {
|
|
1659
1667
|
if (!inputPath) {
|
|
1660
|
-
throw new Error(formatError(`missing feedback input file for ai revise phase '${phase}'`));
|
|
1668
|
+
throw new Error(formatError(`missing feedback input file for ai revise phase '${phase}'. Use: npx create-quiver ai revise --phase ${phase} --input <feedback.md> --dry-run`));
|
|
1661
1669
|
}
|
|
1662
1670
|
const feedbackText = readTextFile(inputPath, repoRoot);
|
|
1663
1671
|
const revisionInput = buildRevisionInput({
|
|
@@ -2190,20 +2198,112 @@ async function runRevise(repoRoot, options = {}) {
|
|
|
2190
2198
|
});
|
|
2191
2199
|
}
|
|
2192
2200
|
|
|
2201
|
+
function formatApprovalCandidateHint(candidate) {
|
|
2202
|
+
const parts = [];
|
|
2203
|
+
if (candidate.current) {
|
|
2204
|
+
parts.push('current');
|
|
2205
|
+
}
|
|
2206
|
+
if (candidate.created_at) {
|
|
2207
|
+
parts.push(candidate.created_at);
|
|
2208
|
+
}
|
|
2209
|
+
if (candidate.review?.recommendation) {
|
|
2210
|
+
parts.push(`review=${candidate.review.recommendation}`);
|
|
2211
|
+
}
|
|
2212
|
+
if (candidate.review?.required_fixes_count) {
|
|
2213
|
+
parts.push(`required fixes=${candidate.review.required_fixes_count}`);
|
|
2214
|
+
}
|
|
2215
|
+
if (candidate.review?.optional_hardening_count) {
|
|
2216
|
+
parts.push(`optional=${candidate.review.optional_hardening_count}`);
|
|
2217
|
+
}
|
|
2218
|
+
if (candidate.review?.risks_count) {
|
|
2219
|
+
parts.push(`risks=${candidate.review.risks_count}`);
|
|
2220
|
+
}
|
|
2221
|
+
parts.push(candidate.reason);
|
|
2222
|
+
return parts.filter(Boolean).join(', ');
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
function approvalSelectionOptions(report) {
|
|
2226
|
+
return report.candidates.map((candidate) => ({
|
|
2227
|
+
label: `${candidate.label}${candidate.recommended ? ' (recommended)' : candidate.current ? ' (current)' : ' (history)'}`,
|
|
2228
|
+
value: String(candidate.version || ''),
|
|
2229
|
+
hint: formatApprovalCandidateHint(candidate),
|
|
2230
|
+
default: candidate.recommended === true,
|
|
2231
|
+
raw: candidate,
|
|
2232
|
+
}));
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
async function resolveApprovalVersion(repoRoot, phase, options = {}) {
|
|
2236
|
+
if (options.version) {
|
|
2237
|
+
return options.version;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const canPrompt = isInteractiveAgentPromptAvailable(options);
|
|
2241
|
+
const shouldPrompt = options.interactive === true || canPrompt;
|
|
2242
|
+
const report = buildApprovalCandidateReport(repoRoot, phase);
|
|
2243
|
+
|
|
2244
|
+
if (!shouldPrompt || !canPrompt) {
|
|
2245
|
+
const recommended = report.recommended?.version || report.latest_version || '<n>';
|
|
2246
|
+
throw new Error(formatActionableError({
|
|
2247
|
+
failure: `ai approve --phase ${phase} requires --version <n> when prompts are not available.`,
|
|
2248
|
+
impact: 'Quiver cannot safely guess which saved planner draft the human approved.',
|
|
2249
|
+
fix: 'Review drafts with `npx create-quiver ai approvals`, then pass the version explicitly.',
|
|
2250
|
+
nextCommand: `npx create-quiver ai approve --phase ${phase} --version ${recommended}`,
|
|
2251
|
+
}));
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
if (report.candidates.length === 0) {
|
|
2255
|
+
throw new Error(formatActionableError({
|
|
2256
|
+
failure: `ai approve --phase ${phase} has no saved drafts to approve.`,
|
|
2257
|
+
impact: 'There is no planner artifact that can pass the approval gate.',
|
|
2258
|
+
fix: `Generate a ${phase} draft first.`,
|
|
2259
|
+
nextCommand: `npx create-quiver ai plan --phase ${phase}${phase === 'acceptance' ? ' --input <requirements.md>' : ''} --dry-run`,
|
|
2260
|
+
}));
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
const selected = await selectOption(`¿Qué ${phase} draft querés aprobar?`, approvalSelectionOptions(report), {
|
|
2264
|
+
env: options.env,
|
|
2265
|
+
error: options.error,
|
|
2266
|
+
input: options.input,
|
|
2267
|
+
interactive: true,
|
|
2268
|
+
noColor: options.noColor,
|
|
2269
|
+
output: options.output,
|
|
2270
|
+
prompts: options.prompts,
|
|
2271
|
+
promptSelect: options.promptSelect,
|
|
2272
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
2273
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
2274
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
2275
|
+
defaultValue: report.recommended?.version ? String(report.recommended.version) : undefined,
|
|
2276
|
+
flag: '--version',
|
|
2277
|
+
name: `${phase} approval version`,
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
const candidate = selected.raw;
|
|
2281
|
+
if (!candidate?.approvable) {
|
|
2282
|
+
throw new Error(formatActionableError({
|
|
2283
|
+
failure: `${phase} draft ${selected.label} is not approvable.`,
|
|
2284
|
+
impact: candidate?.review?.blocking
|
|
2285
|
+
? 'The current review gate blocks technical-plan approval.'
|
|
2286
|
+
: 'Quiver only approves the current eligible draft version.',
|
|
2287
|
+
fix: candidate?.reason || 'Inspect planner approvals before approving.',
|
|
2288
|
+
nextCommand: candidate?.next_command || `npx create-quiver ai approvals`,
|
|
2289
|
+
}));
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
return selected.value;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2193
2295
|
async function runApprove(repoRoot, options = {}) {
|
|
2194
2296
|
const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
|
|
2195
2297
|
if (phase === 'spec') {
|
|
2196
2298
|
throw new Error(formatError(`ai approve does not support phase '${phase}'`));
|
|
2197
2299
|
}
|
|
2198
2300
|
|
|
2199
|
-
if (!options.version) {
|
|
2200
|
-
throw new Error(formatError(`ai approve --phase ${phase} requires --version <n>. Review drafts with \`npx create-quiver ai approvals\`.`));
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
2301
|
if (options.input) {
|
|
2204
2302
|
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.`));
|
|
2205
2303
|
}
|
|
2206
2304
|
|
|
2305
|
+
const version = await resolveApprovalVersion(repoRoot, phase, options);
|
|
2306
|
+
|
|
2207
2307
|
if (phase === 'technical-plan') {
|
|
2208
2308
|
const review = readPlanReview(repoRoot);
|
|
2209
2309
|
if (review.status !== 'unapproved' && review.status !== 'reviewed') {
|
|
@@ -2214,24 +2314,24 @@ async function runApprove(repoRoot, options = {}) {
|
|
|
2214
2314
|
const requiredFixes = Array.isArray(result.required_fixes) ? result.required_fixes.length : 0;
|
|
2215
2315
|
throw new Error(formatError(`ai approve --phase technical-plan is blocked by plan review; approval recommendation is ${result.approval_recommendation}. Required fixes: ${requiredFixes}. Next command: ${result.next_command}`));
|
|
2216
2316
|
}
|
|
2217
|
-
assertTechnicalPlanDraftHasSpecContract(repoRoot,
|
|
2317
|
+
assertTechnicalPlanDraftHasSpecContract(repoRoot, version);
|
|
2218
2318
|
}
|
|
2219
2319
|
|
|
2220
2320
|
const inputText = '';
|
|
2221
2321
|
|
|
2222
2322
|
if (options.dryRun) {
|
|
2223
|
-
process.stdout.write(formatApprovalDryRunResult({ phase, input: options.input, version
|
|
2323
|
+
process.stdout.write(formatApprovalDryRunResult({ phase, input: options.input, version }));
|
|
2224
2324
|
return {
|
|
2225
2325
|
task: 'approve',
|
|
2226
2326
|
phase,
|
|
2227
2327
|
input: options.input,
|
|
2228
|
-
version:
|
|
2328
|
+
version: version || null,
|
|
2229
2329
|
dryRun: true,
|
|
2230
2330
|
};
|
|
2231
2331
|
}
|
|
2232
2332
|
|
|
2233
2333
|
const result = approvePlannerPhase(repoRoot, phase, options.input || '', inputText, {
|
|
2234
|
-
version:
|
|
2334
|
+
version: version || undefined,
|
|
2235
2335
|
});
|
|
2236
2336
|
const lifecycleRun = ensureAiRun(repoRoot, {
|
|
2237
2337
|
command: `ai approve --phase ${phase}`,
|
|
@@ -2241,7 +2341,7 @@ async function runApprove(repoRoot, options = {}) {
|
|
|
2241
2341
|
recordAiRunApproval(repoRoot, lifecycleRun.run_id, {
|
|
2242
2342
|
artifact: path.relative(repoRoot, result.filePath).split(path.sep).join('/'),
|
|
2243
2343
|
phase,
|
|
2244
|
-
source_file: options.input || `draft version ${
|
|
2344
|
+
source_file: options.input || `draft version ${version}`,
|
|
2245
2345
|
version: result.version || null,
|
|
2246
2346
|
});
|
|
2247
2347
|
updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-approved' : 'technical-plan-approved', {
|
|
@@ -2250,7 +2350,7 @@ async function runApprove(repoRoot, options = {}) {
|
|
|
2250
2350
|
});
|
|
2251
2351
|
process.stdout.write(formatApprovalResult({
|
|
2252
2352
|
...result,
|
|
2253
|
-
sourceFile: options.input || `draft version ${
|
|
2353
|
+
sourceFile: options.input || `draft version ${version}`,
|
|
2254
2354
|
}, repoRoot));
|
|
2255
2355
|
|
|
2256
2356
|
return {
|
|
@@ -2,6 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
const { readPhaseApproval } = require('../lib/approvals');
|
|
5
|
+
const { approvalCandidateCommand, buildApprovalCandidateReport, formatCandidateSummary } = require('../lib/ai/approval-candidates');
|
|
5
6
|
const { readPlanReview } = require('../lib/ai/plan-review');
|
|
6
7
|
const { listAgentProfiles } = require('../lib/agent-profiles');
|
|
7
8
|
const { readProjectScanStatus } = require('../lib/project-scan');
|
|
@@ -89,6 +90,25 @@ function safeReadPlanReview(projectRoot) {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
function safeBuildApprovalCandidateReport(projectRoot, phase) {
|
|
94
|
+
try {
|
|
95
|
+
return buildApprovalCandidateReport(projectRoot, phase);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
phase,
|
|
99
|
+
approval_status: 'invalid',
|
|
100
|
+
latest_version: null,
|
|
101
|
+
current: null,
|
|
102
|
+
recommended: null,
|
|
103
|
+
candidates: [],
|
|
104
|
+
history: [],
|
|
105
|
+
approved: null,
|
|
106
|
+
next_command: 'npx create-quiver ai approvals',
|
|
107
|
+
error: error.message,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
function summarizeDocs(projectRoot) {
|
|
93
113
|
const docs = {
|
|
94
114
|
hasProjectMap: exists(projectRoot, 'docs/PROJECT_MAP.md'),
|
|
@@ -250,11 +270,23 @@ function detectFlowState(projectRoot) {
|
|
|
250
270
|
acceptance: safeReadApproval(projectRoot, 'acceptance'),
|
|
251
271
|
technicalPlan: safeReadApproval(projectRoot, 'technical-plan'),
|
|
252
272
|
};
|
|
273
|
+
const approvalCandidates = {
|
|
274
|
+
acceptance: safeBuildApprovalCandidateReport(projectRoot, 'acceptance'),
|
|
275
|
+
technicalPlan: safeBuildApprovalCandidateReport(projectRoot, 'technical-plan'),
|
|
276
|
+
};
|
|
253
277
|
const planReview = safeReadPlanReview(projectRoot);
|
|
254
278
|
const agents = summarizeAgentProfiles(projectRoot);
|
|
255
279
|
const packageManager = detectPackageManager(projectRoot);
|
|
256
280
|
const specSlugs = listSpecSlugs(projectRoot);
|
|
257
281
|
const facts = buildFacts({ initialized, docs, approvals, planReview, agents, packageManager, specSlugs, state });
|
|
282
|
+
const acceptanceApprovalCommand = approvalCandidateCommand(
|
|
283
|
+
approvalCandidates.acceptance,
|
|
284
|
+
'npx create-quiver ai approve --phase acceptance --version <n>',
|
|
285
|
+
);
|
|
286
|
+
const technicalPlanApprovalCommand = approvalCandidateCommand(
|
|
287
|
+
approvalCandidates.technicalPlan,
|
|
288
|
+
'npx create-quiver ai approve --phase technical-plan --version <n>',
|
|
289
|
+
);
|
|
258
290
|
|
|
259
291
|
if (!initialized) {
|
|
260
292
|
return baseReport({
|
|
@@ -334,12 +366,12 @@ function detectFlowState(projectRoot) {
|
|
|
334
366
|
return baseReport({
|
|
335
367
|
stage: 'criteria-draft',
|
|
336
368
|
label: 'acceptance criteria need approval',
|
|
337
|
-
blockers: ['Acceptance criteria draft exists but is not approved.'],
|
|
338
|
-
nextCommand:
|
|
369
|
+
blockers: ['Acceptance criteria draft exists but is not approved.', formatCandidateSummary(approvalCandidates.acceptance.current)].filter(Boolean),
|
|
370
|
+
nextCommand: acceptanceApprovalCommand,
|
|
339
371
|
suggestedCommands: [
|
|
340
372
|
'npx create-quiver ai approvals',
|
|
341
373
|
'npx create-quiver ai revise --phase acceptance --input feedback.md --dry-run',
|
|
342
|
-
|
|
374
|
+
acceptanceApprovalCommand,
|
|
343
375
|
],
|
|
344
376
|
facts,
|
|
345
377
|
});
|
|
@@ -349,12 +381,12 @@ function detectFlowState(projectRoot) {
|
|
|
349
381
|
return baseReport({
|
|
350
382
|
stage: 'criteria-stale',
|
|
351
383
|
label: 'acceptance criteria approval is stale',
|
|
352
|
-
blockers: ['Acceptance criteria changed after approval.'],
|
|
353
|
-
nextCommand:
|
|
384
|
+
blockers: ['Acceptance criteria changed after approval.', formatCandidateSummary(approvalCandidates.acceptance.current)].filter(Boolean),
|
|
385
|
+
nextCommand: acceptanceApprovalCommand,
|
|
354
386
|
suggestedCommands: [
|
|
355
387
|
'npx create-quiver ai approvals',
|
|
356
388
|
'npx create-quiver ai revise --phase acceptance --input feedback.md --dry-run',
|
|
357
|
-
|
|
389
|
+
acceptanceApprovalCommand,
|
|
358
390
|
],
|
|
359
391
|
facts,
|
|
360
392
|
});
|
|
@@ -390,14 +422,29 @@ function detectFlowState(projectRoot) {
|
|
|
390
422
|
});
|
|
391
423
|
}
|
|
392
424
|
|
|
425
|
+
if (!approvalCandidates.technicalPlan.current?.approvable) {
|
|
426
|
+
return baseReport({
|
|
427
|
+
stage: 'technical-plan-review-blocked',
|
|
428
|
+
label: 'technical plan review requires revision',
|
|
429
|
+
blockers: ['Technical plan review blocks approval.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
|
|
430
|
+
nextCommand: approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
|
|
431
|
+
suggestedCommands: [
|
|
432
|
+
'npx create-quiver ai approvals',
|
|
433
|
+
approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
|
|
434
|
+
'npx create-quiver ai review-plan --dry-run',
|
|
435
|
+
],
|
|
436
|
+
facts,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
393
440
|
return baseReport({
|
|
394
441
|
stage: 'technical-plan-draft',
|
|
395
442
|
label: 'technical plan needs approval',
|
|
396
|
-
blockers: ['Technical plan draft was reviewed but is not approved.'],
|
|
397
|
-
nextCommand:
|
|
443
|
+
blockers: ['Technical plan draft was reviewed but is not approved.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
|
|
444
|
+
nextCommand: technicalPlanApprovalCommand,
|
|
398
445
|
suggestedCommands: [
|
|
399
446
|
'npx create-quiver ai approvals',
|
|
400
|
-
|
|
447
|
+
technicalPlanApprovalCommand,
|
|
401
448
|
],
|
|
402
449
|
facts,
|
|
403
450
|
});
|
|
@@ -419,14 +466,29 @@ function detectFlowState(projectRoot) {
|
|
|
419
466
|
});
|
|
420
467
|
}
|
|
421
468
|
|
|
469
|
+
if (!approvalCandidates.technicalPlan.current?.approvable) {
|
|
470
|
+
return baseReport({
|
|
471
|
+
stage: 'technical-plan-review-blocked',
|
|
472
|
+
label: 'technical plan review requires revision',
|
|
473
|
+
blockers: ['Technical plan changed after approval and latest review blocks approval.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
|
|
474
|
+
nextCommand: approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
|
|
475
|
+
suggestedCommands: [
|
|
476
|
+
'npx create-quiver ai approvals',
|
|
477
|
+
approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
|
|
478
|
+
'npx create-quiver ai review-plan --dry-run',
|
|
479
|
+
],
|
|
480
|
+
facts,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
422
484
|
return baseReport({
|
|
423
485
|
stage: 'technical-plan-stale',
|
|
424
486
|
label: 'technical plan approval is stale',
|
|
425
|
-
blockers: ['Technical plan changed after approval and the latest draft was reviewed.'],
|
|
426
|
-
nextCommand:
|
|
487
|
+
blockers: ['Technical plan changed after approval and the latest draft was reviewed.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
|
|
488
|
+
nextCommand: technicalPlanApprovalCommand,
|
|
427
489
|
suggestedCommands: [
|
|
428
490
|
'npx create-quiver ai approvals',
|
|
429
|
-
|
|
491
|
+
technicalPlanApprovalCommand,
|
|
430
492
|
],
|
|
431
493
|
facts,
|
|
432
494
|
});
|
|
@@ -7,6 +7,7 @@ const { selectOption } = require('../lib/cli/selectors');
|
|
|
7
7
|
const { createUx } = require('../lib/cli/ux');
|
|
8
8
|
const { parseJsonWithComments } = require('../lib/json');
|
|
9
9
|
const { assertPathInsideRoot, validateProjectRelativePaths } = require('../lib/paths');
|
|
10
|
+
const { buildApprovalCandidateReport, formatReviewSummary } = require('../lib/ai/approval-candidates');
|
|
10
11
|
const { resolveReviewedTechnicalPlanInput } = require('../lib/ai/plan-review');
|
|
11
12
|
const {
|
|
12
13
|
buildSpecGenerationManifest,
|
|
@@ -439,6 +440,11 @@ async function resolveInteractiveSpecCreateOptions(repoRoot, preview, options =
|
|
|
439
440
|
stdinIsTTY: options.stdinIsTTY,
|
|
440
441
|
stdoutIsTTY: options.stdoutIsTTY,
|
|
441
442
|
};
|
|
443
|
+
const technicalPlanCandidates = buildApprovalCandidateReport(repoRoot, 'technical-plan');
|
|
444
|
+
const approvedVersion = technicalPlanCandidates.approved?.version
|
|
445
|
+
? `v${technicalPlanCandidates.approved.version}`
|
|
446
|
+
: 'approved';
|
|
447
|
+
const reviewSummary = formatReviewSummary(technicalPlanCandidates.review);
|
|
442
448
|
const selectedMethodology = await selectOption('¿Qué metodología aplica esta spec?', SPEC_METHODOLOGY_OPTIONS, {
|
|
443
449
|
...selectorOptions,
|
|
444
450
|
defaultValue: 'wdd-sdd',
|
|
@@ -450,7 +456,7 @@ async function resolveInteractiveSpecCreateOptions(repoRoot, preview, options =
|
|
|
450
456
|
{
|
|
451
457
|
label: preview.inputPath,
|
|
452
458
|
value: preview.inputPath,
|
|
453
|
-
hint: 'Plan técnico revisado y aprobado',
|
|
459
|
+
hint: ['Plan técnico revisado y aprobado', approvedVersion, reviewSummary].filter(Boolean).join(', '),
|
|
454
460
|
default: true,
|
|
455
461
|
},
|
|
456
462
|
], {
|
|
@@ -484,6 +490,7 @@ async function resolveInteractiveSpecCreateOptions(repoRoot, preview, options =
|
|
|
484
490
|
{ label: 'Spec', value: preview.manifest.slug },
|
|
485
491
|
{ label: 'Metodologia', value: selectedMethodology.label },
|
|
486
492
|
{ label: 'Input', value: selectedInput.value },
|
|
493
|
+
{ label: 'Plan tecnico', value: ['aprobado', approvedVersion, reviewSummary].filter(Boolean).join(', ') },
|
|
487
494
|
{ label: 'Target', value: preview.relativeSpecDir },
|
|
488
495
|
{ label: 'Archivos planificados', value: preview.preview.files.length },
|
|
489
496
|
{ label: 'Revision', value: selectedReview.value === 'review' ? 'abrir preview' : 'confirmacion directa' },
|
|
@@ -525,6 +525,14 @@ function parseArgs(argv) {
|
|
|
525
525
|
|
|
526
526
|
const positional = [];
|
|
527
527
|
|
|
528
|
+
function missingInputValueMessage() {
|
|
529
|
+
const aiCommandHint = result.mode === 'ai' ? positional[0] : '';
|
|
530
|
+
if (aiCommandHint === 'revise') {
|
|
531
|
+
return `missing feedback input file for ai revise --phase ${result.aiPhase}. Use: npx create-quiver ai revise --phase ${result.aiPhase} --input <feedback.md> --dry-run`;
|
|
532
|
+
}
|
|
533
|
+
return 'missing value for --input';
|
|
534
|
+
}
|
|
535
|
+
|
|
528
536
|
for (let index = 0; index < args.length; index += 1) {
|
|
529
537
|
const arg = args[index];
|
|
530
538
|
|
|
@@ -875,8 +883,8 @@ function parseArgs(argv) {
|
|
|
875
883
|
|
|
876
884
|
if (arg === '--input') {
|
|
877
885
|
const value = args[++index];
|
|
878
|
-
if (!value) {
|
|
879
|
-
throw new Error(formatError(
|
|
886
|
+
if (!value || String(value).startsWith('--')) {
|
|
887
|
+
throw new Error(formatError(missingInputValueMessage()));
|
|
880
888
|
}
|
|
881
889
|
result.aiInput = value;
|
|
882
890
|
continue;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { buildPlannerApprovalCandidates } = require('../approvals');
|
|
2
|
+
const { buildTechnicalPlanApprovalCandidates } = require('./plan-review');
|
|
3
|
+
|
|
4
|
+
function buildApprovalCandidateReport(projectRoot, phase) {
|
|
5
|
+
return phase === 'technical-plan'
|
|
6
|
+
? buildTechnicalPlanApprovalCandidates(projectRoot)
|
|
7
|
+
: buildPlannerApprovalCandidates(projectRoot, phase);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formatReviewSummary(review) {
|
|
11
|
+
if (!review || !review.recommendation) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parts = [`review=${review.recommendation}`];
|
|
16
|
+
if (review.blocking) {
|
|
17
|
+
parts.push('blocking');
|
|
18
|
+
}
|
|
19
|
+
if (review.required_fixes_count) {
|
|
20
|
+
parts.push(`required fixes=${review.required_fixes_count}`);
|
|
21
|
+
}
|
|
22
|
+
if (review.optional_hardening_count) {
|
|
23
|
+
parts.push(`optional=${review.optional_hardening_count}`);
|
|
24
|
+
}
|
|
25
|
+
if (review.risks_count) {
|
|
26
|
+
parts.push(`risks=${review.risks_count}`);
|
|
27
|
+
}
|
|
28
|
+
return parts.join(', ');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatCandidateSummary(candidate) {
|
|
32
|
+
if (!candidate) {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parts = [
|
|
37
|
+
candidate.label,
|
|
38
|
+
candidate.current ? 'current' : 'history',
|
|
39
|
+
candidate.approvable ? 'approvable' : 'blocked',
|
|
40
|
+
candidate.reason,
|
|
41
|
+
formatReviewSummary(candidate.review),
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
return parts.join(', ');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function approvalCandidateCommand(report, fallback = 'npx create-quiver ai approvals') {
|
|
47
|
+
if (report?.recommended?.next_command) {
|
|
48
|
+
return report.recommended.next_command;
|
|
49
|
+
}
|
|
50
|
+
if (report?.current?.next_command) {
|
|
51
|
+
return report.current.next_command;
|
|
52
|
+
}
|
|
53
|
+
return report?.next_command || fallback;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatApprovalDecisionLines(report) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
if (!report || !Array.isArray(report.candidates)) {
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
lines.push(`Candidates: ${report.candidates.length}`);
|
|
63
|
+
if (report.latest_version) {
|
|
64
|
+
lines.push(`Latest draft: v${report.latest_version}`);
|
|
65
|
+
}
|
|
66
|
+
if (report.current) {
|
|
67
|
+
lines.push(`Current candidate: ${formatCandidateSummary(report.current)}`);
|
|
68
|
+
}
|
|
69
|
+
if (report.recommended) {
|
|
70
|
+
lines.push(`Recommended approval: ${report.recommended.next_command}`);
|
|
71
|
+
} else if (report.next_command) {
|
|
72
|
+
lines.push(`Recommended next command: ${report.next_command}`);
|
|
73
|
+
}
|
|
74
|
+
if (report.review?.status) {
|
|
75
|
+
lines.push(`Review status: ${report.review.status}`);
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
approvalCandidateCommand,
|
|
82
|
+
buildApprovalCandidateReport,
|
|
83
|
+
formatApprovalDecisionLines,
|
|
84
|
+
formatCandidateSummary,
|
|
85
|
+
formatReviewSummary,
|
|
86
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
|
|
4
|
-
const { readPhaseApproval, resolveApprovedPlannerInput } = require('../approvals');
|
|
4
|
+
const { buildPlannerApprovalCandidates, 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';
|
|
@@ -186,6 +186,81 @@ function reviewBlocksApproval(review) {
|
|
|
186
186
|
return result.blocking === true || result.approval_recommendation === 'revise';
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
function normalizeReviewDecision(review) {
|
|
190
|
+
const result = review?.meta?.review_result || null;
|
|
191
|
+
const requiredFixes = normalizeList(result?.required_fixes);
|
|
192
|
+
const optionalHardening = normalizeList(result?.optional_hardening);
|
|
193
|
+
const risks = normalizeList(result?.risks);
|
|
194
|
+
const blocking = reviewBlocksApproval(review);
|
|
195
|
+
const recommendation = result?.approval_recommendation || (review.status === 'missing' || review.status === 'stale' ? 'review-required' : 'approve-with-risk');
|
|
196
|
+
const nextCommand = result?.next_command || (review.status === 'unapproved' || review.status === 'reviewed'
|
|
197
|
+
? 'npx create-quiver ai approve --phase technical-plan --version <n>'
|
|
198
|
+
: 'npx create-quiver ai review-plan --dry-run');
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
status: review.status,
|
|
202
|
+
recommendation,
|
|
203
|
+
blocking: blocking || review.status === 'missing' || review.status === 'stale',
|
|
204
|
+
required_fixes: requiredFixes,
|
|
205
|
+
optional_hardening: optionalHardening,
|
|
206
|
+
risks,
|
|
207
|
+
required_fixes_count: requiredFixes.length,
|
|
208
|
+
optional_hardening_count: optionalHardening.length,
|
|
209
|
+
risks_count: risks.length,
|
|
210
|
+
source_file: review.meta?.source_file || '',
|
|
211
|
+
source_kind: review.meta?.source_kind || null,
|
|
212
|
+
source_version: review.meta?.source_version || null,
|
|
213
|
+
reviewed_at: review.meta?.reviewed_at || '',
|
|
214
|
+
next_command: nextCommand,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function buildTechnicalPlanApprovalCandidates(projectRoot) {
|
|
219
|
+
const base = buildPlannerApprovalCandidates(projectRoot, 'technical-plan');
|
|
220
|
+
const review = readPlanReview(projectRoot);
|
|
221
|
+
const reviewDecision = normalizeReviewDecision(review);
|
|
222
|
+
const reviewReady = (review.status === 'unapproved' || review.status === 'reviewed') && reviewDecision.blocking !== true;
|
|
223
|
+
const candidates = base.candidates.map((candidate) => {
|
|
224
|
+
const matchesReviewVersion = !reviewDecision.source_version
|
|
225
|
+
|| !candidate.version
|
|
226
|
+
|| Number(reviewDecision.source_version) === Number(candidate.version);
|
|
227
|
+
const approvable = candidate.approvable === true && reviewReady && matchesReviewVersion;
|
|
228
|
+
const reason = approvable
|
|
229
|
+
? `latest draft has plan-review recommendation ${reviewDecision.recommendation}`
|
|
230
|
+
: review.status === 'missing'
|
|
231
|
+
? 'technical-plan requires production review before approval'
|
|
232
|
+
: review.status === 'stale'
|
|
233
|
+
? 'technical-plan review is stale'
|
|
234
|
+
: reviewDecision.blocking
|
|
235
|
+
? `plan-review blocks approval with recommendation ${reviewDecision.recommendation}`
|
|
236
|
+
: matchesReviewVersion
|
|
237
|
+
? candidate.reason
|
|
238
|
+
: `plan-review targets v${reviewDecision.source_version}; candidate is v${candidate.version}`;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
...candidate,
|
|
242
|
+
approvable,
|
|
243
|
+
recommended: candidate.current === true && approvable,
|
|
244
|
+
blocked: !approvable,
|
|
245
|
+
status: approvable ? 'approvable' : candidate.current ? 'blocked' : 'history',
|
|
246
|
+
reason,
|
|
247
|
+
recommended_action: approvable ? 'approve' : reviewDecision.blocking || review.status === 'missing' || review.status === 'stale' ? 'review-or-revise' : 'inspect',
|
|
248
|
+
next_command: approvable ? candidate.next_command : reviewDecision.next_command,
|
|
249
|
+
review: reviewDecision,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...base,
|
|
255
|
+
candidates,
|
|
256
|
+
current: candidates.find((candidate) => candidate.current) || null,
|
|
257
|
+
recommended: candidates.find((candidate) => candidate.recommended) || null,
|
|
258
|
+
history: candidates.filter((candidate) => !candidate.current),
|
|
259
|
+
review: reviewDecision,
|
|
260
|
+
next_command: candidates.find((candidate) => candidate.recommended)?.next_command || reviewDecision.next_command,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
189
264
|
function latestTechnicalPlanDraft(approval) {
|
|
190
265
|
const version = Number(approval.meta?.draft?.version || 0);
|
|
191
266
|
if (!version) {
|
|
@@ -435,6 +510,7 @@ module.exports = {
|
|
|
435
510
|
PLAN_REVIEW_RECOMMENDATIONS,
|
|
436
511
|
assertPlanReviewed,
|
|
437
512
|
buildPlanReviewPrompt,
|
|
513
|
+
buildTechnicalPlanApprovalCandidates,
|
|
438
514
|
derivePlanReviewResult,
|
|
439
515
|
planReviewMetaPath,
|
|
440
516
|
planReviewPath,
|
|
@@ -2,6 +2,7 @@ const fs = require('node:fs');
|
|
|
2
2
|
const os = require('node:os');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
|
|
5
|
+
const { buildApprovalCandidateReport } = require('./approval-candidates');
|
|
5
6
|
const { quiverInternalPaths } = require('../init-layout');
|
|
6
7
|
|
|
7
8
|
const AI_RUN_PHASES = Object.freeze([
|
|
@@ -104,8 +105,24 @@ function phaseRank(phase) {
|
|
|
104
105
|
return AI_RUN_PHASES.indexOf(phase);
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
function nextCommandForPhase(phase) {
|
|
108
|
+
function nextCommandForPhase(phase, projectRoot = '') {
|
|
108
109
|
assertKnownPhase(phase);
|
|
110
|
+
if (projectRoot && phase === 'acceptance-draft') {
|
|
111
|
+
try {
|
|
112
|
+
const report = buildApprovalCandidateReport(projectRoot, 'acceptance');
|
|
113
|
+
return report.recommended?.next_command || report.current?.next_command || PHASE_NEXT_COMMAND[phase];
|
|
114
|
+
} catch {
|
|
115
|
+
return PHASE_NEXT_COMMAND[phase];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (projectRoot && phase === 'technical-plan-reviewed') {
|
|
119
|
+
try {
|
|
120
|
+
const report = buildApprovalCandidateReport(projectRoot, 'technical-plan');
|
|
121
|
+
return report.recommended?.next_command || report.current?.next_command || PHASE_NEXT_COMMAND[phase];
|
|
122
|
+
} catch {
|
|
123
|
+
return PHASE_NEXT_COMMAND[phase];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
109
126
|
return PHASE_NEXT_COMMAND[phase];
|
|
110
127
|
}
|
|
111
128
|
|
|
@@ -374,12 +391,12 @@ function formatAiRunStatus(projectRoot, run) {
|
|
|
374
391
|
if (otherOpenRuns.length > 0) {
|
|
375
392
|
lines.push('Other open runs:');
|
|
376
393
|
for (const item of otherOpenRuns) {
|
|
377
|
-
lines.push(`- ${item.run_id}: ${item.phase} (${item.status}) -> ${nextCommandForPhase(item.phase)}`);
|
|
394
|
+
lines.push(`- ${item.run_id}: ${item.phase} (${item.status}) -> ${nextCommandForPhase(item.phase, projectRoot)}`);
|
|
378
395
|
}
|
|
379
396
|
}
|
|
380
397
|
|
|
381
398
|
lines.push(
|
|
382
|
-
`Next safe command: ${nextCommandForPhase(run.phase)}`,
|
|
399
|
+
`Next safe command: ${nextCommandForPhase(run.phase, projectRoot)}`,
|
|
383
400
|
'',
|
|
384
401
|
);
|
|
385
402
|
|
|
@@ -400,7 +417,7 @@ function formatAiRunResume(projectRoot, run) {
|
|
|
400
417
|
'AI run resume',
|
|
401
418
|
`Run: ${run.run_id}`,
|
|
402
419
|
`Current phase: ${run.phase}`,
|
|
403
|
-
`Next safe command: ${nextCommandForPhase(run.phase)}`,
|
|
420
|
+
`Next safe command: ${nextCommandForPhase(run.phase, projectRoot)}`,
|
|
404
421
|
`State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
|
|
405
422
|
'',
|
|
406
423
|
].join('\n');
|