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.
Files changed (92) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +183 -511
  3. package/README_FOR_AI.md +36 -26
  4. package/ROADMAP.md +11 -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 +19 -4
  10. package/docs/GITFLOW_PR_GUIDE.md +70 -0
  11. package/docs/TROUBLESHOOTING.md.template +29 -0
  12. package/docs/WORKFLOW.md.template +13 -12
  13. package/docs/getting-started/linux.md +84 -0
  14. package/docs/getting-started/macos.md +85 -0
  15. package/docs/getting-started/windows-git-bash-wsl.md +78 -0
  16. package/docs/getting-started/windows-powershell.md +96 -0
  17. package/docs/reference/commands.md +94 -0
  18. package/docs/workflows/existing-project.md +131 -0
  19. package/docs/workflows/full-ai-spec-to-pr.md +311 -0
  20. package/docs/workflows/legacy-quiver-project.md +102 -0
  21. package/docs/workflows/new-project.md +76 -0
  22. package/package.json +5 -1
  23. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  24. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  25. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  26. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  27. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  28. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  29. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  30. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  31. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  32. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  33. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  34. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  35. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  36. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  37. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  38. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  39. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  40. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  41. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  42. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  43. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  44. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  45. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  46. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  47. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  48. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  49. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  50. package/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
  51. package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
  52. package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
  53. package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
  54. package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
  55. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
  56. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
  57. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
  58. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
  59. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
  60. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
  61. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
  62. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
  63. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
  64. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
  65. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
  66. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
  67. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
  68. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
  69. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
  70. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
  71. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
  72. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
  73. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
  74. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
  75. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
  76. package/src/create-quiver/commands/ai.js +938 -16
  77. package/src/create-quiver/commands/spec.js +74 -2
  78. package/src/create-quiver/index.js +91 -5
  79. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  80. package/src/create-quiver/lib/ai/context-proposal.js +389 -0
  81. package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
  82. package/src/create-quiver/lib/ai/export-state.js +52 -5
  83. package/src/create-quiver/lib/ai/github.js +14 -2
  84. package/src/create-quiver/lib/ai/plan-review.js +159 -0
  85. package/src/create-quiver/lib/ai/run-state.js +17 -2
  86. package/src/create-quiver/lib/ai/spec-generator.js +15 -0
  87. package/src/create-quiver/lib/cli/editor.js +118 -0
  88. package/src/create-quiver/lib/cli/theme.js +100 -0
  89. package/src/create-quiver/lib/cli/ux-flags.js +151 -0
  90. package/src/create-quiver/lib/cli/ux.js +130 -0
  91. package/src/create-quiver/lib/project-state-resolver.js +195 -1
  92. package/src/create-quiver/lib/spec-worktrees.js +50 -2
@@ -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
+ };
@@ -5,6 +5,7 @@ const { listAgentProfiles } = require('../agent-profiles');
5
5
  const { PLANNER_APPROVAL_PHASES, readPhaseApproval } = require('../approvals');
6
6
  const { collectLayoutReport } = require('../doctor');
