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
|
@@ -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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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: '
|
|
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,
|