create-quiver 0.14.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 +11 -0
- package/README.md +183 -518
- package/README_FOR_AI.md +32 -26
- package/ROADMAP.md +5 -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 +16 -3
- package/docs/GITFLOW_PR_GUIDE.md +70 -0
- 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-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 +458 -3
- package/src/create-quiver/commands/spec.js +64 -2
- package/src/create-quiver/index.js +49 -1
- 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/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
|
@@ -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');
|
|
@@ -313,6 +317,255 @@ function formatContextPreparationReport({ dryRun, plan, writePlan, writtenDocs,
|
|
|
313
317
|
return `${lines.join('\n')}\n`;
|
|
314
318
|
}
|
|
315
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
|
+
|
|
316
569
|
function writeProviderOutput(result) {
|
|
317
570
|
if (result.stdout) {
|
|
318
571
|
process.stdout.write(redactSecrets(result.stdout));
|
|
@@ -937,6 +1190,10 @@ async function runOnboard(repoRoot, options = {}) {
|
|
|
937
1190
|
}
|
|
938
1191
|
|
|
939
1192
|
async function runPrepareContext(repoRoot, options = {}) {
|
|
1193
|
+
if (options.withPlanner === true) {
|
|
1194
|
+
return runPrepareContextWithPlanner(repoRoot, options);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
940
1197
|
const draftPack = buildContextPreparationDrafts(repoRoot);
|
|
941
1198
|
const writePlan = buildContextWritePlan(repoRoot, draftPack.docs);
|
|
942
1199
|
const report = {
|
|
@@ -998,6 +1255,152 @@ async function runPrepareContext(repoRoot, options = {}) {
|
|
|
998
1255
|
};
|
|
999
1256
|
}
|
|
1000
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
|
+
|
|
1001
1404
|
async function runPlan(repoRoot, options = {}) {
|
|
1002
1405
|
const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
|
|
1003
1406
|
const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
|
|
@@ -1118,6 +1521,15 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
1118
1521
|
invocation,
|
|
1119
1522
|
};
|
|
1120
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
|
+
}
|
|
1121
1533
|
return report;
|
|
1122
1534
|
}
|
|
1123
1535
|
|
|
@@ -1157,13 +1569,27 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
1157
1569
|
throw annotateProviderError(result.error || new Error('provider run failed'), 'plan', phase);
|
|
1158
1570
|
}
|
|
1159
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);
|
|
1160
1587
|
const lifecycleRun = ensureAiRun(repoRoot, {
|
|
1161
1588
|
command: `ai plan --phase ${phase}`,
|
|
1162
1589
|
input: inputPath,
|
|
1163
1590
|
runId: options.runId,
|
|
1164
1591
|
});
|
|
1165
|
-
|
|
1166
|
-
writeCleanProviderOutput(clean);
|
|
1592
|
+
writeCleanProviderOutput({ cleanOutput });
|
|
1167
1593
|
const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, `ai-plan-${phase}`, result, {
|
|
1168
1594
|
metadata: {
|
|
1169
1595
|
phase,
|
|
@@ -1174,10 +1600,11 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
1174
1600
|
input_compaction: inputCompaction,
|
|
1175
1601
|
},
|
|
1176
1602
|
});
|
|
1177
|
-
const draft = savePlannerDraft(repoRoot, phase, inputPath,
|
|
1603
|
+
const draft = savePlannerDraft(repoRoot, phase, inputPath, cleanOutput, {
|
|
1178
1604
|
rawArtifactPath: rawArtifact.path,
|
|
1179
1605
|
outputSource: clean.source,
|
|
1180
1606
|
inputCompaction,
|
|
1607
|
+
reviewPath,
|
|
1181
1608
|
});
|
|
1182
1609
|
updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-draft' : 'technical-plan-draft', {
|
|
1183
1610
|
artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
|
|
@@ -1192,6 +1619,7 @@ async function runPlan(repoRoot, options = {}) {
|
|
|
1192
1619
|
phase,
|
|
1193
1620
|
invocation,
|
|
1194
1621
|
result,
|
|
1622
|
+
reviewPath,
|
|
1195
1623
|
};
|
|
1196
1624
|
}
|
|
1197
1625
|
|
|
@@ -1928,6 +2356,31 @@ async function runPr(repoRoot, options = {}) {
|
|
|
1928
2356
|
throw annotateGitHubError(error, 'pr');
|
|
1929
2357
|
}
|
|
1930
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
|
+
|
|
1931
2384
|
if (dryRun || !create) {
|
|
1932
2385
|
process.stdout.write(formatPrCreateReport({ preflight, plan }, { dryRun, create }));
|
|
1933
2386
|
return {
|
|
@@ -1939,6 +2392,8 @@ async function runPr(repoRoot, options = {}) {
|
|
|
1939
2392
|
};
|
|
1940
2393
|
}
|
|
1941
2394
|
|
|
2395
|
+
await confirmInteractiveAction(`Create GitHub PR '${plan.title}'?`, options);
|
|
2396
|
+
|
|
1942
2397
|
let result;
|
|
1943
2398
|
try {
|
|
1944
2399
|
result = runGhPrCreate(plan, {
|
|
@@ -2,6 +2,8 @@ const fs = require('node:fs');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
|
|
4
4
|
const { checkHandoff } = require('../lib/handoff');
|
|
5
|
+
const { openEditor } = require('../lib/cli/editor');
|
|
6
|
+
const { createUx } = require('../lib/cli/ux');
|
|
5
7
|
const { parseJsonWithComments } = require('../lib/json');
|
|
6
8
|
const { assertPathInsideRoot, validateProjectRelativePaths } = require('../lib/paths');
|
|
7
9
|
const { resolveReviewedTechnicalPlanInput } = require('../lib/ai/plan-review');
|
|
@@ -362,6 +364,10 @@ function formatSpecCreateDryRun(preview) {
|
|
|
362
364
|
lines.push(`- ${file}`);
|
|
363
365
|
}
|
|
364
366
|
|
|
367
|
+
if (preview.reviewRequested) {
|
|
368
|
+
lines.push('Review requested: yes (dry-run preview only; no editor opened and no files written).');
|
|
369
|
+
}
|
|
370
|
+
|
|
365
371
|
lines.push(...formatNextCommands(preview.relativeSpecDir));
|
|
366
372
|
lines.push('No files will be written in dry-run mode.');
|
|
367
373
|
return `${lines.join('\n')}\n`;
|
|
@@ -384,11 +390,63 @@ function formatSpecCreateResult(result, repoRoot) {
|
|
|
384
390
|
return `${lines.join('\n')}\n`;
|
|
385
391
|
}
|
|
386
392
|
|
|
387
|
-
function
|
|
393
|
+
function createSpecReviewFile(preview, options = {}) {
|
|
394
|
+
const os = require('node:os');
|
|
395
|
+
const reviewDir = options.reviewDir || fs.mkdtempSync(path.join(os.tmpdir(), 'quiver-spec-create-review-'));
|
|
396
|
+
const reviewPath = path.join(reviewDir, 'spec-create-preview.md');
|
|
397
|
+
fs.mkdirSync(reviewDir, { recursive: true });
|
|
398
|
+
fs.writeFileSync(reviewPath, formatSpecCreateDryRun({ ...preview, reviewRequested: false }));
|
|
399
|
+
return reviewPath;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function confirmSpecCreate(message, options = {}) {
|
|
403
|
+
if (options.interactive !== true) {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const ux = options.ux || createUx({
|
|
408
|
+
interactive: true,
|
|
409
|
+
promptConfirm: options.promptConfirm,
|
|
410
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
411
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
412
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
413
|
+
write: options.write,
|
|
414
|
+
});
|
|
415
|
+
const confirmed = await ux.promptConfirm(message, { initialValue: false });
|
|
416
|
+
if (!confirmed) {
|
|
417
|
+
throw new Error(formatError('spec create interactive approval declined. No files were written.'));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function reviewSpecCreatePreview(repoRoot, preview, options = {}) {
|
|
422
|
+
if (options.review !== true) {
|
|
423
|
+
return '';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const reviewPath = createSpecReviewFile(preview, options);
|
|
427
|
+
const hasEditorRunner = typeof options.openEditorFn === 'function';
|
|
428
|
+
const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
|
|
429
|
+
|
|
430
|
+
if (!canOpenEditor) {
|
|
431
|
+
throw new Error(formatError(`spec create --review requires an interactive terminal or an injected editor runner.\nReview artifact: ${reviewPath}`));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const editorResult = hasEditorRunner
|
|
435
|
+
? options.openEditorFn(reviewPath, { cwd: repoRoot, env: options.env || process.env })
|
|
436
|
+
: openEditor(reviewPath, { cwd: repoRoot, env: options.env || process.env });
|
|
437
|
+
|
|
438
|
+
if (!editorResult || editorResult.ok !== true) {
|
|
439
|
+
throw new Error(formatError(`${editorResult?.reason || 'spec create review was canceled.'}\nReview artifact: ${reviewPath}`));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return reviewPath;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function runCreateSpec(repoRoot, options = {}) {
|
|
388
446
|
const preview = buildSpecCreatePreview(repoRoot, options);
|
|
389
447
|
|
|
390
448
|
if (options.dryRun) {
|
|
391
|
-
process.stdout.write(formatSpecCreateDryRun(preview));
|
|
449
|
+
process.stdout.write(formatSpecCreateDryRun({ ...preview, reviewRequested: options.review === true }));
|
|
392
450
|
return {
|
|
393
451
|
task: 'spec-create',
|
|
394
452
|
dryRun: true,
|
|
@@ -398,6 +456,9 @@ function runCreateSpec(repoRoot, options = {}) {
|
|
|
398
456
|
};
|
|
399
457
|
}
|
|
400
458
|
|
|
459
|
+
const reviewPath = await reviewSpecCreatePreview(repoRoot, preview, options);
|
|
460
|
+
await confirmSpecCreate(`Create spec '${preview.manifest.slug}'?`, options);
|
|
461
|
+
|
|
401
462
|
const result = generateSpecArtifacts(repoRoot, {
|
|
402
463
|
input: preview.inputPath,
|
|
403
464
|
specSlug: options.specSlug,
|
|
@@ -411,6 +472,7 @@ function runCreateSpec(repoRoot, options = {}) {
|
|
|
411
472
|
specDir: toRelativePosix(repoRoot, result.specDir),
|
|
412
473
|
files: result.files.map((filePath) => toRelativePosix(repoRoot, filePath)),
|
|
413
474
|
manifest: result.manifest,
|
|
475
|
+
reviewPath,
|
|
414
476
|
};
|
|
415
477
|
}
|
|
416
478
|
|