7
7
  const {
8
+ collectActiveSliceState,
8
9
  filterSlicesForExecution,
9
10
  groupSlicesBySpec: groupResolvedSlicesBySpec,
10
11
  isBlockedStatus: isCanonicalBlockedStatus,
@@ -296,14 +297,44 @@ function collectWarnings({ graph, layout, specs, slices }) {
296
297
  function collectNextSteps(data) {
297
298
  const activeRun = [...data.runs].reverse().find((run) => run.status !== 'closed');
298
299
  const commands = [];
300
+ const firstSpec = data.specs[0] || null;
301
+ const firstSlice = data.slices[0] || null;
302
+ const activeRunWantsSpecCreate = Boolean(activeRun?.next_command && activeRun.next_command.includes('spec create'));
299
303
 
300
- commands.push({
301
- id: activeRun ? 'continue-active-run' : 'create-ai-run',
302
- command: activeRun ? activeRun.next_command : 'npx create-quiver ai run create --input <requirements.md>',
303
- reason: activeRun ? `Continue AI run ${activeRun.run_id}.` : 'Start a new AI lifecycle run.',
304
- });
304
+ if (activeRun && activeRunWantsSpecCreate && firstSpec) {
305
+ commands.push({
306
+ id: 'validate-existing-spec',
307
+ command: `npx create-quiver spec validate ${firstSpec.path}`,
308
+ reason: `A spec already exists while run ${activeRun.run_id} points to spec creation.`,
309
+ });
310
+ commands.push({
311
+ id: 'find-ready-slice',
312
+ command: 'npx create-quiver next --all-ready',
313
+ reason: 'Find ready slices before creating another spec.',
314
+ });
315
+ if (firstSlice?.slice_json) {
316
+ commands.push({
317
+ id: 'prompt-existing-slice',
318
+ command: `npx create-quiver ai prompt-slice --slice ${firstSlice.slice_json}`,
319
+ reason: 'Prepare a minimal executor prompt for the existing spec.',
320
+ });
321
+ }
322
+ } else {
323
+ commands.push({
324
+ id: activeRun ? 'continue-active-run' : 'create-ai-run',
325
+ command: activeRun ? activeRun.next_command : 'npx create-quiver ai run create --input <requirements.md>',
326
+ reason: activeRun ? `Continue AI run ${activeRun.run_id}.` : 'Start a new AI lifecycle run.',
327
+ });
328
+ }
305
329
 
306
330
  if (data.summary.slices > 0) {
331
+ if (data.active_slice?.reconciliation?.decision && data.active_slice.reconciliation.decision !== 'preserve') {
332
+ commands.push({
333
+ id: 'reconcile-active-slice',
334
+ command: 'npx create-quiver ai active-slice reconcile --dry-run',
335
+ reason: 'Review active-slice state before assigning more execution work.',
336
+ });
337
+ }
307
338
  commands.push({
308
339
  id: 'inspect-slices',
309
340
  command: 'npx create-quiver ai slices list',
@@ -379,6 +410,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
379
410
  const approvals = normalizeApprovals(projectRoot);
380
411
  const progress = summarizeProgress(slices);
381
412
  const evidence = collectEvidenceEntries(slices);
413
+ const activeSlice = collectActiveSliceState(projectRoot, { slices: allSlices });
382
414
  const blockers = normalizedSlices
383
415
  .filter((slice) => slice.blocked_reason || String(slice.status).toLowerCase() === 'blocked')
384
416
  .map((slice) => ({ ref: slice.ref, reason: slice.blocked_reason || 'blocked' }));
@@ -389,6 +421,9 @@ function collectLifecycleExport(projectRoot, options = {}) {
389
421
  if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
390
422
  blockers.push({ ref: 'migration', reason: layout.recommendations.join(' ') });
391
423
  }
424
+ if (activeSlice.reconciliation.decision === 'blocked') {
425
+ blockers.push({ ref: 'active-slice', reason: activeSlice.reconciliation.reason });
426
+ }
392
427
 
393
428
  const exportData = {
394
429
  schema_version: EXPORT_SCHEMA_VERSION,
@@ -413,6 +448,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
413
448
  runs: runs.length,
414
449
  configured_agents: agents.filter((agent) => agent.configured).length,
415
450
  approvals: approvals.length,
451
+ active_slice_sources: activeSlice.sources.length,
416
452
  warnings: 0,
417
453
  },
418
454
  agents,
@@ -437,6 +473,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
437
473
  dry_run_command: 'npx create-quiver migrate --dry-run',
438
474
  },
439
475
  evidence,
476
+ active_slice: activeSlice,
440
477
  warnings: [],
441
478
  blockers,
442
479
  next_steps: [],
@@ -480,6 +517,7 @@ function collectLifecycleExport(projectRoot, options = {}) {
480
517
  blocker: slice.blocked_reason,
481
518
  })),
482
519
  dependencies: graph.edges,
520
+ active_slice: activeSlice,
483
521
  },
484
522
  };
485
523
 
@@ -513,6 +551,15 @@ function formatLifecycleInspect(data) {
513
551
  lines.push(`- ${step.command}`);
514
552
  }
515
553
 
554
+ if (data.active_slice) {
555
+ lines.push(
556
+ '',
557
+ 'Active slice state',
558
+ `- Sources: ${data.active_slice.sources.length}`,
559
+ `- Reconciliation: ${data.active_slice.reconciliation.decision} (${data.active_slice.reconciliation.reason})`,
560
+ );
561
+ }
562
+
516
563
  if (data.dashboard.blockers.length > 0) {
517
564
  lines.push('', 'Blockers');
518
565
  for (const blocker of data.dashboard.blockers) {
@@ -59,7 +59,8 @@ function formatGhInstallGuidance() {
59
59
  'GitHub CLI is not installed.',
60
60
  'macOS: brew install gh',
61
61
  'Linux: follow https://github.com/cli/cli/blob/trunk/docs/install_linux.md or use your distro package manager',
62
- 'Windows: winget install GitHub.cli',
62
+ 'Windows PowerShell: winget install GitHub.cli',
63
+ 'Git Bash/WSL: install gh inside the environment where the command will run, then authenticate there',
63
64
  ].join('\n');
64
65
  }
65
66
 
@@ -89,6 +90,16 @@ function formatShellPathGuidance(optionName, examplePath) {
89
90
  ].join('\n');
90
91
  }
91
92
 
93
+ function formatSshAliasGuidance(alias = '<alias>') {
94
+ const resolvedAlias = alias || '<alias>';
95
+ return [
96
+ 'SSH alias setup:',
97
+ `- macOS/Linux/Git Bash/WSL: edit ~/.ssh/config and add a Host entry such as \`Host ${resolvedAlias}\`, \`HostName github.com\`, \`User git\`, and \`IdentityFile ~/.ssh/<key>\`.`,
98
+ `- Windows PowerShell: edit $HOME\\.ssh\\config and add the same Host entry for ${resolvedAlias}.`,
99
+ `- Verify the alias with: ssh -T ${resolvedAlias}`,
100
+ ].join('\n');
101
+ }
102
+
92
103
  function formatCommandForShell(command, args, quoter) {
93
104
  return `${command} ${args.map(quoter).join(' ')}`;
94
105
  }
@@ -326,7 +337,7 @@ function ensureSshHostAlias(sshHostAlias) {
326
337
  formatActionableError({
327
338
  failure: 'missing SSH host alias. Pass --ssh-host-alias <alias> before opening the PR.',
328
339
  impact: 'Quiver cannot verify which GitHub SSH identity should be used for this PR flow.',
329
- fix: 'macOS/Linux/Git Bash/WSL: add a Host entry in ~/.ssh/config, for example `Host github-work`. Windows PowerShell: add the Host entry in $HOME\\.ssh\\config.',
340
+ fix: formatSshAliasGuidance('github-work'),
330
341
  nextCommand: 'ssh -T <alias>',
331
342
  }),
332
343
  );
@@ -675,6 +686,7 @@ module.exports = {
675
686
  ensureWorktreeReady,
676
687
  findPrBodyCandidates,
677
688
  formatGhInstallGuidance,
689
+ formatSshAliasGuidance,
678
690
  formatPreflightReport,
679
691
  formatPrCreateReport,
680
692
  preflightGitHubPr,