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.
Files changed (55) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +183 -518
  3. package/README_FOR_AI.md +32 -26
  4. package/ROADMAP.md +5 -0
  5. package/assets/quiver-wordmark.svg +22 -0
  6. package/docs/AI_CONTEXT.md.template +2 -0
  7. package/docs/AI_ONBOARDING_PROMPT.md.template +9 -1
  8. package/docs/CLI_UX_GUIDE.md +125 -0
  9. package/docs/COMMANDS.md.template +16 -3
  10. package/docs/GITFLOW_PR_GUIDE.md +70 -0
  11. package/docs/getting-started/linux.md +84 -0
  12. package/docs/getting-started/macos.md +85 -0
  13. package/docs/getting-started/windows-git-bash-wsl.md +78 -0
  14. package/docs/getting-started/windows-powershell.md +96 -0
  15. package/docs/reference/commands.md +94 -0
  16. package/docs/workflows/existing-project.md +131 -0
  17. package/docs/workflows/full-ai-spec-to-pr.md +311 -0
  18. package/docs/workflows/legacy-quiver-project.md +102 -0
  19. package/docs/workflows/new-project.md +76 -0
  20. package/package.json +5 -1
  21. package/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
  22. package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
  23. package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
  24. package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
  25. package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
  26. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
  27. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
  28. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
  29. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
  30. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
  31. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
  32. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
  33. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
  34. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
  35. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
  36. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
  37. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
  38. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
  39. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
  40. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
  41. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
  42. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
  43. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
  44. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
  45. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
  46. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
  47. package/src/create-quiver/commands/ai.js +458 -3
  48. package/src/create-quiver/commands/spec.js +64 -2
  49. package/src/create-quiver/index.js +49 -1
  50. package/src/create-quiver/lib/ai/context-proposal.js +389 -0
  51. package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
  52. package/src/create-quiver/lib/cli/editor.js +118 -0
  53. package/src/create-quiver/lib/cli/theme.js +100 -0
  54. package/src/create-quiver/lib/cli/ux-flags.js +151 -0
  55. package/src/create-quiver/lib/cli/ux.js +130 -0
