create-quiver 0.13.0 → 0.14.1
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 +30 -0
- package/README.md +183 -511
- package/README_FOR_AI.md +36 -26
- package/ROADMAP.md +11 -0
- package/assets/quiver-wordmark.svg +22 -0
- package/docs/AI_CONTEXT.md.template +2 -0
- package/docs/AI_ONBOARDING_PROMPT.md.template +9 -1
- package/docs/CLI_UX_GUIDE.md +125 -0
- package/docs/COMMANDS.md.template +19 -4
- package/docs/GITFLOW_PR_GUIDE.md +70 -0
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- package/docs/getting-started/linux.md +84 -0
- package/docs/getting-started/macos.md +85 -0
- package/docs/getting-started/windows-git-bash-wsl.md +78 -0
- package/docs/getting-started/windows-powershell.md +96 -0
- package/docs/reference/commands.md +94 -0
- package/docs/workflows/existing-project.md +131 -0
- package/docs/workflows/full-ai-spec-to-pr.md +311 -0
- package/docs/workflows/legacy-quiver-project.md +102 -0
- package/docs/workflows/new-project.md +76 -0
- package/package.json +5 -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/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
- package/src/create-quiver/commands/ai.js +938 -16
- package/src/create-quiver/commands/spec.js +74 -2
- package/src/create-quiver/index.js +91 -5
- package/src/create-quiver/lib/ai/context-packs.js +2 -2
- package/src/create-quiver/lib/ai/context-proposal.js +389 -0
- package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
- 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/cli/editor.js +118 -0
- package/src/create-quiver/lib/cli/theme.js +100 -0
- package/src/create-quiver/lib/cli/ux-flags.js +151 -0
- package/src/create-quiver/lib/cli/ux.js +130 -0
- package/src/create-quiver/lib/project-state-resolver.js +195 -1
- package/src/create-quiver/lib/spec-worktrees.js +50 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
2
3
|
const path = require('node:path');
|
|
3
4
|
|
|
4
5
|
const { redactSecrets } = require('../lib/evidence');
|
|
@@ -10,6 +11,9 @@ const {
|
|
|
10
11
|
writeRawProviderArtifact,
|
|
11
12
|
} = require('../lib/ai/artifacts');
|
|
12
13
|
const { buildContextPackMetadata, normalizeRole } = require('../lib/ai/context-packs');
|
|
14
|
+
const { parseContextProposalOutput } = require('../lib/ai/context-proposal');
|
|
15
|
+
const { openEditor } = require('../lib/cli/editor');
|
|
16
|
+
const { createUx } = require('../lib/cli/ux');
|
|
13
17
|
const { runExecuteSlice, runPromptSlice } = require('../lib/ai/executor');
|
|
14
18
|
const { runExecutePlan } = require('../lib/ai/execution-plan');
|
|
15
19
|
const { buildPrCreatePlan, formatPreflightReport, formatPrCreateReport, preflightGitHubPr, runGhPrCreate } = require('../lib/ai/github');
|
|
@@ -26,18 +30,25 @@ const {
|
|
|
26
30
|
PLAN_REVIEW_PROMPT_SOURCE,
|
|
27
31
|
buildPlanReviewPrompt,
|
|
28
32
|
readPlanReview,
|
|
33
|
+
reviewBlocksApproval,
|
|
29
34
|
resolveReviewedTechnicalPlanInput,
|
|
30
35
|
resolveTechnicalPlanReviewInput,
|
|
31
36
|
savePlanReview,
|
|
32
37
|
summarizePlanReview,
|
|
33
38
|
} = require('../lib/ai/plan-review');
|
|
34
|
-
const {
|
|
39
|
+
const {
|
|
40
|
+
buildSpecGenerationManifest,
|
|
41
|
+
describeSpecGeneration,
|
|
42
|
+
generateSpecArtifacts,
|
|
43
|
+
validateTechnicalPlanSpecContract,
|
|
44
|
+
} = require('../lib/ai/spec-generator');
|
|
35
45
|
const { buildProviderInvocation, runProvider } = require('../lib/ai/providers');
|
|
36
46
|
const {
|
|
37
47
|
createAiRun,
|
|
38
48
|
ensureAiRun,
|
|
39
49
|
formatAiRunResume,
|
|
40
50
|
formatAiRunStatus,
|
|
51
|
+
listAiRuns,
|
|
41
52
|
recordAiRunApproval,
|
|
42
53
|
resolveAiRun,
|
|
43
54
|
updateAiRunPhase,
|
|
@@ -53,12 +64,15 @@ const {
|
|
|
53
64
|
const {
|
|
54
65
|
PLANNER_APPROVAL_PHASES,
|
|
55
66
|
approvePlannerPhase,
|
|
67
|
+
findDraftVersion,
|
|
68
|
+
latestDraftVersion,
|
|
56
69
|
readPhaseApproval,
|
|
57
70
|
resolveApprovedPlannerInput,
|
|
58
71
|
savePlannerDraft,
|
|
59
72
|
summarizePlannerApproval,
|
|
60
73
|
} = require('../lib/approvals');
|
|
61
74
|
const { assertPlannerPhaseReady, getPlannerPhaseDetails, normalizePlannerPhase, PlannerPhaseError } = require('../lib/ai/phase-gates');
|
|
75
|
+
const { collectActiveSliceState, resolveProjectState } = require('../lib/project-state-resolver');
|
|
62
76
|
|
|
63
77
|
const DEFAULT_ONBOARD_PROVIDER = 'codex';
|
|
64
78
|
const DEFAULT_ONBOARD_ROLE = 'planner';
|
|
@@ -133,6 +147,13 @@ function buildPlanContext({ role, context, phase, inputText, inputPath, repoRoot
|
|
|
133
147
|
: 'Task: produce a technical plan only. Do not create files or modify product code.',
|
|
134
148
|
];
|
|
135
149
|
|
|
150
|
+
if (phaseDetails.phase === 'technical-plan') {
|
|
151
|
+
sections.push(
|
|
152
|
+
'Required output contract: include a fenced json block with `{ "spec": { "slices": [...] } }` so Quiver can create specs after review and approval.',
|
|
153
|
+
'Each `spec.slices[]` item must include at least `slice_id`, `title`, `objective`, and `files`.',
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
136
157
|
if (relativeInputPath) {
|
|
137
158
|
sections.push(`Input file: ${relativeInputPath}`);
|
|
138
159
|
}
|
|
@@ -296,6 +317,255 @@ function formatContextPreparationReport({ dryRun, plan, writePlan, writtenDocs,
|
|
|
296
317
|
return `${lines.join('\n')}\n`;
|
|
297
318
|
}
|
|
298
319
|
|
|
320
|
+
function truncatePromptSection(text, maxChars = 1200) {
|
|
321
|
+
const value = String(text || '').trimEnd();
|
|
322
|
+
if (value.length <= maxChars) {
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
return `${value.slice(0, maxChars).trimEnd()}\n[... truncated ${value.length - maxChars} chars ...]`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildPrepareContextPlannerPrompt({ pack, draftPack }) {
|
|
329
|
+
const plan = draftPack.plan;
|
|
330
|
+
const allowedPaths = draftPack.docs.map((doc) => doc.path);
|
|
331
|
+
const sections = [
|
|
332
|
+
pack.prompt,
|
|
333
|
+
'Task: planner-assisted Quiver context preparation.',
|
|
334
|
+
'Goal: improve the docs-only onboarding context for future AI work while preserving WDD + SDD safety.',
|
|
335
|
+
'Rules:',
|
|
336
|
+
'- Return only valid JSON. Do not include Markdown outside the JSON object.',
|
|
337
|
+
'- Do not modify product code, UI code, tests, migrations, dependencies, lockfiles, build files, runtime config, generated files, or paths outside the repo.',
|
|
338
|
+
'- Only propose writes to the allowed docs paths listed below.',
|
|
339
|
+
'- If information is ambiguous, document assumptions and risks instead of inventing facts.',
|
|
340
|
+
'- Keep human-authored content safe; Quiver will merge proposals through managed context blocks.',
|
|
341
|
+
'',
|
|
342
|
+
'Allowed docs-only output paths:',
|
|
343
|
+
...allowedPaths.map((item) => `- ${item}`),
|
|
344
|
+
'',
|
|
345
|
+
'Project context:',
|
|
346
|
+
`- Project: ${plan.projectName}`,
|
|
347
|
+
`- Project slug: ${plan.projectSlug}`,
|
|
348
|
+
`- Package manager: ${plan.facts.packageManager}`,
|
|
349
|
+
`- Stack summary: ${plan.facts.stackSummary}`,
|
|
350
|
+
'',
|
|
351
|
+
'Files considered by deterministic prepare-context:',
|
|
352
|
+
...plan.filesConsidered.map((item) => `- ${item.path}: ${item.present ? 'present' : 'absent'}${item.reason ? ` (${item.reason})` : ''}`),
|
|
353
|
+
'',
|
|
354
|
+
'Known assumptions:',
|
|
355
|
+
...formatPathList(plan.assumptions),
|
|
356
|
+
'',
|
|
357
|
+
'Known risks:',
|
|
358
|
+
...formatPathList(plan.risks),
|
|
359
|
+
'',
|
|
360
|
+
'Known contradictions:',
|
|
361
|
+
...formatPathList(plan.contradictions),
|
|
362
|
+
'',
|
|
363
|
+
'Deterministic candidate docs:',
|
|
364
|
+
...draftPack.docs.flatMap((doc) => [
|
|
365
|
+
`### ${doc.path}`,
|
|
366
|
+
truncatePromptSection(doc.content),
|
|
367
|
+
]),
|
|
368
|
+
'',
|
|
369
|
+
'Required JSON output shape:',
|
|
370
|
+
JSON.stringify({
|
|
371
|
+
schema_version: 1,
|
|
372
|
+
kind: 'quiver-context-proposal',
|
|
373
|
+
summary: 'short summary',
|
|
374
|
+
assumptions: ['assumption to confirm'],
|
|
375
|
+
risks: ['risk to track'],
|
|
376
|
+
docs: [
|
|
377
|
+
{
|
|
378
|
+
path: 'docs/AI_CONTEXT.md',
|
|
379
|
+
action: 'update',
|
|
380
|
+
reason: 'why this doc should change',
|
|
381
|
+
content: '# AI Context\\n\\nFull proposed content or managed section content.\\n',
|
|
382
|
+
assumptions: [],
|
|
383
|
+
risks: [],
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
omitted_paths: ['paths intentionally omitted'],
|
|
387
|
+
next_steps: ['safe next step'],
|
|
388
|
+
}, null, 2),
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
allowedPaths,
|
|
393
|
+
plan,
|
|
394
|
+
prompt: sections.join('\n'),
|
|
395
|
+
promptSource: 'quiver prepare-context planner proposal contract',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function formatPrepareContextPlannerDryRunReport({ provider, role, context, invocation, promptInfo, review, interactive }) {
|
|
400
|
+
const plan = promptInfo.plan;
|
|
401
|
+
const lines = [
|
|
402
|
+
'AI prepare-context planner dry-run',
|
|
403
|
+
'Mode: dry-run',
|
|
404
|
+
'Planner: enabled',
|
|
405
|
+
`Provider: ${provider}`,
|
|
406
|
+
`Role: ${role}`,
|
|
407
|
+
`Context pack: ${context}`,
|
|
408
|
+
`Command: ${invocation.command} ${invocation.args.join(' ')}`.trim(),
|
|
409
|
+
`Prompt bytes: ${invocation.promptLength}`,
|
|
410
|
+
`Prompt source: ${promptInfo.promptSource}`,
|
|
411
|
+
`Review requested: ${review ? 'yes' : 'no'}`,
|
|
412
|
+
`Interactive requested: ${interactive ? 'yes' : 'no'}`,
|
|
413
|
+
'Provider execution: skipped',
|
|
414
|
+
'Writes: none',
|
|
415
|
+
'Product code: untouched',
|
|
416
|
+
`Candidate docs: ${promptInfo.allowedPaths.join(', ')}`,
|
|
417
|
+
'Files considered:',
|
|
418
|
+
...plan.filesConsidered.map((item) => `- ${item.path}: ${item.present ? 'present' : 'absent'}`),
|
|
419
|
+
'Allowed docs-only paths:',
|
|
420
|
+
...promptInfo.allowedPaths.map((item) => `- ${item}`),
|
|
421
|
+
'Next safe commands:',
|
|
422
|
+
'- npx create-quiver ai prepare-context --with-planner --print-prompt',
|
|
423
|
+
'- npx create-quiver ai prepare-context --with-planner --dry-run --review',
|
|
424
|
+
'- npx create-quiver ai prepare-context --with-planner',
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
return `${lines.join('\n')}\n`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function serializeProposalForReview(proposal) {
|
|
431
|
+
return {
|
|
432
|
+
schema_version: proposal.schemaVersion,
|
|
433
|
+
kind: proposal.kind,
|
|
434
|
+
summary: proposal.summary,
|
|
435
|
+
assumptions: proposal.assumptions,
|
|
436
|
+
risks: proposal.risks,
|
|
437
|
+
docs: proposal.docs.map((doc) => ({
|
|
438
|
+
path: doc.path,
|
|
439
|
+
action: doc.action,
|
|
440
|
+
reason: doc.reason,
|
|
441
|
+
content: doc.content,
|
|
442
|
+
assumptions: doc.assumptions,
|
|
443
|
+
risks: doc.risks,
|
|
444
|
+
})),
|
|
445
|
+
omitted_paths: proposal.omittedPaths,
|
|
446
|
+
next_steps: proposal.nextSteps,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function createProposalReviewFile(proposal, options = {}) {
|
|
451
|
+
const reviewDir = options.reviewDir || fs.mkdtempSync(path.join(os.tmpdir(), 'quiver-context-review-'));
|
|
452
|
+
const reviewPath = path.join(reviewDir, 'context-proposal.json');
|
|
453
|
+
fs.mkdirSync(reviewDir, { recursive: true });
|
|
454
|
+
fs.writeFileSync(reviewPath, `${JSON.stringify(serializeProposalForReview(proposal), null, 2)}\n`);
|
|
455
|
+
return reviewPath;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function makeReviewError(message, reviewPath, cause) {
|
|
459
|
+
const error = new Error(formatError(`${message}\nReview artifact: ${reviewPath}\nNext safe step: edit the artifact into valid proposal JSON or rerun with --with-planner --dry-run.`));
|
|
460
|
+
error.code = cause?.code || 'AI_CONTEXT_REVIEW_FAILED';
|
|
461
|
+
error.cause = cause;
|
|
462
|
+
error.reviewPath = reviewPath;
|
|
463
|
+
return error;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function createReviewTextFile(contents, options = {}) {
|
|
467
|
+
const reviewDir = options.reviewDir || fs.mkdtempSync(path.join(os.tmpdir(), 'quiver-review-'));
|
|
468
|
+
const filename = options.reviewFileName || 'review.md';
|
|
469
|
+
const reviewPath = path.join(reviewDir, filename);
|
|
470
|
+
fs.mkdirSync(reviewDir, { recursive: true });
|
|
471
|
+
fs.writeFileSync(reviewPath, String(contents || ''));
|
|
472
|
+
return reviewPath;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function reviewTextWithEditor(repoRoot, contents, options = {}) {
|
|
476
|
+
const reviewPath = createReviewTextFile(contents, options);
|
|
477
|
+
const hasEditorRunner = typeof options.openEditorFn === 'function';
|
|
478
|
+
const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
|
|
479
|
+
|
|
480
|
+
if (!canOpenEditor) {
|
|
481
|
+
throw makeReviewError(`${options.reviewLabel || 'review'} requires an interactive terminal or an injected editor runner.`, reviewPath);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const editorResult = hasEditorRunner
|
|
485
|
+
? options.openEditorFn(reviewPath, { cwd: repoRoot, env: options.env || process.env })
|
|
486
|
+
: openEditor(reviewPath, { cwd: repoRoot, env: options.env || process.env });
|
|
487
|
+
|
|
488
|
+
if (!editorResult || editorResult.ok !== true) {
|
|
489
|
+
throw makeReviewError(editorResult?.reason || `${options.reviewLabel || 'review'} was canceled.`, reviewPath);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
reviewPath,
|
|
494
|
+
text: fs.readFileSync(reviewPath, 'utf8'),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function confirmInteractiveAction(message, options = {}) {
|
|
499
|
+
if (options.interactive !== true) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const ux = options.ux || createUx({
|
|
504
|
+
interactive: true,
|
|
505
|
+
promptConfirm: options.promptConfirm,
|
|
506
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
507
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
508
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
509
|
+
write: options.write,
|
|
510
|
+
});
|
|
511
|
+
const confirmed = await ux.promptConfirm(message, {
|
|
512
|
+
initialValue: false,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
if (!confirmed) {
|
|
516
|
+
const error = new Error(formatError('interactive approval declined. No files were written.'));
|
|
517
|
+
error.code = 'AI_INTERACTIVE_APPROVAL_DECLINED';
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function reviewPlannerContextProposal(repoRoot, proposal, options = {}) {
|
|
523
|
+
const reviewPath = createProposalReviewFile(proposal, options);
|
|
524
|
+
const hasEditorRunner = typeof options.openEditorFn === 'function';
|
|
525
|
+
const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
|
|
526
|
+
|
|
527
|
+
if (!canOpenEditor) {
|
|
528
|
+
throw makeReviewError('ai prepare-context review requires an interactive terminal or an injected editor runner.', reviewPath);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const editorResult = hasEditorRunner
|
|
532
|
+
? options.openEditorFn(reviewPath, { cwd: repoRoot, env: options.env || process.env })
|
|
533
|
+
: openEditor(reviewPath, { cwd: repoRoot, env: options.env || process.env });
|
|
534
|
+
|
|
535
|
+
if (!editorResult || editorResult.ok !== true) {
|
|
536
|
+
throw makeReviewError(editorResult?.reason || 'ai prepare-context review was canceled before applying docs.', reviewPath);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
return {
|
|
541
|
+
proposal: parseContextProposalOutput(fs.readFileSync(reviewPath, 'utf8')),
|
|
542
|
+
reviewPath,
|
|
543
|
+
};
|
|
544
|
+
} catch (error) {
|
|
545
|
+
throw makeReviewError('edited planner proposal is invalid after review.', reviewPath, error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function confirmPlannerContextWrites(writePlan, options = {}) {
|
|
550
|
+
const changed = writePlan.filter((item) => item.action !== 'skip').length;
|
|
551
|
+
await confirmInteractiveAction(`Apply ${changed} docs-only context update${changed === 1 ? '' : 's'}?`, options);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function buildPlannerContextWritePlan(repoRoot, proposal) {
|
|
555
|
+
const reasonByPath = new Map(proposal.docs.map((doc) => [doc.path, doc.reason]));
|
|
556
|
+
const draftDocs = proposal.docs
|
|
557
|
+
.filter((doc) => doc.action !== 'skip')
|
|
558
|
+
.map((doc) => ({
|
|
559
|
+
path: doc.path,
|
|
560
|
+
content: doc.content,
|
|
561
|
+
}));
|
|
562
|
+
|
|
563
|
+
return buildContextWritePlan(repoRoot, draftDocs).map((item) => ({
|
|
564
|
+
...item,
|
|
565
|
+
reason: item.action === 'skip' ? item.reason : reasonByPath.get(item.path) || item.reason,
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
|
|
299
569
|
function writeProviderOutput(result) {
|
|
300
570
|
if (result.stdout) {
|
|
301
571
|
process.stdout.write(redactSecrets(result.stdout));
|
|
@@ -305,6 +575,14 @@ function writeProviderOutput(result) {
|
|
|
305
575
|
}
|
|
306
576
|
}
|
|
307
577
|
|
|
578
|
+
function writeCleanProviderOutput(clean) {
|
|
579
|
+
const output = String(clean?.cleanOutput || '');
|
|
580
|
+
if (!output) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
process.stdout.write(output.endsWith('\n') ? output : `${output}\n`);
|
|
584
|
+
}
|
|
585
|
+
|
|
308
586
|
function normalizeText(value) {
|
|
309
587
|
return String(value || '').replace(/\r\n/g, '\n');
|
|
310
588
|
}
|
|
@@ -548,10 +826,262 @@ function formatApprovalDryRunResult({ phase, input, version }) {
|
|
|
548
826
|
return `${lines.join('\n')}\n`;
|
|
549
827
|
}
|
|
550
828
|
|
|
829
|
+
function stripCreateQuiverPrefix(message) {
|
|
830
|
+
return String(message || '').replace(/^create-quiver:\s*/, '');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function readCurrentDraftForApproval(repoRoot, phase, version) {
|
|
834
|
+
const approval = readPhaseApproval(repoRoot, phase);
|
|
835
|
+
const selectedDraft = findDraftVersion(approval.meta, version);
|
|
836
|
+
if (!selectedDraft) {
|
|
837
|
+
throw new Error(formatError(`missing ${phase} draft version ${version}`));
|
|
838
|
+
}
|
|
839
|
+
const latestVersion = latestDraftVersion(approval.meta);
|
|
840
|
+
if (latestVersion && Number(selectedDraft.version) !== latestVersion) {
|
|
841
|
+
throw new Error(formatError(`${phase} draft version ${version} is not current; latest draft version is ${latestVersion}. Approve the latest version or revise again.`));
|
|
842
|
+
}
|
|
843
|
+
const draftPath = path.resolve(repoRoot, selectedDraft.path);
|
|
844
|
+
if (!fs.existsSync(draftPath)) {
|
|
845
|
+
throw new Error(formatError(`missing ${phase} draft artifact: ${selectedDraft.path}`));
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
approval,
|
|
849
|
+
contents: fs.readFileSync(draftPath, 'utf8'),
|
|
850
|
+
draft: selectedDraft,
|
|
851
|
+
path: selectedDraft.path,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function assertTechnicalPlanDraftHasSpecContract(repoRoot, version) {
|
|
856
|
+
const draft = readCurrentDraftForApproval(repoRoot, 'technical-plan', version);
|
|
857
|
+
try {
|
|
858
|
+
validateTechnicalPlanSpecContract(repoRoot, {
|
|
859
|
+
inputPath: draft.path,
|
|
860
|
+
inputText: draft.contents,
|
|
861
|
+
});
|
|
862
|
+
} catch (error) {
|
|
863
|
+
throw new Error(formatError([
|
|
864
|
+
`technical-plan draft v${version} cannot be approved because it cannot create specs.`,
|
|
865
|
+
stripCreateQuiverPrefix(error.message || error),
|
|
866
|
+
'Required contract: include a structured JSON block with `spec.slices[]` before approval.',
|
|
867
|
+
'Next safe command: npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
|
|
868
|
+
].join('\n')));
|
|
869
|
+
}
|
|
870
|
+
return draft;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function resolveApprovedTechnicalPlanForRepair(repoRoot, explicitInput = '') {
|
|
874
|
+
const approval = readPhaseApproval(repoRoot, 'technical-plan');
|
|
875
|
+
if (!approval.approved?.path) {
|
|
876
|
+
throw new Error(formatError('ai repair-plan requires an approved technical-plan artifact. Run `npx create-quiver ai approvals` to inspect planner state.'));
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const approvedPath = approval.approved.path;
|
|
880
|
+
if (explicitInput) {
|
|
881
|
+
const explicit = path.resolve(repoRoot, explicitInput);
|
|
882
|
+
const approved = path.resolve(repoRoot, approvedPath);
|
|
883
|
+
if (explicit !== approved) {
|
|
884
|
+
throw new Error(formatError(`ai repair-plan input must match the approved technical-plan artifact: ${approvedPath}`));
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const contents = readTextFile(approvedPath, repoRoot);
|
|
889
|
+
try {
|
|
890
|
+
validateTechnicalPlanSpecContract(repoRoot, {
|
|
891
|
+
inputPath: approvedPath,
|
|
892
|
+
inputText: contents,
|
|
893
|
+
});
|
|
894
|
+
} catch (error) {
|
|
895
|
+
return {
|
|
896
|
+
approval,
|
|
897
|
+
contents,
|
|
898
|
+
path: approvedPath,
|
|
899
|
+
validationError: stripCreateQuiverPrefix(error.message || error),
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
throw new Error(formatError('approved technical-plan already includes a valid structured `spec.slices[]` contract. No repair draft is needed.'));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function buildRepairPlanContext({ context, inputText, inputPath, repoRoot, role, validationError }) {
|
|
907
|
+
const pack = buildContextPackMetadata({
|
|
908
|
+
role,
|
|
909
|
+
packName: context,
|
|
910
|
+
repoRoot,
|
|
911
|
+
});
|
|
912
|
+
const prompt = [
|
|
913
|
+
pack.prompt,
|
|
914
|
+
'Phase: technical-plan',
|
|
915
|
+
'Task: repair the approved technical plan into a new draft only. Do not approve it, create specs, modify product code, or expand scope.',
|
|
916
|
+
'Preserve the approved intent, scope, risks, and decisions.',
|
|
917
|
+
'Add the required Quiver structured JSON contract in a fenced json block.',
|
|
918
|
+
'The JSON must include `{ "spec": { "slug": "...", "title": "...", "objective": "...", "slices": [...] } }`.',
|
|
919
|
+
'Each item in `spec.slices[]` must include at least `slice_id`, `title`, `objective`, and `files`.',
|
|
920
|
+
`Validation failure to repair: ${validationError}`,
|
|
921
|
+
`Approved technical-plan artifact: ${inputPath}`,
|
|
922
|
+
'Approved technical-plan contents:',
|
|
923
|
+
inputText.trimEnd(),
|
|
924
|
+
].join('\n\n');
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
pack,
|
|
928
|
+
prompt,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function formatRepairPlanResult(result, repoRoot) {
|
|
933
|
+
const relativePath = path.relative(repoRoot, result.filePath).split(path.sep).join('/');
|
|
934
|
+
return [
|
|
935
|
+
'AI technical-plan repair draft saved',
|
|
936
|
+
`Draft: ${relativePath}`,
|
|
937
|
+
`Version: v${result.version}`,
|
|
938
|
+
`Source approved artifact: ${result.sourcePath}`,
|
|
939
|
+
'Original approved artifact: preserved',
|
|
940
|
+
'Next safe commands:',
|
|
941
|
+
'- npx create-quiver ai review-plan --dry-run',
|
|
942
|
+
'- npx create-quiver ai review-plan',
|
|
943
|
+
`- npx create-quiver ai approve --phase technical-plan --version ${result.version}`,
|
|
944
|
+
].join('\n').concat('\n');
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function formatActiveSliceReconciliationReport(report, options = {}) {
|
|
948
|
+
const lines = [
|
|
949
|
+
'AI active-slice reconciliation',
|
|
950
|
+
`Mode: ${options.dryRun ? 'dry-run' : 'read-only'}`,
|
|
951
|
+
`Decision: ${report.reconciliation.decision}`,
|
|
952
|
+
`Reason: ${report.reconciliation.reason}`,
|
|
953
|
+
'',
|
|
954
|
+
'Supported sources:',
|
|
955
|
+
];
|
|
956
|
+
|
|
957
|
+
for (const source of report.supported_sources) {
|
|
958
|
+
lines.push(`- ${source.path}: ${source.exists ? 'exists' : 'missing'}`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
lines.push('', 'Detected sources:');
|
|
962
|
+
if (report.sources.length === 0) {
|
|
963
|
+
lines.push('- none');
|
|
964
|
+
} else {
|
|
965
|
+
for (const source of report.sources) {
|
|
966
|
+
const ref = source.ref || '(unresolved)';
|
|
967
|
+
const status = source.status ? ` status=${source.status}` : '';
|
|
968
|
+
const issue = source.issue ? ` issue=${source.issue}` : '';
|
|
969
|
+
lines.push(`- ${source.source_id}: ${ref}${status}${issue}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
lines.push('', 'Planned changes:');
|
|
974
|
+
if (report.reconciliation.planned_changes.length === 0) {
|
|
975
|
+
lines.push('- none');
|
|
976
|
+
} else {
|
|
977
|
+
for (const change of report.reconciliation.planned_changes) {
|
|
978
|
+
lines.push(`- ${change}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
lines.push('', 'Risks:');
|
|
983
|
+
if (report.reconciliation.risks.length === 0) {
|
|
984
|
+
lines.push('- none');
|
|
985
|
+
} else {
|
|
986
|
+
for (const risk of report.reconciliation.risks) {
|
|
987
|
+
lines.push(`- ${risk}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
lines.push('', options.dryRun ? 'No files were changed.' : 'This command is read-only; use start-slice or cleanup-slice for intentional writes.');
|
|
992
|
+
return `${lines.join('\n')}\n`;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function readRunApprovals(repoRoot, run) {
|
|
996
|
+
if (!run?.approvals_path) {
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
const filePath = path.resolve(repoRoot, run.approvals_path);
|
|
1000
|
+
if (!fs.existsSync(filePath)) {
|
|
1001
|
+
return [];
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
1005
|
+
return Array.isArray(parsed.approvals) ? parsed.approvals : [];
|
|
1006
|
+
} catch {
|
|
1007
|
+
return [];
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function collectRunApprovalRows(repoRoot) {
|
|
1012
|
+
const activeRun = resolveAiRun(repoRoot, '');
|
|
1013
|
+
return listAiRuns(repoRoot)
|
|
1014
|
+
.flatMap((run) => readRunApprovals(repoRoot, run).map((approval) => ({
|
|
1015
|
+
run,
|
|
1016
|
+
approval,
|
|
1017
|
+
relation: activeRun && run.run_id === activeRun.run_id
|
|
1018
|
+
? 'active'
|
|
1019
|
+
: run.status === 'closed'
|
|
1020
|
+
? 'historical'
|
|
1021
|
+
: 'other-open',
|
|
1022
|
+
})));
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function approvalArtifactForRelation(report) {
|
|
1026
|
+
return report?.approved?.path || report?.draft?.path || '';
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function classifyGlobalApprovalRelation(report, runApprovalRows) {
|
|
1030
|
+
const artifact = approvalArtifactForRelation(report);
|
|
1031
|
+
if (!artifact || report.status === 'missing') {
|
|
1032
|
+
return 'none';
|
|
1033
|
+
}
|
|
1034
|
+
const matches = runApprovalRows.filter((row) => row.approval?.artifact === artifact);
|
|
1035
|
+
if (matches.some((row) => row.relation === 'active')) {
|
|
1036
|
+
return 'active';
|
|
1037
|
+
}
|
|
1038
|
+
if (matches.length > 0) {
|
|
1039
|
+
return 'historical';
|
|
1040
|
+
}
|
|
1041
|
+
return 'orphaned';
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function formatRunScopedApprovals(repoRoot, runApprovalRows) {
|
|
1045
|
+
const runs = listAiRuns(repoRoot);
|
|
1046
|
+
const activeRun = resolveAiRun(repoRoot, '');
|
|
1047
|
+
const lines = [
|
|
1048
|
+
'Run-scoped approvals',
|
|
1049
|
+
`Active run: ${activeRun ? activeRun.run_id : '(none)'}`,
|
|
1050
|
+
];
|
|
1051
|
+
|
|
1052
|
+
if (runs.length === 0) {
|
|
1053
|
+
lines.push('- no AI runs found');
|
|
1054
|
+
return `${lines.join('\n')}\n`;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
for (const run of runs.slice().reverse()) {
|
|
1058
|
+
const relation = activeRun && run.run_id === activeRun.run_id
|
|
1059
|
+
? 'active'
|
|
1060
|
+
: run.status === 'closed'
|
|
1061
|
+
? 'historical'
|
|
1062
|
+
: 'other-open';
|
|
1063
|
+
const approvals = runApprovalRows.filter((row) => row.run.run_id === run.run_id);
|
|
1064
|
+
lines.push(`Run: ${run.run_id} (${relation}, phase: ${run.phase}, status: ${run.status})`);
|
|
1065
|
+
if (approvals.length === 0) {
|
|
1066
|
+
lines.push('- no run-scoped approvals');
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
for (const row of approvals) {
|
|
1070
|
+
const version = row.approval.version ? ` v${row.approval.version}` : '';
|
|
1071
|
+
lines.push(`- ${row.approval.phase || 'unknown'}${version}: ${row.approval.artifact || '(missing artifact)'}`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return `${lines.join('\n')}\n`;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
551
1078
|
function formatApprovalStatusReport(repoRoot) {
|
|
552
|
-
const
|
|
1079
|
+
const runApprovalRows = collectRunApprovalRows(repoRoot);
|
|
1080
|
+
const sections = ['AI approvals status', formatRunScopedApprovals(repoRoot, runApprovalRows).trimEnd(), 'Global planner approvals'];
|
|
553
1081
|
for (const phase of PLANNER_APPROVAL_PHASES) {
|
|
554
|
-
|
|
1082
|
+
const summary = summarizePlannerApproval(repoRoot, phase).trimEnd();
|
|
1083
|
+
const relation = classifyGlobalApprovalRelation(readPhaseApproval(repoRoot, phase), runApprovalRows);
|
|
1084
|
+
sections.push(`${summary}\nRun relation: ${relation}`);
|
|
555
1085
|
}
|
|
556
1086
|
sections.push(summarizePlanReview(repoRoot).trimEnd());
|
|
557
1087
|
return `${sections.join('\n\n')}\n`;
|
|
@@ -640,12 +1170,14 @@ async function runOnboard(repoRoot, options = {}) {
|
|
|
640
1170
|
throw annotateProviderError(error, 'onboard');
|
|
641
1171
|
}
|
|
642
1172
|
|
|
643
|
-
writeProviderOutput(result);
|
|
644
|
-
|
|
645
1173
|
if (!result.ok) {
|
|
1174
|
+
writeProviderOutput(result);
|
|
646
1175
|
throw annotateProviderError(result.error || new Error('provider run failed'), 'onboard');
|
|
647
1176
|
}
|
|
648
1177
|
|
|
1178
|
+
const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
|
|
1179
|
+
writeCleanProviderOutput(clean);
|
|
1180
|
+
|
|
649
1181
|
return {
|
|
650
1182
|
task: 'onboard',
|
|
651
1183
|
provider,
|
|
@@ -658,6 +1190,10 @@ async function runOnboard(repoRoot, options = {}) {
|
|
|
658
1190
|
}
|
|
659
1191
|
|
|
660
1192
|
async function runPrepareContext(repoRoot, options = {}) {
|
|
1193
|
+
if (options.withPlanner === true) {
|
|
1194
|
+
return runPrepareContextWithPlanner(repoRoot, options);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
661
1197
|
const draftPack = buildContextPreparationDrafts(repoRoot);
|
|
662
1198
|
const writePlan = buildContextWritePlan(repoRoot, draftPack.docs);
|
|
663
1199
|
const report = {
|
|
@@ -719,6 +1255,152 @@ async function runPrepareContext(repoRoot, options = {}) {
|
|
|
719
1255
|
};
|
|
720
1256
|
}
|
|
721
1257
|
|
|
1258
|
+
async function runPrepareContextWithPlanner(repoRoot, options = {}) {
|
|
1259
|
+
const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
|
|
1260
|
+
const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
|
|
1261
|
+
const context = options.context || DEFAULT_PLAN_CONTEXT;
|
|
1262
|
+
const timeoutMs = normalizeTimeout(options.timeout);
|
|
1263
|
+
const draftPack = buildContextPreparationDrafts(repoRoot);
|
|
1264
|
+
const pack = buildContextPackMetadata({
|
|
1265
|
+
role,
|
|
1266
|
+
packName: context,
|
|
1267
|
+
repoRoot,
|
|
1268
|
+
});
|
|
1269
|
+
const promptInfo = buildPrepareContextPlannerPrompt({ pack, draftPack });
|
|
1270
|
+
const prompt = promptInfo.prompt;
|
|
1271
|
+
assertProviderPromptWithinLimit(prompt, options.promptLimitOptions || {});
|
|
1272
|
+
let invocation;
|
|
1273
|
+
|
|
1274
|
+
try {
|
|
1275
|
+
invocation = buildProviderInvocation(provider, {
|
|
1276
|
+
prompt,
|
|
1277
|
+
cwd: repoRoot,
|
|
1278
|
+
timeoutMs,
|
|
1279
|
+
});
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
throw annotateProviderError(error, 'prepare-context');
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
if (options.dryRun) {
|
|
1285
|
+
const report = {
|
|
1286
|
+
task: 'prepare-context',
|
|
1287
|
+
mode: 'planner',
|
|
1288
|
+
dryRun: true,
|
|
1289
|
+
provider,
|
|
1290
|
+
role,
|
|
1291
|
+
contextPack: context,
|
|
1292
|
+
invocation,
|
|
1293
|
+
candidateDocs: promptInfo.allowedPaths,
|
|
1294
|
+
plan: draftPack.plan,
|
|
1295
|
+
};
|
|
1296
|
+
process.stdout.write(formatPrepareContextPlannerDryRunReport({
|
|
1297
|
+
provider,
|
|
1298
|
+
role,
|
|
1299
|
+
context,
|
|
1300
|
+
invocation,
|
|
1301
|
+
promptInfo,
|
|
1302
|
+
review: options.review === true,
|
|
1303
|
+
interactive: options.interactive === true,
|
|
1304
|
+
}));
|
|
1305
|
+
return report;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (options.printPrompt) {
|
|
1309
|
+
const report = {
|
|
1310
|
+
task: 'prepare-context',
|
|
1311
|
+
provider,
|
|
1312
|
+
role,
|
|
1313
|
+
contextPack: context,
|
|
1314
|
+
invocation,
|
|
1315
|
+
prompt,
|
|
1316
|
+
promptSource: promptInfo.promptSource,
|
|
1317
|
+
};
|
|
1318
|
+
process.stdout.write(formatPromptOnlyReport(report));
|
|
1319
|
+
return report;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
let result;
|
|
1323
|
+
try {
|
|
1324
|
+
result = await (options.runProviderFn || runProvider)(provider, {
|
|
1325
|
+
prompt,
|
|
1326
|
+
cwd: repoRoot,
|
|
1327
|
+
timeoutMs,
|
|
1328
|
+
dryRun: false,
|
|
1329
|
+
probe: options.probe,
|
|
1330
|
+
spawn: options.spawn,
|
|
1331
|
+
tempRoot: options.tempRoot,
|
|
1332
|
+
tempFileName: options.tempFileName,
|
|
1333
|
+
tempFilePrefix: options.tempFilePrefix,
|
|
1334
|
+
});
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
throw annotateProviderError(error, 'prepare-context');
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (!result.ok) {
|
|
1340
|
+
writeProviderOutput(result);
|
|
1341
|
+
throw annotateProviderError(result.error || new Error('provider run failed'), 'prepare-context');
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
|
|
1345
|
+
let proposal = parseContextProposalOutput(clean.cleanOutput);
|
|
1346
|
+
let reviewPath = '';
|
|
1347
|
+
|
|
1348
|
+
if (options.review === true) {
|
|
1349
|
+
const reviewed = await reviewPlannerContextProposal(repoRoot, proposal, options);
|
|
1350
|
+
proposal = reviewed.proposal;
|
|
1351
|
+
reviewPath = reviewed.reviewPath;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const writePlan = buildPlannerContextWritePlan(repoRoot, proposal);
|
|
1355
|
+
await confirmPlannerContextWrites(writePlan, options);
|
|
1356
|
+
|
|
1357
|
+
const lifecycleRun = ensureAiRun(repoRoot, {
|
|
1358
|
+
command: 'ai prepare-context --with-planner',
|
|
1359
|
+
input: options.input || '',
|
|
1360
|
+
runId: options.runId,
|
|
1361
|
+
phase: 'created',
|
|
1362
|
+
});
|
|
1363
|
+
const snapshot = createContextSnapshots(repoRoot, lifecycleRun, writePlan, options.now || new Date());
|
|
1364
|
+
const plannedDocs = writePlan.filter((item) => item.action !== 'skip').map((item) => item.path);
|
|
1365
|
+
|
|
1366
|
+
process.stdout.write(formatContextPreparationReport({
|
|
1367
|
+
dryRun: false,
|
|
1368
|
+
plan: draftPack.plan,
|
|
1369
|
+
writePlan,
|
|
1370
|
+
writtenDocs: plannedDocs,
|
|
1371
|
+
snapshot,
|
|
1372
|
+
}));
|
|
1373
|
+
|
|
1374
|
+
const writtenDocs = writeDraftDocs(writePlan);
|
|
1375
|
+
updateAiRunPhase(repoRoot, lifecycleRun.run_id, 'onboarding-ready', {
|
|
1376
|
+
artifact: snapshot.manifestPath,
|
|
1377
|
+
command: 'ai prepare-context --with-planner',
|
|
1378
|
+
});
|
|
1379
|
+
process.stdout.write(formatContextPreparationReport({
|
|
1380
|
+
dryRun: false,
|
|
1381
|
+
plan: draftPack.plan,
|
|
1382
|
+
writePlan,
|
|
1383
|
+
writtenDocs,
|
|
1384
|
+
snapshot,
|
|
1385
|
+
completed: true,
|
|
1386
|
+
}));
|
|
1387
|
+
|
|
1388
|
+
return {
|
|
1389
|
+
task: 'prepare-context',
|
|
1390
|
+
mode: 'planner',
|
|
1391
|
+
dryRun: false,
|
|
1392
|
+
provider,
|
|
1393
|
+
role,
|
|
1394
|
+
contextPack: context,
|
|
1395
|
+
invocation,
|
|
1396
|
+
proposal,
|
|
1397
|
+
reviewPath,
|
|
1398
|
+
runId: lifecycleRun.run_id,
|
|
1399
|
+
snapshot,
|
|
1400
|
+
writtenDocs,
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
722
1404
|
async function runPlan(repoRoot, options = {}) {
|
|
723
1405
|
const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
|
|
724
1406
|
const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
|
|
@@ -839,6 +1521,15 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
839
1521
|
invocation,
|
|
840
1522
|
};
|
|
841
1523
|
process.stdout.write(formatDryRunReport(report));
|
|
1524
|
+
if (options.withPlanner === true) {
|
|
1525
|
+
process.stdout.write('Planner mode: already active for ai plan; --with-planner is accepted for UX consistency.\n');
|
|
1526
|
+
}
|
|
1527
|
+
if (options.review === true) {
|
|
1528
|
+
process.stdout.write('Review requested: provider output will be opened for review before saving the draft in live mode.\n');
|
|
1529
|
+
}
|
|
1530
|
+
if (options.interactive === true) {
|
|
1531
|
+
process.stdout.write('Interactive requested: live mode will ask before saving the draft.\n');
|
|
1532
|
+
}
|
|
842
1533
|
return report;
|
|
843
1534
|
}
|
|
844
1535
|
|
|
@@ -873,18 +1564,32 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
873
1564
|
throw annotateProviderError(error, 'plan', phase);
|
|
874
1565
|
}
|
|
875
1566
|
|
|
876
|
-
writeProviderOutput(result);
|
|
877
|
-
|
|
878
1567
|
if (!result.ok) {
|
|
1568
|
+
writeProviderOutput(result);
|
|
879
1569
|
throw annotateProviderError(result.error || new Error('provider run failed'), 'plan', phase);
|
|
880
1570
|
}
|
|
881
1571
|
|
|
1572
|
+
const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
|
|
1573
|
+
let cleanOutput = clean.cleanOutput;
|
|
1574
|
+
let reviewPath = '';
|
|
1575
|
+
|
|
1576
|
+
if (options.review === true) {
|
|
1577
|
+
const reviewed = await reviewTextWithEditor(repoRoot, cleanOutput, {
|
|
1578
|
+
...options,
|
|
1579
|
+
reviewFileName: `ai-plan-${phase}-draft.md`,
|
|
1580
|
+
reviewLabel: `ai plan --phase ${phase} review`,
|
|
1581
|
+
});
|
|
1582
|
+
cleanOutput = reviewed.text;
|
|
1583
|
+
reviewPath = reviewed.reviewPath;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
await confirmInteractiveAction(`Save ${phase} planner draft?`, options);
|
|
882
1587
|
const lifecycleRun = ensureAiRun(repoRoot, {
|
|
883
1588
|
command: `ai plan --phase ${phase}`,
|
|
884
1589
|
input: inputPath,
|
|
885
1590
|
runId: options.runId,
|
|
886
1591
|
});
|
|
887
|
-
|
|
1592
|
+
writeCleanProviderOutput({ cleanOutput });
|
|
888
1593
|
const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, `ai-plan-${phase}`, result, {
|
|
889
1594
|
metadata: {
|
|
890
1595
|
phase,
|
|
@@ -895,10 +1600,11 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
895
1600
|
input_compaction: inputCompaction,
|
|
896
1601
|
},
|
|
897
1602
|
});
|
|
898
|
-
const draft = savePlannerDraft(repoRoot, phase, inputPath,
|
|
1603
|
+
const draft = savePlannerDraft(repoRoot, phase, inputPath, cleanOutput, {
|
|
899
1604
|
rawArtifactPath: rawArtifact.path,
|
|
900
1605
|
outputSource: clean.source,
|
|
901
1606
|
inputCompaction,
|
|
1607
|
+
reviewPath,
|
|
902
1608
|
});
|
|
903
1609
|
updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-draft' : 'technical-plan-draft', {
|
|
904
1610
|
artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
|
|
@@ -913,6 +1619,7 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
913
1619
|
phase,
|
|
914
1620
|
invocation,
|
|
915
1621
|
result,
|
|
1622
|
+
reviewPath,
|
|
916
1623
|
};
|
|
917
1624
|
}
|
|
918
1625
|
|
|
@@ -1011,9 +1718,8 @@ async function runReviewPlan(repoRoot, options = {}) {
|
|
|
1011
1718
|
throw annotateProviderError(error, 'review-plan');
|
|
1012
1719
|
}
|
|
1013
1720
|
|
|
1014
|
-
writeProviderOutput(result);
|
|
1015
|
-
|
|
1016
1721
|
if (!result.ok) {
|
|
1722
|
+
writeProviderOutput(result);
|
|
1017
1723
|
throw annotateProviderError(result.error || new Error('provider run failed'), 'review-plan');
|
|
1018
1724
|
}
|
|
1019
1725
|
|
|
@@ -1035,6 +1741,7 @@ async function runReviewPlan(repoRoot, options = {}) {
|
|
|
1035
1741
|
stripped_prompt_echo: clean.strippedPromptEcho,
|
|
1036
1742
|
},
|
|
1037
1743
|
});
|
|
1744
|
+
writeCleanProviderOutput(clean);
|
|
1038
1745
|
const saved = savePlanReview(repoRoot, {
|
|
1039
1746
|
contents: clean.cleanOutput,
|
|
1040
1747
|
inputPath,
|
|
@@ -1044,7 +1751,8 @@ async function runReviewPlan(repoRoot, options = {}) {
|
|
|
1044
1751
|
rawArtifactPath: rawArtifact.path,
|
|
1045
1752
|
});
|
|
1046
1753
|
const relativePath = path.relative(repoRoot, saved.filePath).split(path.sep).join('/');
|
|
1047
|
-
|
|
1754
|
+
const summary = summarizePlanReview(repoRoot).trimEnd();
|
|
1755
|
+
process.stdout.write(`AI plan review saved\nArtifact: ${relativePath}\nPrompt source: ${PLAN_REVIEW_PROMPT_SOURCE}\n${summary}\n`);
|
|
1048
1756
|
|
|
1049
1757
|
return {
|
|
1050
1758
|
task: 'review-plan',
|
|
@@ -1060,6 +1768,146 @@ async function runReviewPlan(repoRoot, options = {}) {
|
|
|
1060
1768
|
};
|
|
1061
1769
|
}
|
|
1062
1770
|
|
|
1771
|
+
async function runRepairPlan(repoRoot, options = {}) {
|
|
1772
|
+
const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
|
|
1773
|
+
const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
|
|
1774
|
+
const context = options.context || DEFAULT_PLAN_CONTEXT;
|
|
1775
|
+
const timeoutMs = normalizeTimeout(options.timeout);
|
|
1776
|
+
const source = resolveApprovedTechnicalPlanForRepair(repoRoot, options.input || '');
|
|
1777
|
+
const built = buildRepairPlanContext({
|
|
1778
|
+
context,
|
|
1779
|
+
inputText: source.contents,
|
|
1780
|
+
inputPath: source.path,
|
|
1781
|
+
repoRoot,
|
|
1782
|
+
role,
|
|
1783
|
+
validationError: source.validationError,
|
|
1784
|
+
});
|
|
1785
|
+
assertProviderPromptWithinLimit(built.prompt, options);
|
|
1786
|
+
let invocation;
|
|
1787
|
+
|
|
1788
|
+
try {
|
|
1789
|
+
invocation = buildProviderInvocation(provider, {
|
|
1790
|
+
prompt: built.prompt,
|
|
1791
|
+
cwd: repoRoot,
|
|
1792
|
+
timeoutMs,
|
|
1793
|
+
});
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
throw annotateProviderError(error, 'repair-plan');
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
if (options.dryRun) {
|
|
1799
|
+
const report = {
|
|
1800
|
+
task: 'repair-plan',
|
|
1801
|
+
provider,
|
|
1802
|
+
role,
|
|
1803
|
+
contextPack: built.pack.packName,
|
|
1804
|
+
phase: 'technical-plan',
|
|
1805
|
+
invocation,
|
|
1806
|
+
};
|
|
1807
|
+
process.stdout.write(formatDryRunReport(report));
|
|
1808
|
+
process.stdout.write(`Source approved artifact: ${source.path}\n`);
|
|
1809
|
+
process.stdout.write(`Validation failure: ${source.validationError}\n`);
|
|
1810
|
+
return report;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (options.printPrompt) {
|
|
1814
|
+
const report = {
|
|
1815
|
+
task: 'repair-plan',
|
|
1816
|
+
provider,
|
|
1817
|
+
role,
|
|
1818
|
+
contextPack: built.pack.packName,
|
|
1819
|
+
phase: 'technical-plan',
|
|
1820
|
+
invocation,
|
|
1821
|
+
prompt: built.prompt,
|
|
1822
|
+
inputPath: source.path,
|
|
1823
|
+
inputKind: 'approved',
|
|
1824
|
+
inputVersion: source.approval.meta?.approved?.version || null,
|
|
1825
|
+
};
|
|
1826
|
+
process.stdout.write(formatPromptOnlyReport(report));
|
|
1827
|
+
return report;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
let providerResult;
|
|
1831
|
+
try {
|
|
1832
|
+
providerResult = await (options.runProviderFn || runProvider)(provider, {
|
|
1833
|
+
prompt: built.prompt,
|
|
1834
|
+
cwd: repoRoot,
|
|
1835
|
+
timeoutMs,
|
|
1836
|
+
dryRun: false,
|
|
1837
|
+
probe: options.probe,
|
|
1838
|
+
spawn: options.spawn,
|
|
1839
|
+
tempRoot: options.tempRoot,
|
|
1840
|
+
tempFileName: options.tempFileName,
|
|
1841
|
+
tempFilePrefix: options.tempFilePrefix,
|
|
1842
|
+
});
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
throw annotateProviderError(error, 'repair-plan');
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
if (!providerResult.ok) {
|
|
1848
|
+
writeProviderOutput(providerResult);
|
|
1849
|
+
throw annotateProviderError(providerResult.error || new Error('provider run failed'), 'repair-plan');
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const lifecycleRun = ensureAiRun(repoRoot, {
|
|
1853
|
+
command: 'ai repair-plan',
|
|
1854
|
+
input: source.path,
|
|
1855
|
+
runId: options.runId,
|
|
1856
|
+
});
|
|
1857
|
+
const clean = extractCleanProviderOutput(providerResult, { prompt: built.prompt, projectRoot: repoRoot });
|
|
1858
|
+
const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, 'ai-repair-plan', providerResult, {
|
|
1859
|
+
metadata: {
|
|
1860
|
+
phase: 'technical-plan-repair',
|
|
1861
|
+
input_path: source.path,
|
|
1862
|
+
prompt_bytes: invocation.promptLength,
|
|
1863
|
+
clean_output_source: clean.source,
|
|
1864
|
+
stripped_prompt_echo: clean.strippedPromptEcho,
|
|
1865
|
+
validation_failure: source.validationError,
|
|
1866
|
+
},
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
try {
|
|
1870
|
+
validateTechnicalPlanSpecContract(repoRoot, {
|
|
1871
|
+
inputPath: source.path,
|
|
1872
|
+
inputText: clean.cleanOutput,
|
|
1873
|
+
});
|
|
1874
|
+
} catch (error) {
|
|
1875
|
+
throw new Error(formatError([
|
|
1876
|
+
'ai repair-plan provider output is still missing the required structured `spec.slices[]` contract.',
|
|
1877
|
+
stripCreateQuiverPrefix(error.message || error),
|
|
1878
|
+
`Raw provider artifact: ${rawArtifact.path}`,
|
|
1879
|
+
'No technical-plan draft was written.',
|
|
1880
|
+
].join('\n')));
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
writeCleanProviderOutput(clean);
|
|
1884
|
+
const draft = savePlannerDraft(repoRoot, 'technical-plan', source.path, clean.cleanOutput, {
|
|
1885
|
+
rawArtifactPath: rawArtifact.path,
|
|
1886
|
+
outputSource: clean.source,
|
|
1887
|
+
});
|
|
1888
|
+
updateAiRunPhase(repoRoot, lifecycleRun.run_id, 'technical-plan-draft', {
|
|
1889
|
+
artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
|
|
1890
|
+
command: 'ai repair-plan',
|
|
1891
|
+
});
|
|
1892
|
+
process.stdout.write(formatRepairPlanResult({
|
|
1893
|
+
...draft,
|
|
1894
|
+
sourcePath: source.path,
|
|
1895
|
+
}, repoRoot));
|
|
1896
|
+
|
|
1897
|
+
return {
|
|
1898
|
+
task: 'repair-plan',
|
|
1899
|
+
provider,
|
|
1900
|
+
role,
|
|
1901
|
+
contextPack: built.pack.packName,
|
|
1902
|
+
phase: 'technical-plan',
|
|
1903
|
+
inputPath: source.path,
|
|
1904
|
+
filePath: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
|
|
1905
|
+
version: draft.version || null,
|
|
1906
|
+
invocation,
|
|
1907
|
+
result: providerResult,
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1063
1911
|
async function runRevise(repoRoot, options = {}) {
|
|
1064
1912
|
const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
|
|
1065
1913
|
if (phase === 'spec') {
|
|
@@ -1095,8 +1943,14 @@ async function runApprove(repoRoot, options = {}) {
|
|
|
1095
1943
|
if (phase === 'technical-plan') {
|
|
1096
1944
|
const review = readPlanReview(repoRoot);
|
|
1097
1945
|
if (review.status !== 'unapproved' && review.status !== 'reviewed') {
|
|
1098
|
-
throw new Error(formatError(`ai approve --phase technical-plan requires a production review for the current draft; current review status is ${review.status}. Run \`npx create-quiver ai review-plan\`.`));
|
|
1946
|
+
throw new Error(formatError(`ai approve --phase technical-plan requires a production review for the current draft; current review status is ${review.status}. Run \`npx create-quiver ai review-plan --dry-run\`, then \`npx create-quiver ai review-plan\`.`));
|
|
1947
|
+
}
|
|
1948
|
+
if (reviewBlocksApproval(review)) {
|
|
1949
|
+
const result = review.meta.review_result;
|
|
1950
|
+
const requiredFixes = Array.isArray(result.required_fixes) ? result.required_fixes.length : 0;
|
|
1951
|
+
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}`));
|
|
1099
1952
|
}
|
|
1953
|
+
assertTechnicalPlanDraftHasSpecContract(repoRoot, options.version);
|
|
1100
1954
|
}
|
|
1101
1955
|
|
|
1102
1956
|
const inputText = '';
|
|
@@ -1255,14 +2109,53 @@ function runTraceReport(repoRoot, options = {}) {
|
|
|
1255
2109
|
};
|
|
1256
2110
|
}
|
|
1257
2111
|
|
|
2112
|
+
function runActiveSlice(repoRoot, options = {}) {
|
|
2113
|
+
const command = String(options.command || 'status').trim().toLowerCase();
|
|
2114
|
+
if (command !== 'status' && command !== 'reconcile') {
|
|
2115
|
+
throw new Error(formatError(`unsupported ai active-slice subcommand: ${command}. Supported tasks: status, reconcile`));
|
|
2116
|
+
}
|
|
2117
|
+
if (command === 'reconcile' && options.dryRun !== true) {
|
|
2118
|
+
throw new Error(formatError('ai active-slice reconcile is dry-run first. Run `npx create-quiver ai active-slice reconcile --dry-run`.'));
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
const state = resolveProjectState(repoRoot, { allowGraphErrors: true });
|
|
2122
|
+
const report = collectActiveSliceState(repoRoot, { slices: state.graph.nodes });
|
|
2123
|
+
process.stdout.write(formatActiveSliceReconciliationReport(report, {
|
|
2124
|
+
dryRun: options.dryRun === true,
|
|
2125
|
+
}));
|
|
2126
|
+
return {
|
|
2127
|
+
task: 'active-slice',
|
|
2128
|
+
command,
|
|
2129
|
+
dryRun: options.dryRun === true,
|
|
2130
|
+
report,
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
|
|
1258
2134
|
function runLifecycleRun(repoRoot, options = {}) {
|
|
1259
2135
|
const command = String(options.command || '').trim().toLowerCase();
|
|
1260
|
-
if (command !== 'create') {
|
|
1261
|
-
throw new Error(formatError(`unsupported ai run subcommand: ${command}. Supported tasks: create`));
|
|
2136
|
+
if (command !== 'create' && command !== 'close') {
|
|
2137
|
+
throw new Error(formatError(`unsupported ai run subcommand: ${command}. Supported tasks: create, close`));
|
|
1262
2138
|
}
|
|
1263
|
-
if (!options.input) {
|
|
2139
|
+
if (command === 'create' && !options.input) {
|
|
1264
2140
|
throw new Error(formatError('ai run create requires --input <requirements.md>'));
|
|
1265
2141
|
}
|
|
2142
|
+
if (command === 'close') {
|
|
2143
|
+
const current = resolveAiRun(repoRoot, options.runId || '');
|
|
2144
|
+
if (!current) {
|
|
2145
|
+
throw new Error(formatError('ai run close requires an active run or --run <id>'));
|
|
2146
|
+
}
|
|
2147
|
+
const run = updateAiRunPhase(repoRoot, current.run_id, 'closed', {
|
|
2148
|
+
command: 'ai run close',
|
|
2149
|
+
});
|
|
2150
|
+
const report = `AI run closed\n${formatAiRunStatus(repoRoot, run)}`;
|
|
2151
|
+
process.stdout.write(report);
|
|
2152
|
+
return {
|
|
2153
|
+
task: 'run',
|
|
2154
|
+
command,
|
|
2155
|
+
run,
|
|
2156
|
+
report,
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
1266
2159
|
const run = createAiRun(repoRoot, {
|
|
1267
2160
|
command: 'ai run create',
|
|
1268
2161
|
input: options.input,
|
|
@@ -1463,6 +2356,31 @@ async function runPr(repoRoot, options = {}) {
|
|
|
1463
2356
|
throw annotateGitHubError(error, 'pr');
|
|
1464
2357
|
}
|
|
1465
2358
|
|
|
2359
|
+
if (options.review === true) {
|
|
2360
|
+
const hasEditorRunner = typeof options.openEditorFn === 'function';
|
|
2361
|
+
const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
|
|
2362
|
+
if (!canOpenEditor) {
|
|
2363
|
+
throw annotateGitHubError(makeReviewError('ai pr --review requires an interactive terminal or an injected editor runner.', plan.prBodyPath), 'pr');
|
|
2364
|
+
}
|
|
2365
|
+
const editorResult = hasEditorRunner
|
|
2366
|
+
? options.openEditorFn(plan.prBodyPath, { cwd: repoRoot, env: options.env || process.env })
|
|
2367
|
+
: openEditor(plan.prBodyPath, { cwd: repoRoot, env: options.env || process.env });
|
|
2368
|
+
if (!editorResult || editorResult.ok !== true) {
|
|
2369
|
+
throw annotateGitHubError(makeReviewError(editorResult?.reason || 'ai pr review was canceled.', plan.prBodyPath), 'pr');
|
|
2370
|
+
}
|
|
2371
|
+
try {
|
|
2372
|
+
plan = buildPrCreatePlan(repoRoot, preflight, {
|
|
2373
|
+
baseBranch: options.baseBranch,
|
|
2374
|
+
ghCommand: options.ghCommand,
|
|
2375
|
+
input: options.input,
|
|
2376
|
+
prBodyPath: options.prBodyPath,
|
|
2377
|
+
title: options.title,
|
|
2378
|
+
});
|
|
2379
|
+
} catch (error) {
|
|
2380
|
+
throw annotateGitHubError(error, 'pr');
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
1466
2384
|
if (dryRun || !create) {
|
|
1467
2385
|
process.stdout.write(formatPrCreateReport({ preflight, plan }, { dryRun, create }));
|
|
1468
2386
|
return {
|
|
@@ -1474,6 +2392,8 @@ async function runPr(repoRoot, options = {}) {
|
|
|
1474
2392
|
};
|
|
1475
2393
|
}
|
|
1476
2394
|
|
|
2395
|
+
await confirmInteractiveAction(`Create GitHub PR '${plan.title}'?`, options);
|
|
2396
|
+
|
|
1477
2397
|
let result;
|
|
1478
2398
|
try {
|
|
1479
2399
|
result = runGhPrCreate(plan, {
|
|
@@ -1516,6 +2436,7 @@ module.exports = {
|
|
|
1516
2436
|
normalizeTimeout,
|
|
1517
2437
|
readTextFile,
|
|
1518
2438
|
runAgent,
|
|
2439
|
+
runActiveSlice,
|
|
1519
2440
|
runDoctor,
|
|
1520
2441
|
runExecutePlan,
|
|
1521
2442
|
runExecuteSlice,
|
|
@@ -1528,6 +2449,7 @@ module.exports = {
|
|
|
1528
2449
|
runApprove,
|
|
1529
2450
|
runApprovalStatus,
|
|
1530
2451
|
runPrepareContext,
|
|
2452
|
+
runRepairPlan,
|
|
1531
2453
|
runReviewPlan,
|
|
1532
2454
|
runRevise,
|
|
1533
2455
|
runPr,
|