@@ -53,6 +53,7 @@ const { checkPrReadiness, checkScope, checkSliceReadiness } = require('./lib/rea
53
53
  const { cleanupSlice, refreshActiveSlicesBoard, startSlice } = require('./lib/lifecycle');
54
54
  const { buildSpecStatus, closeSpecWorktree, formatSpecCloseResult, formatSpecStartResult, formatSpecStatus, startSpecWorktree } = require('./lib/spec-worktrees');
55
55
  const { getContextPathExclusionReason } = require('./lib/ai/safety');
56
+ const { validateUxFlags } = require('./lib/cli/ux-flags');
56
57
  const { relativePosixPath, resolveTargetRoot } = require('./lib/paths');
57
58
  const {
58
59
  CURRENT_SCAN_RELATIVE_PATH,
@@ -306,6 +307,10 @@ Options:
306
307
  --include-templates Export packaged templates in init profile
307
308
  --dry-run Preview init, analyze, migrate, prepare, spec create/start/close, demo, ai agent set, or AI work without executing writes/providers
308
309
  --print-prompt Print the exact AI prompt and exit without executing provider CLIs
310
+ --with-planner Enable planner-assisted behavior on commands that explicitly support it
311
+ --interactive Enable prompts on commands that explicitly support interactive choices
312
+ --review Open or prepare human review before persistent writes where supported
313
+ --no-color Disable ANSI colors in human output
309
314
  --fix For doctor, apply safe non-destructive repairs
310
315
  --execute For ai execute-plan, run the planned slices instead of printing commands
311
316
  --create For ai pr, create the PR after preflight instead of printing the plan only
@@ -417,6 +422,10 @@ function parseArgs(argv) {
417
422
  strict: false,
418
423
  strictOverlap: false,
419
424
  json: false,
425
+ noColor: false,
426
+ withPlanner: false,
427
+ interactive: false,
428
+ review: false,
420
429
  includeCompleted: false,
421
430
  onlyReady: false,
422
431
  allReady: false,
@@ -647,6 +656,26 @@ function parseArgs(argv) {
647
656
  continue;
648
657
  }
649
658
 
659
+ if (arg === '--no-color') {
660
+ result.noColor = true;
661
+ continue;
662
+ }
663
+
664
+ if (arg === '--with-planner') {
665
+ result.withPlanner = true;
666
+ continue;
667
+ }
668
+
669
+ if (arg === '--interactive') {
670
+ result.interactive = true;
671
+ continue;
672
+ }
673
+
674
+ if (arg === '--review') {
675
+ result.review = true;
676
+ continue;
677
+ }
678
+
650
679
  if (arg === '--include-completed') {
651
680
  result.includeCompleted = true;
652
681
  continue;
@@ -2342,6 +2371,8 @@ async function run(argv) {
2342
2371
  return;
2343
2372
  }
2344
2373
 
2374
+ validateUxFlags(args);
2375
+
2345
2376
  if (args.mode === 'analyze') {
2346
2377
  runAnalyze(args.targetDir, {
2347
2378
  dryRun: args.dryRun,
@@ -2481,8 +2512,17 @@ async function run(argv) {
2481
2512
 
2482
2513
  if (args.aiCommand === 'prepare-context') {
2483
2514
  await runAiPrepareContext(process.cwd(), {
2515
+ context: args.aiContext || undefined,
2484
2516
  dryRun: args.dryRun,
2517
+ interactive: args.interactive,
2518
+ printPrompt: args.aiPrintPrompt,
2519
+ provider: args.aiProvider,
2520
+ providerExplicit: args.aiProviderExplicit,
2521
+ review: args.review,
2522
+ role: args.aiRole,
2485
2523
  runId: args.aiRunId || undefined,
2524
+ timeout: args.aiTimeout,
2525
+ withPlanner: args.withPlanner,
2486
2526
  });
2487
2527
  return;
2488
2528
  }
@@ -2496,10 +2536,13 @@ async function run(argv) {
2496
2536
  printPrompt: args.aiPrintPrompt,
2497
2537
  provider: args.aiProvider,
2498
2538
  providerExplicit: args.aiProviderExplicit,
2539
+ interactive: args.interactive,
2540
+ review: args.review,
2499
2541
  role: args.aiRole,
2500
2542
  runId: args.aiRunId || undefined,
2501
2543
  specSlug: args.specSlug || undefined,
2502
2544
  timeout: args.aiTimeout,
2545
+ withPlanner: args.withPlanner,
2503
2546
  });
2504
2547
  return;
2505
2548
  }
@@ -2620,7 +2663,9 @@ async function run(argv) {
2620
2663
  create: args.aiCreate,
2621
2664
  dryRun: args.dryRun,
2622
2665
  input: args.aiInput || undefined,
2666
+ interactive: args.interactive,
2623
2667
  remote: args.aiRemote || undefined,
2668
+ review: args.review,
2624
2669
  sshHostAlias: args.aiSshHostAlias || undefined,
2625
2670
  identityFile: args.aiIdentityFile || undefined,
2626
2671
  title: args.aiTitle || undefined,
@@ -2760,10 +2805,13 @@ async function run(argv) {
2760
2805
 
2761
2806
  if (args.mode === 'spec') {
2762
2807
  if (args.specCommand === 'create') {
2763
- runCreateSpec(process.cwd(), {
2808
+ await runCreateSpec(process.cwd(), {
2764
2809
  dryRun: args.dryRun,
2765
2810
  input: args.aiInput || undefined,
2811
+ interactive: args.interactive,
2812
+ review: args.review,
2766
2813
  specSlug: args.specSlug || undefined,
2814
+ withPlanner: args.withPlanner,
2767
2815
  });
2768
2816
  return;
2769
2817
  }
@@ -0,0 +1,389 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { redactSensitiveLocalValues } = require('./artifacts');
5
+ const { getPreparedContextDocPaths } = require('./context-packs');
6
+ const { contextProposalSchema } = require('./context-proposal.schema');
7
+ const { quiverInternalPaths } = require('../init-layout');
8
+ const {
9
+ getProjectRelativePathIssue,
10
+ validateProjectRelativePath,
11
+ } = require('../paths');
12
+
13
+ const INVALID_PROPOSAL_ARTIFACT_SCHEMA_VERSION = 1;
14
+
15
+ function formatError(message) {
16
+ return `create-quiver: ${message}`;
17
+ }
18
+
19
+ class ContextProposalError extends Error {
20
+ constructor(message, issues = []) {
21
+ super(formatError(message));
22
+ this.name = 'ContextProposalError';
23
+ this.code = 'AI_CONTEXT_PROPOSAL_INVALID';
24
+ this.issues = issues;
25
+ this.safeNextSteps = [
26
+ 'Run deterministic prepare-context without --with-planner if you need the safest fallback.',
27
+ 'Rerun with --with-planner --print-prompt and ask the planner to return only valid JSON.',
28
+ 'Review the blocked paths and keep proposal writes limited to approved docs-only context files.',
29
+ ];
30
+ }
31
+ }
32
+
33
+ function normalizeProposalShape(value) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
35
+ return value;
36
+ }
37
+
38
+ const proposal = { ...value };
39
+ if (!proposal.docs && Array.isArray(proposal.files)) {
40
+ proposal.docs = proposal.files;
41
+ delete proposal.files;
42
+ }
43
+ if (proposal.schemaVersion && !proposal.schema_version) {
44
+ proposal.schema_version = proposal.schemaVersion;
45
+ delete proposal.schemaVersion;
46
+ }
47
+ if (proposal.nextSteps && !proposal.next_steps) {
48
+ proposal.next_steps = proposal.nextSteps;
49
+ delete proposal.nextSteps;
50
+ }
51
+ if (proposal.omittedPaths && !proposal.omitted_paths) {
52
+ proposal.omitted_paths = proposal.omittedPaths;
53
+ delete proposal.omittedPaths;
54
+ }
55
+
56
+ return proposal;
57
+ }
58
+
59
+ function getAllowedContextProposalPaths() {
60
+ return getPreparedContextDocPaths();
61
+ }
62
+
63
+ function classifyDisallowedContextPath(relativePath) {
64
+ const normalized = String(relativePath || '').replace(/\\/g, '/').toLowerCase();
65
+
66
+ if (/^(src|app|apps|packages|lib|server|client|web)\//.test(normalized)) {
67
+ return 'product-code';
68
+ }
69
+ if (/^(package(?:-lock)?|npm-shrinkwrap)\.json$/.test(normalized)
70
+ || /^pnpm-lock\.yaml$/.test(normalized)
71
+ || /^yarn\.lock$/.test(normalized)
72
+ || /^bun\.lockb?$/.test(normalized)) {
73
+ return 'dependency-or-lockfile';
74
+ }
75
+ if (/^(vite|next|webpack|rollup|tsconfig|jsconfig|babel|postcss|tailwind|eslint|prettier|turbo|vercel|netlify|dockerfile|compose)(\.|$)/i.test(normalized)) {
76
+ return 'build-or-runtime-config';
77
+ }
78
+ if (/^(dist|build|coverage|out|generated|gen|artifacts|reports)\//.test(normalized)) {
79
+ return 'generated-output';
80
+ }
81
+ if (/^\.quiver\//.test(normalized)) {
82
+ return 'quiver-internal-state';
83
+ }
84
+ if (!normalized.startsWith('docs/')) {
85
+ return 'not-a-context-doc';
86
+ }
87
+ return 'unapproved-context-doc';
88
+ }
89
+
90
+ function validateContextProposalPath(filePath, options = {}) {
91
+ const issue = getProjectRelativePathIssue(filePath, options.pathLib);
92
+ if (issue) {
93
+ return {
94
+ ok: false,
95
+ path: String(filePath || ''),
96
+ issue,
97
+ message: `path must be project-relative and stay inside the repository (issue=${issue})`,
98
+ };
99
+ }
100
+
101
+ let normalized;
102
+ try {
103
+ normalized = validateProjectRelativePath(filePath, 'planner proposal path', options.pathLib);
104
+ } catch (error) {
105
+ return {
106
+ ok: false,
107
+ path: String(filePath || ''),
108
+ issue: 'invalid-project-relative-path',
109
+ message: error.message,
110
+ };
111
+ }
112
+
113
+ const allowed = new Set(options.allowedPaths || getAllowedContextProposalPaths());
114
+ if (!allowed.has(normalized)) {
115
+ const issueName = classifyDisallowedContextPath(normalized);
116
+ return {
117
+ ok: false,
118
+ path: normalized,
119
+ issue: issueName,
120
+ message: `path is not approved for planner context writes (${issueName}). Allowed paths: ${Array.from(allowed).join(', ')}`,
121
+ };
122
+ }
123
+
124
+ return {
125
+ ok: true,
126
+ path: normalized,
127
+ };
128
+ }
129
+
130
+ function extractBalancedJsonObject(text) {
131
+ const input = String(text || '');
132
+ const start = input.indexOf('{');
133
+ if (start === -1) {
134
+ return null;
135
+ }
136
+
137
+ let depth = 0;
138
+ let quote = null;
139
+ let escaping = false;
140
+
141
+ for (let index = start; index < input.length; index += 1) {
142
+ const char = input[index];
143
+
144
+ if (escaping) {
145
+ escaping = false;
146
+ continue;
147
+ }
148
+ if (char === '\\') {
149
+ escaping = true;
150
+ continue;
151
+ }
152
+ if (quote) {
153
+ if (char === quote) {
154
+ quote = null;
155
+ }
156
+ continue;
157
+ }
158
+ if (char === '"' || char === "'") {
159
+ quote = char;
160
+ continue;
161
+ }
162
+ if (char === '{') {
163
+ depth += 1;
164
+ }
165
+ if (char === '}') {
166
+ depth -= 1;
167
+ if (depth === 0) {
168
+ return input.slice(start, index + 1);
169
+ }
170
+ }
171
+ }
172
+
173
+ return null;
174
+ }
175
+
176
+ function parseJsonCandidate(jsonText, source) {
177
+ try {
178
+ return {
179
+ ok: true,
180
+ source,
181
+ value: JSON.parse(jsonText),
182
+ };
183
+ } catch (error) {
184
+ return {
185
+ ok: false,
186
+ source,
187
+ error,
188
+ };
189
+ }
190
+ }
191
+
192
+ function parsePlannerProposalJson(text) {
193
+ const input = String(text || '').trim();
194
+ if (!input) {
195
+ throw new ContextProposalError('planner proposal output is empty', [
196
+ { path: null, issue: 'empty-output', message: 'The planner returned no proposal JSON.' },
197
+ ]);
198
+ }
199
+
200
+ const direct = parseJsonCandidate(input, 'raw-json');
201
+ if (direct.ok) {
202
+ return direct;
203
+ }
204
+
205
+ const fencedMatches = Array.from(input.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi));
206
+ const parseFailures = [direct];
207
+ for (const match of fencedMatches) {
208
+ const parsed = parseJsonCandidate(match[1].trim(), 'fenced-json');
209
+ if (parsed.ok) {
210
+ return parsed;
211
+ }
212
+ parseFailures.push(parsed);
213
+ }
214
+
215
+ const balanced = extractBalancedJsonObject(input);
216
+ if (balanced) {
217
+ const parsed = parseJsonCandidate(balanced, 'embedded-json');
218
+ if (parsed.ok) {
219
+ return parsed;
220
+ }
221
+ parseFailures.push(parsed);
222
+ }
223
+
224
+ const firstError = parseFailures.find((item) => item.error)?.error;
225
+ throw new ContextProposalError('planner proposal output is not valid JSON', [
226
+ {
227
+ path: null,
228
+ issue: 'malformed-json',
229
+ message: firstError ? firstError.message : 'No JSON object or fenced JSON block could be parsed.',
230
+ },
231
+ ]);
232
+ }
233
+
234
+ function mapZodIssues(error) {
235
+ return error.issues.map((issue) => ({
236
+ path: issue.path.length > 0 ? issue.path.join('.') : null,
237
+ issue: issue.code,
238
+ message: issue.message,
239
+ }));
240
+ }
241
+
242
+ function normalizeStrings(values) {
243
+ return Array.from(new Set((Array.isArray(values) ? values : [])
244
+ .map((item) => String(item || '').trim())
245
+ .filter(Boolean)));
246
+ }
247
+
248
+ function normalizeContextProposal(value, options = {}) {
249
+ const proposal = normalizeProposalShape(value);
250
+ const parsed = contextProposalSchema.safeParse(proposal);
251
+ if (!parsed.success) {
252
+ throw new ContextProposalError('planner context proposal does not match the required schema', mapZodIssues(parsed.error));
253
+ }
254
+
255
+ const issues = [];
256
+ const seenPaths = new Set();
257
+ const docs = [];
258
+
259
+ for (const [index, doc] of parsed.data.docs.entries()) {
260
+ const pathResult = validateContextProposalPath(doc.path, options);
261
+ if (!pathResult.ok) {
262
+ issues.push({
263
+ index,
264
+ path: pathResult.path,
265
+ issue: pathResult.issue,
266
+ message: pathResult.message,
267
+ });
268
+ continue;
269
+ }
270
+
271
+ if (seenPaths.has(pathResult.path)) {
272
+ issues.push({
273
+ index,
274
+ path: pathResult.path,
275
+ issue: 'duplicate-path',
276
+ message: 'planner proposal contains multiple updates for the same context doc path',
277
+ });
278
+ continue;
279
+ }
280
+ seenPaths.add(pathResult.path);
281
+
282
+ if (doc.action !== 'skip' && !doc.content.trim()) {
283
+ issues.push({
284
+ index,
285
+ path: pathResult.path,
286
+ issue: 'missing-content',
287
+ message: 'create/update proposal entries require non-empty content',
288
+ });
289
+ continue;
290
+ }
291
+
292
+ docs.push({
293
+ path: pathResult.path,
294
+ action: doc.action,
295
+ content: doc.content,
296
+ reason: doc.reason,
297
+ assumptions: normalizeStrings(doc.assumptions),
298
+ risks: normalizeStrings(doc.risks),
299
+ source: 'planner-proposal',
300
+ });
301
+ }
302
+
303
+ if (issues.length > 0) {
304
+ throw new ContextProposalError('planner context proposal contains unsafe or ambiguous writes', issues);
305
+ }
306
+
307
+ return {
308
+ schemaVersion: parsed.data.schema_version,
309
+ kind: parsed.data.kind,
310
+ summary: parsed.data.summary,
311
+ assumptions: normalizeStrings(parsed.data.assumptions),
312
+ risks: normalizeStrings(parsed.data.risks),
313
+ omittedPaths: normalizeStrings(parsed.data.omitted_paths),
314
+ nextSteps: normalizeStrings(parsed.data.next_steps),
315
+ docs,
316
+ writePlan: docs
317
+ .filter((doc) => doc.action !== 'skip')
318
+ .map((doc) => ({
319
+ path: doc.path,
320
+ content: doc.content,
321
+ reason: doc.reason,
322
+ source: doc.source,
323
+ })),
324
+ };
325
+ }
326
+
327
+ function parseContextProposalOutput(text, options = {}) {
328
+ const parsed = parsePlannerProposalJson(text);
329
+ return {
330
+ ...normalizeContextProposal(parsed.value, options),
331
+ parseSource: parsed.source,
332
+ };
333
+ }
334
+
335
+ function safeArtifactName(now = new Date()) {
336
+ return `${now.toISOString()
337
+ .replace(/\.\d{3}Z$/, 'z')
338
+ .replace(/[^0-9a-z]+/gi, '-')
339
+ .toLowerCase()
340
+ .replace(/^-+|-+$/g, '')}-invalid-context-proposal.json`;
341
+ }
342
+
343
+ function writeInvalidContextProposalArtifact(projectRoot, runId, rawOutput, error, options = {}) {
344
+ if (!projectRoot) {
345
+ throw new Error(formatError('missing project root for invalid context proposal artifact'));
346
+ }
347
+ if (!runId) {
348
+ throw new Error(formatError('missing AI run id for invalid context proposal artifact'));
349
+ }
350
+
351
+ const now = options.now || new Date();
352
+ const artifactDir = path.join(quiverInternalPaths(projectRoot).runsDir, String(runId), 'raw');
353
+ const filePath = path.join(artifactDir, safeArtifactName(now));
354
+ const artifact = {
355
+ schema_version: INVALID_PROPOSAL_ARTIFACT_SCHEMA_VERSION,
356
+ kind: 'invalid-context-proposal',
357
+ created_at: now.toISOString(),
358
+ ok: false,
359
+ error: {
360
+ code: error?.code || 'AI_CONTEXT_PROPOSAL_INVALID',
361
+ message: error?.message || String(error || 'unknown error'),
362
+ issues: Array.isArray(error?.issues) ? error.issues : [],
363
+ safe_next_steps: Array.isArray(error?.safeNextSteps) ? error.safeNextSteps : [],
364
+ },
365
+ raw_output: redactSensitiveLocalValues(rawOutput, { projectRoot }),
366
+ };
367
+
368
+ fs.mkdirSync(artifactDir, { recursive: true });
369
+ fs.writeFileSync(filePath, `${JSON.stringify(artifact, null, 2)}\n`);
370
+
371
+ return {
372
+ filePath,
373
+ path: path.relative(projectRoot, filePath).split(path.sep).join('/'),
374
+ artifact,
375
+ };
376
+ }
377
+
378
+ module.exports = {
379
+ INVALID_PROPOSAL_ARTIFACT_SCHEMA_VERSION,
380
+ ContextProposalError,
381
+ classifyDisallowedContextPath,
382
+ extractBalancedJsonObject,
383
+ getAllowedContextProposalPaths,
384
+ normalizeContextProposal,
385
+ parseContextProposalOutput,
386
+ parsePlannerProposalJson,
387
+ validateContextProposalPath,
388
+ writeInvalidContextProposalArtifact,
389
+ };
@@ -0,0 +1,31 @@
1
+ const { z } = require('zod');
2
+
3
+ const CONTEXT_PROPOSAL_SCHEMA_VERSION = 1;
4
+ const MAX_DOC_CONTENT_LENGTH = 400_000;
5
+
6
+ const contextProposalDocSchema = z.object({
7
+ path: z.string().trim().min(1, 'doc path is required'),
8
+ action: z.enum(['create', 'update', 'skip']).default('update'),
9
+ content: z.string().max(MAX_DOC_CONTENT_LENGTH).default(''),
10
+ reason: z.string().trim().max(2_000).default('Planner proposed this documentation update.'),
11
+ assumptions: z.array(z.string().trim().min(1)).default([]),
12
+ risks: z.array(z.string().trim().min(1)).default([]),
13
+ });
14
+
15
+ const contextProposalSchema = z.object({
16
+ schema_version: z.literal(CONTEXT_PROPOSAL_SCHEMA_VERSION).default(CONTEXT_PROPOSAL_SCHEMA_VERSION),
17
+ kind: z.literal('quiver-context-proposal').default('quiver-context-proposal'),
18
+ summary: z.string().trim().max(4_000).default(''),
19
+ assumptions: z.array(z.string().trim().min(1)).default([]),
20
+ risks: z.array(z.string().trim().min(1)).default([]),
21
+ docs: z.array(contextProposalDocSchema).min(1, 'at least one docs update is required'),
22
+ omitted_paths: z.array(z.string().trim().min(1)).default([]),
23
+ next_steps: z.array(z.string().trim().min(1)).default([]),
24
+ }).strict();
25
+
26
+ module.exports = {
27
+ CONTEXT_PROPOSAL_SCHEMA_VERSION,
28
+ MAX_DOC_CONTENT_LENGTH,
29
+ contextProposalDocSchema,
30
+ contextProposalSchema,
31
+ };
@@ -0,0 +1,118 @@
1
+ const os = require('os');
2
+ const { spawnSync } = require('child_process');
3
+
4
+ function splitEditorCommand(value) {
5
+ const input = String(value || '').trim();
6
+ if (!input) return null;
7
+ const parts = [];
8
+ let current = '';
9
+ let quote = null;
10
+ let escaping = false;
11
+
12
+ for (const char of input) {
13
+ if (escaping) {
14
+ current += char;
15
+ escaping = false;
16
+ continue;
17
+ }
18
+ if (char === '\\') {
19
+ escaping = true;
20
+ continue;
21
+ }
22
+ if (quote) {
23
+ if (char === quote) {
24
+ quote = null;
25
+ } else {
26
+ current += char;
27
+ }
28
+ continue;
29
+ }
30
+ if (char === '"' || char === "'") {
31
+ quote = char;
32
+ continue;
33
+ }
34
+ if (/\s/.test(char)) {
35
+ if (current) {
36
+ parts.push(current);
37
+ current = '';
38
+ }
39
+ continue;
40
+ }
41
+ current += char;
42
+ }
43
+
44
+ if (current) parts.push(current);
45
+ return parts.length > 0 ? { command: parts[0], args: parts.slice(1) } : null;
46
+ }
47
+
48
+ function defaultEditorForPlatform(platform = os.platform()) {
49
+ return platform === 'win32' ? 'notepad' : 'vi';
50
+ }
51
+
52
+ function resolveEditor(env = process.env, options = {}) {
53
+ const candidates = [
54
+ { source: 'VISUAL', value: env.VISUAL },
55
+ { source: 'EDITOR', value: env.EDITOR },
56
+ ...((options.fallbacks || [defaultEditorForPlatform(options.platform)]).map((value) => ({
57
+ source: 'fallback',
58
+ value,
59
+ }))),
60
+ ];
61
+
62
+ for (const candidate of candidates) {
63
+ const parsed = splitEditorCommand(candidate.value);
64
+ if (parsed) {
65
+ return { ...parsed, source: candidate.source };
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ function openEditor(filePath, options = {}) {
73
+ const editor = options.editor || resolveEditor(options.env || process.env, options);
74
+ if (!editor) {
75
+ return {
76
+ ok: false,
77
+ canceled: true,
78
+ filePath,
79
+ reason: 'No editor configured. Set VISUAL or EDITOR, then rerun the command.',
80
+ };
81
+ }
82
+
83
+ const runner = options.spawnSync || spawnSync;
84
+ const result = runner(editor.command, [...editor.args, filePath], {
85
+ cwd: options.cwd || process.cwd(),
86
+ env: options.env || process.env,
87
+ shell: false,
88
+ stdio: options.stdio || 'inherit',
89
+ });
90
+
91
+ if (result.error) {
92
+ return {
93
+ ok: false,
94
+ canceled: true,
95
+ command: editor.command,
96
+ filePath,
97
+ reason: result.error.message,
98
+ source: editor.source,
99
+ };
100
+ }
101
+
102
+ const status = result.status ?? 0;
103
+ return {
104
+ ok: status === 0,
105
+ canceled: status !== 0,
106
+ command: editor.command,
107
+ exitCode: status,
108
+ filePath,
109
+ source: editor.source,
110
+ };
111
+ }
112
+
113
+ module.exports = {
114
+ defaultEditorForPlatform,
115
+ openEditor,
116
+ resolveEditor,
117
+ splitEditorCommand,
118
+ };