create-quiver 0.12.0 → 0.12.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 +44 -0
- package/README.md +49 -17
- package/README_FOR_AI.md +31 -29
- package/ROADMAP.md +15 -3
- package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
- package/docs/COMMANDS.md.template +44 -18
- package/docs/STATUS.md.template +5 -1
- package/docs/WORKFLOW.md.template +13 -11
- package/package.json +9 -3
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
- package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
- package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
- package/src/create-quiver/commands/ai.js +577 -27
- package/src/create-quiver/commands/flow.js +6 -5
- package/src/create-quiver/commands/graph.js +6 -4
- package/src/create-quiver/commands/plan.js +3 -3
- package/src/create-quiver/index.js +328 -12
- package/src/create-quiver/lib/actionable-error.js +27 -0
- package/src/create-quiver/lib/agent-profiles.js +1 -1
- package/src/create-quiver/lib/ai/context-packs.js +4 -0
- package/src/create-quiver/lib/ai/execution-plan.js +7 -1
- package/src/create-quiver/lib/ai/executor.js +270 -20
- package/src/create-quiver/lib/ai/export-state.js +534 -0
- package/src/create-quiver/lib/ai/github.js +83 -0
- package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
- package/src/create-quiver/lib/ai/plan-review.js +5 -2
- package/src/create-quiver/lib/ai/providers.js +4 -3
- package/src/create-quiver/lib/ai/run-state.js +414 -0
- package/src/create-quiver/lib/ai/spec-generator.js +12 -0
- package/src/create-quiver/lib/ai/spec-templates.js +78 -9
- package/src/create-quiver/lib/approvals.js +22 -3
- package/src/create-quiver/lib/demo.js +189 -14
- package/src/create-quiver/lib/doctor.js +75 -0
- package/src/create-quiver/lib/handoff.js +81 -12
- package/src/create-quiver/lib/init-docs.js +24 -6
- package/src/create-quiver/lib/init-layout.js +8 -0
- package/src/create-quiver/lib/json.js +53 -3
- package/src/create-quiver/lib/readiness.js +18 -3
- package/src/create-quiver/lib/scope.js +50 -7
- package/src/create-quiver/lib/slice-graph.js +138 -38
- package/src/create-quiver/lib/slice.js +6 -1
- package/src/create-quiver/lib/spec-worktrees.js +16 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function formatActionableError({ failure, impact, fix, nextCommand } = {}) {
|
|
2
|
+
const lines = [`create-quiver: ${String(failure || 'operation failed').trim()}`];
|
|
3
|
+
|
|
4
|
+
if (impact) {
|
|
5
|
+
lines.push(`Impact: ${String(impact).trim()}`);
|
|
6
|
+
}
|
|
7
|
+
if (fix) {
|
|
8
|
+
lines.push(`Fix: ${String(fix).trim()}`);
|
|
9
|
+
}
|
|
10
|
+
if (nextCommand) {
|
|
11
|
+
lines.push(`Next command: ${String(nextCommand).trim()}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return lines.join('\n');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createActionableError(code, fields = {}, details = {}) {
|
|
18
|
+
const error = new Error(formatActionableError(fields));
|
|
19
|
+
error.code = code;
|
|
20
|
+
error.details = details;
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
createActionableError,
|
|
26
|
+
formatActionableError,
|
|
27
|
+
};
|
|
@@ -4,7 +4,7 @@ const path = require('node:path');
|
|
|
4
4
|
const { assertSupportedProvider, formatProviderList } = require('./ai/providers');
|
|
5
5
|
const { quiverInternalPaths } = require('./init-layout');
|
|
6
6
|
|
|
7
|
-
const AGENT_PROFILE_ROLES = Object.freeze(['planner', 'executor', 'reviewer', '
|
|
7
|
+
const AGENT_PROFILE_ROLES = Object.freeze(['planner', 'executor', 'reviewer', 'doctor']);
|
|
8
8
|
const PROFILE_STATE_VERSION = 1;
|
|
9
9
|
|
|
10
10
|
function formatError(message) {
|
|
@@ -45,9 +45,13 @@ const DEFAULT_CONTEXT_PACK_BY_ROLE = Object.freeze({
|
|
|
45
45
|
|
|
46
46
|
const PACK_ORDER = ['full', 'planning', 'slice', 'minimal'];
|
|
47
47
|
const CONTEXT_PREPARED_DOC_PATHS = Object.freeze([
|
|
48
|
+
'docs/INDEX.md',
|
|
49
|
+
'docs/PROJECT_MAP.md',
|
|
48
50
|
'docs/AI_CONTEXT.md',
|
|
49
51
|
'docs/AI_ONBOARDING_PROMPT.md',
|
|
50
52
|
'docs/CONTEXTO.md',
|
|
53
|
+
'docs/WORKFLOW.md',
|
|
54
|
+
'docs/ARCHITECTURE.md',
|
|
51
55
|
'docs/STATUS.md',
|
|
52
56
|
'docs/DECISIONS.md',
|
|
53
57
|
]);
|
|
@@ -35,6 +35,9 @@ function summarizeSlice(node, repoRoot) {
|
|
|
35
35
|
title: node.title || node.sliceId,
|
|
36
36
|
status: node.status || 'draft',
|
|
37
37
|
files: Array.isArray(node.files) ? node.files : [],
|
|
38
|
+
expected_read_paths: Array.isArray(node.expected_read_paths) ? node.expected_read_paths : [],
|
|
39
|
+
allowed_write_paths: Array.isArray(node.allowed_write_paths) ? node.allowed_write_paths : [],
|
|
40
|
+
validation_hints: Array.isArray(node.validation_hints) ? node.validation_hints : [],
|
|
38
41
|
depends_on: Array.isArray(node.depends_on) ? node.depends_on : [],
|
|
39
42
|
parallel_safe: node.parallel_safe || null,
|
|
40
43
|
parallel_safe_reason: node.parallel_safe_reason || null,
|
|
@@ -274,7 +277,7 @@ function formatHumanExecutionPlan(report) {
|
|
|
274
277
|
|
|
275
278
|
for (const level of report.ready_levels) {
|
|
276
279
|
const modeLabel = level.parallel_ready ? 'parallel-ready' : 'sequential';
|
|
277
|
-
lines.push(`
|
|
280
|
+
lines.push(`Wave ${level.index} (${modeLabel})`);
|
|
278
281
|
lines.push(`Worktree strategy: ${level.worktree_strategy.mode}`);
|
|
279
282
|
if (level.fallback_reason) {
|
|
280
283
|
lines.push(`Fallback: ${level.fallback_reason}`);
|
|
@@ -282,6 +285,7 @@ function formatHumanExecutionPlan(report) {
|
|
|
282
285
|
|
|
283
286
|
for (const slice of level.slices) {
|
|
284
287
|
lines.push(`- ${slice.ref} [${slice.status}]`);
|
|
288
|
+
lines.push(` parallel_safe: ${slice.parallel_safe || 'unspecified'}${slice.parallel_safe_reason ? ` (${slice.parallel_safe_reason})` : ''}`);
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
if (level.conflicts.length > 0) {
|
|
@@ -454,6 +458,7 @@ async function runSequentialGroup(repoRoot, level, group, options = {}) {
|
|
|
454
458
|
providerExplicit: options.providerExplicit,
|
|
455
459
|
role: options.role,
|
|
456
460
|
slice: slice.slice_path,
|
|
461
|
+
skipWorktreeBranchCheck: true,
|
|
457
462
|
timeout: options.timeout,
|
|
458
463
|
});
|
|
459
464
|
results.push({
|
|
@@ -498,6 +503,7 @@ async function runParallelGroupInWorktrees(repoRoot, level, group, options = {})
|
|
|
498
503
|
providerExplicit: options.providerExplicit,
|
|
499
504
|
role: options.role,
|
|
500
505
|
slice: workspace.slice.slice_path,
|
|
506
|
+
skipWorktreeBranchCheck: true,
|
|
501
507
|
timeout: options.timeout,
|
|
502
508
|
});
|
|
503
509
|
const commit = runGit(['rev-parse', 'HEAD'], workspace.worktreePath);
|
|
@@ -5,7 +5,8 @@ const cp = require('node:child_process');
|
|
|
5
5
|
const { buildContextPackMetadata, normalizeRole } = require('./context-packs');
|
|
6
6
|
const { buildProviderInvocation, runProvider } = require('./providers');
|
|
7
7
|
const { resolveProfileProvider } = require('../agent-profiles');
|
|
8
|
-
const { runGit } = require('../git');
|
|
8
|
+
const { currentBranch, runGit } = require('../git');
|
|
9
|
+
const { redactSecrets, truncateText } = require('../evidence');
|
|
9
10
|
const { captureWorktreeSnapshot, validateScopeSnapshot } = require('../scope');
|
|
10
11
|
const { resolveSliceContext } = require('../slice');
|
|
11
12
|
|
|
@@ -77,6 +78,14 @@ function formatList(items) {
|
|
|
77
78
|
return items.map((item) => `- ${item}`);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
function uniqueList(items) {
|
|
82
|
+
return Array.from(new Set((Array.isArray(items) ? items : []).map((item) => String(item)).filter(Boolean)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function escapeRegex(value) {
|
|
86
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
function extractMarkdownHeading(text) {
|
|
81
90
|
const match = String(text || '').match(/^#\s+(.+)$/m);
|
|
82
91
|
return match ? match[1].trim() : '';
|
|
@@ -189,6 +198,9 @@ function buildManualExecutorPrompt({ repoRoot, slicePath, role, context, tokenLi
|
|
|
189
198
|
`- Source: ${specExcerpt.path}`,
|
|
190
199
|
...specExcerpt.lines,
|
|
191
200
|
'',
|
|
201
|
+
'Expected read paths:',
|
|
202
|
+
...formatList(executorContext.expectedReadPaths),
|
|
203
|
+
'',
|
|
192
204
|
'Allowed files:',
|
|
193
205
|
...formatList(executorContext.allowedFiles),
|
|
194
206
|
'',
|
|
@@ -201,6 +213,9 @@ function buildManualExecutorPrompt({ repoRoot, slicePath, role, context, tokenLi
|
|
|
201
213
|
'Validation commands:',
|
|
202
214
|
...formatList(executorContext.validationCommands),
|
|
203
215
|
'',
|
|
216
|
+
'Validation hints:',
|
|
217
|
+
...formatList(executorContext.validationHints),
|
|
218
|
+
'',
|
|
204
219
|
'Exact deliverable expected:',
|
|
205
220
|
'- Implement only this slice.',
|
|
206
221
|
'- Keep the change inside the allowed files.',
|
|
@@ -289,8 +304,10 @@ function buildExecuteSliceContext({ repoRoot, slicePath, role, context }) {
|
|
|
289
304
|
const relativeSlicePath = toRelativePath(canonicalRepoRoot, slice.sliceAbs);
|
|
290
305
|
const relativeBriefPath = toRelativePath(canonicalRepoRoot, briefPath);
|
|
291
306
|
const allowedFiles = Array.isArray(slice.files) ? slice.files.map((file) => String(file)) : [];
|
|
307
|
+
const expectedReadPaths = Array.isArray(slice.expectedReadPaths) ? slice.expectedReadPaths.map((file) => String(file)) : [];
|
|
292
308
|
const acceptance = Array.isArray(slice.acceptance) ? slice.acceptance.map((item) => String(item)) : [];
|
|
293
309
|
const validationCommands = Array.isArray(slice.tests) ? slice.tests.map((item) => String(item)) : [];
|
|
310
|
+
const validationHints = Array.isArray(slice.validationHints) ? slice.validationHints.map((item) => String(item)) : [];
|
|
294
311
|
const mustItems = Array.isArray(slice.json.must) ? slice.json.must.map((item) => String(item)) : [];
|
|
295
312
|
const excludedItems = Array.isArray(slice.json.not_included) ? slice.json.not_included.map((item) => String(item)) : [];
|
|
296
313
|
|
|
@@ -301,6 +318,8 @@ function buildExecuteSliceContext({ repoRoot, slicePath, role, context }) {
|
|
|
301
318
|
`Spec: ${slice.specSlug}`,
|
|
302
319
|
`Slice file: ${relativeSlicePath}`,
|
|
303
320
|
`Execution brief: ${relativeBriefPath}`,
|
|
321
|
+
'Expected read paths:',
|
|
322
|
+
...formatList(expectedReadPaths),
|
|
304
323
|
'Allowed files:',
|
|
305
324
|
...formatList(allowedFiles),
|
|
306
325
|
'Acceptance criteria:',
|
|
@@ -317,6 +336,10 @@ function buildExecuteSliceContext({ repoRoot, slicePath, role, context }) {
|
|
|
317
336
|
sections.push('Not included:', ...formatList(excludedItems));
|
|
318
337
|
}
|
|
319
338
|
|
|
339
|
+
if (validationHints.length > 0) {
|
|
340
|
+
sections.push('Validation hints:', ...formatList(validationHints));
|
|
341
|
+
}
|
|
342
|
+
|
|
320
343
|
sections.push(
|
|
321
344
|
'Constraints:',
|
|
322
345
|
'- Do not commit manually. Quiver can create the slice commit after scope and validation pass when the user enables --commit.',
|
|
@@ -332,8 +355,10 @@ function buildExecuteSliceContext({ repoRoot, slicePath, role, context }) {
|
|
|
332
355
|
briefPath: relativeBriefPath,
|
|
333
356
|
briefText,
|
|
334
357
|
context: pack,
|
|
358
|
+
expectedReadPaths,
|
|
335
359
|
prompt: sections.join('\n\n'),
|
|
336
360
|
slice,
|
|
361
|
+
validationHints,
|
|
337
362
|
validationCommands,
|
|
338
363
|
};
|
|
339
364
|
}
|
|
@@ -407,18 +432,18 @@ function runValidationCommand(command, repoRoot) {
|
|
|
407
432
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
408
433
|
});
|
|
409
434
|
return {
|
|
410
|
-
command,
|
|
435
|
+
command: redactSecrets(command),
|
|
411
436
|
ok: true,
|
|
412
|
-
stdout,
|
|
437
|
+
stdout: redactSecrets(stdout),
|
|
413
438
|
stderr: '',
|
|
414
439
|
exitCode: 0,
|
|
415
440
|
};
|
|
416
441
|
} catch (error) {
|
|
417
442
|
return {
|
|
418
|
-
command,
|
|
443
|
+
command: redactSecrets(command),
|
|
419
444
|
ok: false,
|
|
420
|
-
stdout: error.stdout ? String(error.stdout) : '',
|
|
421
|
-
stderr: error.stderr ? String(error.stderr) : '',
|
|
445
|
+
stdout: redactSecrets(error.stdout ? String(error.stdout) : ''),
|
|
446
|
+
stderr: redactSecrets(error.stderr ? String(error.stderr) : ''),
|
|
422
447
|
exitCode: Number.isInteger(error.status) ? error.status : 1,
|
|
423
448
|
error,
|
|
424
449
|
};
|
|
@@ -428,7 +453,13 @@ function runValidationCommand(command, repoRoot) {
|
|
|
428
453
|
function runValidationCommands(repoRoot, commands, runner = runValidationCommand) {
|
|
429
454
|
const results = [];
|
|
430
455
|
for (const command of commands) {
|
|
431
|
-
const
|
|
456
|
+
const rawResult = runner(command, repoRoot);
|
|
457
|
+
const result = {
|
|
458
|
+
...rawResult,
|
|
459
|
+
command: redactSecrets(rawResult.command || command),
|
|
460
|
+
stderr: redactSecrets(rawResult.stderr || ''),
|
|
461
|
+
stdout: redactSecrets(rawResult.stdout || ''),
|
|
462
|
+
};
|
|
432
463
|
results.push(result);
|
|
433
464
|
if (!result.ok) {
|
|
434
465
|
const details = [
|
|
@@ -492,11 +523,196 @@ function commitSliceChanges(repoRoot, slice, changedFiles, options = {}) {
|
|
|
492
523
|
};
|
|
493
524
|
}
|
|
494
525
|
|
|
526
|
+
function assertCorrectSliceWorktree(repoRoot, slice, options = {}) {
|
|
527
|
+
if (options.skipWorktreeBranchCheck === true) {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const expectedBranch = String(slice.branchName || slice.json.git?.branch_name || '').trim();
|
|
532
|
+
if (!expectedBranch) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const actualBranch = currentBranch(repoRoot);
|
|
537
|
+
if (actualBranch !== expectedBranch) {
|
|
538
|
+
const error = new Error(formatError(`ai execute-slice must run from the slice worktree branch. Current branch: ${actualBranch || '(detached or unavailable)'}. Expected: ${expectedBranch}.`));
|
|
539
|
+
error.code = 'WRONG_WORKTREE';
|
|
540
|
+
error.details = {
|
|
541
|
+
actualBranch,
|
|
542
|
+
expectedBranch,
|
|
543
|
+
slice: slice.sliceRel,
|
|
544
|
+
};
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
actualBranch,
|
|
550
|
+
expectedBranch,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function sliceLifecycleArtifactPaths(repoRoot, slice) {
|
|
555
|
+
const closureAbs = path.join(path.dirname(slice.sliceAbs), 'CLOSURE_BRIEF.md');
|
|
556
|
+
return {
|
|
557
|
+
closure: toRelativePath(repoRoot, closureAbs),
|
|
558
|
+
commandLog: toRelativePath(repoRoot, path.join(slice.specDirAbs, 'COMMAND_LOG.md')),
|
|
559
|
+
evidence: toRelativePath(repoRoot, path.join(slice.specDirAbs, 'EVIDENCE_REPORT.md')),
|
|
560
|
+
sliceJson: toRelativePath(repoRoot, slice.sliceAbs),
|
|
561
|
+
status: toRelativePath(repoRoot, path.join(slice.specDirAbs, 'STATUS.md')),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function renderClosureBrief({ slice, changedFiles, validationResults, completedAt }) {
|
|
566
|
+
const criteria = Array.isArray(slice.acceptance) ? slice.acceptance : [];
|
|
567
|
+
const validationLines = Array.isArray(validationResults) && validationResults.length > 0
|
|
568
|
+
? validationResults.map((result) => `- [x] \`${result.command}\` exited ${result.exitCode}`)
|
|
569
|
+
: ['- [x] No validation commands declared.'];
|
|
570
|
+
|
|
571
|
+
return `${[
|
|
572
|
+
`# CLOSURE BRIEF - ${slice.sliceId}: ${slice.json.title || slice.sliceId}`,
|
|
573
|
+
'',
|
|
574
|
+
'## Summary of Work',
|
|
575
|
+
'',
|
|
576
|
+
`Executed controlled slice closure at ${completedAt}. Quiver validated scope, validation commands, and lifecycle evidence for this slice.`,
|
|
577
|
+
'',
|
|
578
|
+
'## Validation Against Acceptance Criteria',
|
|
579
|
+
'',
|
|
580
|
+
...(criteria.length > 0 ? criteria.map((item) => `- [x] ${item}`) : ['- [x] Slice execution completed with scope validation.']),
|
|
581
|
+
'',
|
|
582
|
+
'## Relevant Changes',
|
|
583
|
+
'',
|
|
584
|
+
...formatList(changedFiles),
|
|
585
|
+
'',
|
|
586
|
+
'## Validation Commands',
|
|
587
|
+
'',
|
|
588
|
+
...validationLines,
|
|
589
|
+
'',
|
|
590
|
+
'## Pending',
|
|
591
|
+
'',
|
|
592
|
+
'None recorded by Quiver.',
|
|
593
|
+
'',
|
|
594
|
+
'## Remaining Risks',
|
|
595
|
+
'',
|
|
596
|
+
'None recorded by Quiver.',
|
|
597
|
+
'',
|
|
598
|
+
'## Future Recommendations',
|
|
599
|
+
'',
|
|
600
|
+
'Review the evidence report and commit diff before opening the PR.',
|
|
601
|
+
'',
|
|
602
|
+
].join('\n')}\n`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function appendSection(filePath, fallbackTitle, section) {
|
|
606
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').trimEnd() : fallbackTitle;
|
|
607
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
608
|
+
fs.writeFileSync(filePath, `${current}\n\n${section.trimEnd()}\n`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function updateStatusMarkdown(filePath, slice, completedAt) {
|
|
612
|
+
const fallback = `# Status - ${slice.specSlug}\n`;
|
|
613
|
+
let text = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : fallback;
|
|
614
|
+
const rowRegex = new RegExp(`(\\|\\s*${escapeRegex(slice.sliceId)}\\s*\\|\\s*)[^|\\n]+(\\|[^\\n]*\\|)`);
|
|
615
|
+
if (rowRegex.test(text)) {
|
|
616
|
+
text = text.replace(rowRegex, '$1Completed $2');
|
|
617
|
+
}
|
|
618
|
+
text = text.replace(/\*\*Current slice:\*\*\s*[^\n]*/i, `**Current slice:** ${slice.sliceId} completed`);
|
|
619
|
+
if (!text.endsWith('\n')) {
|
|
620
|
+
text += '\n';
|
|
621
|
+
}
|
|
622
|
+
const section = [
|
|
623
|
+
'',
|
|
624
|
+
`## Execution Update - ${slice.sliceId}`,
|
|
625
|
+
'',
|
|
626
|
+
`- Status: Completed`,
|
|
627
|
+
`- Completed at: ${completedAt}`,
|
|
628
|
+
`- Source: \`npx create-quiver ai execute-slice --slice ${slice.sliceRel}\``,
|
|
629
|
+
].join('\n');
|
|
630
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
631
|
+
fs.writeFileSync(filePath, `${text}${section}\n`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function updateSliceJson(filePath, completedAt) {
|
|
635
|
+
const json = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
636
|
+
json.status = 'completed';
|
|
637
|
+
json.completed_at = completedAt;
|
|
638
|
+
fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function writeExecutionArtifacts(repoRoot, executorContext, details) {
|
|
642
|
+
const { slice } = executorContext;
|
|
643
|
+
const completedAt = details.completedAt || new Date().toISOString();
|
|
644
|
+
const artifacts = sliceLifecycleArtifactPaths(repoRoot, slice);
|
|
645
|
+
const changedFiles = uniqueList(details.changedFiles);
|
|
646
|
+
const closurePath = path.join(repoRoot, artifacts.closure);
|
|
647
|
+
const evidencePath = path.join(repoRoot, artifacts.evidence);
|
|
648
|
+
const commandLogPath = path.join(repoRoot, artifacts.commandLog);
|
|
649
|
+
const statusPath = path.join(repoRoot, artifacts.status);
|
|
650
|
+
const validationResults = Array.isArray(details.validationResults) ? details.validationResults : [];
|
|
651
|
+
const providerStdout = truncateText(redactSecrets(details.providerOutput?.stdout || ''), 1200).text;
|
|
652
|
+
const providerStderr = truncateText(redactSecrets(details.providerOutput?.stderr || ''), 1200).text;
|
|
653
|
+
|
|
654
|
+
fs.mkdirSync(path.dirname(closurePath), { recursive: true });
|
|
655
|
+
fs.writeFileSync(closurePath, renderClosureBrief({
|
|
656
|
+
slice,
|
|
657
|
+
changedFiles,
|
|
658
|
+
validationResults,
|
|
659
|
+
completedAt,
|
|
660
|
+
}));
|
|
661
|
+
|
|
662
|
+
const validationLines = validationResults.length > 0
|
|
663
|
+
? validationResults.map((result) => `- \`${result.command}\` -> exit ${result.exitCode}`)
|
|
664
|
+
: ['- No validation commands declared.'];
|
|
665
|
+
appendSection(evidencePath, `# Evidence Report - ${slice.specSlug}`, [
|
|
666
|
+
`## ${slice.sliceId} - Execution Evidence`,
|
|
667
|
+
'',
|
|
668
|
+
`- Completed at: ${completedAt}`,
|
|
669
|
+
`- Changed files: ${changedFiles.length}`,
|
|
670
|
+
...changedFiles.map((file) => ` - \`${file}\``),
|
|
671
|
+
`- Scope validation: passed`,
|
|
672
|
+
`- Provider stdout redacted: ${providerStdout ? 'yes' : 'n/a'}`,
|
|
673
|
+
`- Provider stderr redacted: ${providerStderr ? 'yes' : 'n/a'}`,
|
|
674
|
+
'',
|
|
675
|
+
'### Validation',
|
|
676
|
+
'',
|
|
677
|
+
...validationLines,
|
|
678
|
+
'',
|
|
679
|
+
'### Provider Output',
|
|
680
|
+
'',
|
|
681
|
+
'```text',
|
|
682
|
+
providerStdout || 'n/a',
|
|
683
|
+
providerStderr ? `\n${providerStderr}` : '',
|
|
684
|
+
'```',
|
|
685
|
+
].join('\n'));
|
|
686
|
+
|
|
687
|
+
const commandLogRows = [
|
|
688
|
+
`| ${completedAt} | ${slice.sliceId} | \`npx create-quiver ai execute-slice --slice ${slice.sliceRel}\` | passed |`,
|
|
689
|
+
...validationResults.map((result) => `| ${completedAt} | ${slice.sliceId} | \`${result.command}\` | exit ${result.exitCode} |`),
|
|
690
|
+
];
|
|
691
|
+
const commandLogHeader = [
|
|
692
|
+
'# Command Log',
|
|
693
|
+
'',
|
|
694
|
+
'| Timestamp | Slice | Command | Result |',
|
|
695
|
+
'|---|---|---|---|',
|
|
696
|
+
].join('\n');
|
|
697
|
+
const currentCommandLog = fs.existsSync(commandLogPath) ? fs.readFileSync(commandLogPath, 'utf8').trimEnd() : commandLogHeader;
|
|
698
|
+
fs.mkdirSync(path.dirname(commandLogPath), { recursive: true });
|
|
699
|
+
fs.writeFileSync(commandLogPath, `${currentCommandLog}\n${commandLogRows.join('\n')}\n`);
|
|
700
|
+
|
|
701
|
+
updateStatusMarkdown(statusPath, slice, completedAt);
|
|
702
|
+
updateSliceJson(path.join(repoRoot, artifacts.sliceJson), completedAt);
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
completedAt,
|
|
706
|
+
files: Object.values(artifacts),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
495
710
|
async function runExecuteSlice(repoRoot, options = {}) {
|
|
711
|
+
const canonicalRepoRoot = canonicalizeRepoRoot(repoRoot);
|
|
496
712
|
const role = normalizeRole(options.role || DEFAULT_EXECUTE_ROLE);
|
|
497
713
|
const provider = options.providerExplicit === true || (options.provider && options.providerExplicit !== false)
|
|
498
714
|
? String(options.provider || DEFAULT_EXECUTE_PROVIDER).trim().toLowerCase()
|
|
499
|
-
: resolveProfileProvider(
|
|
715
|
+
: resolveProfileProvider(canonicalRepoRoot, role, DEFAULT_EXECUTE_PROVIDER);
|
|
500
716
|
const context = options.context || DEFAULT_EXECUTE_CONTEXT;
|
|
501
717
|
const timeoutMs = normalizeTimeout(options.timeout);
|
|
502
718
|
|
|
@@ -505,7 +721,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
505
721
|
}
|
|
506
722
|
|
|
507
723
|
const executorContext = buildExecuteSliceContext({
|
|
508
|
-
repoRoot,
|
|
724
|
+
repoRoot: canonicalRepoRoot,
|
|
509
725
|
slicePath: options.slice,
|
|
510
726
|
role,
|
|
511
727
|
context,
|
|
@@ -517,7 +733,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
517
733
|
try {
|
|
518
734
|
invocation = buildProviderInvocation(provider, {
|
|
519
735
|
prompt,
|
|
520
|
-
cwd:
|
|
736
|
+
cwd: canonicalRepoRoot,
|
|
521
737
|
timeoutMs,
|
|
522
738
|
});
|
|
523
739
|
} catch (error) {
|
|
@@ -551,7 +767,16 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
551
767
|
return report;
|
|
552
768
|
}
|
|
553
769
|
|
|
554
|
-
|
|
770
|
+
try {
|
|
771
|
+
assertCorrectSliceWorktree(canonicalRepoRoot, executorContext.slice, options);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
throw appendRecovery(error, executorContext.slice);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const beforeSnapshot = captureWorktreeSnapshot(canonicalRepoRoot);
|
|
777
|
+
if (beforeSnapshot.files.length > 0 && options.commit === true) {
|
|
778
|
+
throw appendRecovery(new Error(formatError(`ai execute-slice --commit requires a clean worktree before running. Commit or stash first: ${beforeSnapshot.files.join(', ')}`)), executorContext.slice);
|
|
779
|
+
}
|
|
555
780
|
if (beforeSnapshot.files.length > 0 && options.allowDirty !== true) {
|
|
556
781
|
throw appendRecovery(new Error(formatError(`ai execute-slice requires a clean worktree before running. Commit or stash first: ${beforeSnapshot.files.join(', ')}`)), executorContext.slice);
|
|
557
782
|
}
|
|
@@ -560,7 +785,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
560
785
|
try {
|
|
561
786
|
result = await (options.runProviderFn || runProvider)(provider, {
|
|
562
787
|
prompt,
|
|
563
|
-
cwd:
|
|
788
|
+
cwd: canonicalRepoRoot,
|
|
564
789
|
timeoutMs,
|
|
565
790
|
dryRun: false,
|
|
566
791
|
probe: options.probe,
|
|
@@ -574,17 +799,27 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
574
799
|
}
|
|
575
800
|
|
|
576
801
|
if (result.stdout) {
|
|
577
|
-
process.stdout.write(result.stdout);
|
|
802
|
+
process.stdout.write(redactSecrets(result.stdout));
|
|
578
803
|
}
|
|
579
804
|
if (result.stderr) {
|
|
580
|
-
process.stderr.write(result.stderr);
|
|
805
|
+
process.stderr.write(redactSecrets(result.stderr));
|
|
581
806
|
}
|
|
582
807
|
|
|
583
808
|
if (!result.ok) {
|
|
584
809
|
throw appendRecovery(annotateProviderError(result.error || new Error('provider run failed'), 'execute-slice'), executorContext.slice);
|
|
585
810
|
}
|
|
586
811
|
|
|
587
|
-
const
|
|
812
|
+
const providerOutput = {
|
|
813
|
+
stdout: redactSecrets(result.stdout || ''),
|
|
814
|
+
stderr: redactSecrets(result.stderr || ''),
|
|
815
|
+
};
|
|
816
|
+
const sanitizedResult = {
|
|
817
|
+
...result,
|
|
818
|
+
stdout: providerOutput.stdout,
|
|
819
|
+
stderr: providerOutput.stderr,
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const afterSnapshot = captureWorktreeSnapshot(canonicalRepoRoot);
|
|
588
823
|
let scopeResult;
|
|
589
824
|
try {
|
|
590
825
|
scopeResult = validateScopeSnapshot({
|
|
@@ -596,11 +831,16 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
596
831
|
} catch (error) {
|
|
597
832
|
throw appendRecovery(error, executorContext.slice);
|
|
598
833
|
}
|
|
834
|
+
if (scopeResult.changedFiles.length === 0) {
|
|
835
|
+
const error = new Error(formatError('provider produced no changed files; slice closure was not updated.'));
|
|
836
|
+
error.code = 'NO_CHANGES_TO_CLOSE';
|
|
837
|
+
throw appendRecovery(error, executorContext.slice);
|
|
838
|
+
}
|
|
599
839
|
|
|
600
840
|
let validationResults = [];
|
|
601
841
|
try {
|
|
602
842
|
validationResults = runValidationCommands(
|
|
603
|
-
|
|
843
|
+
canonicalRepoRoot,
|
|
604
844
|
executorContext.validationCommands,
|
|
605
845
|
options.runValidationCommandFn,
|
|
606
846
|
);
|
|
@@ -608,11 +848,18 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
608
848
|
throw appendRecovery(error, executorContext.slice);
|
|
609
849
|
}
|
|
610
850
|
|
|
611
|
-
const
|
|
851
|
+
const artifacts = writeExecutionArtifacts(canonicalRepoRoot, executorContext, {
|
|
852
|
+
changedFiles: scopeResult.changedFiles,
|
|
853
|
+
completedAt: new Date().toISOString(),
|
|
854
|
+
providerOutput,
|
|
855
|
+
validationResults,
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const finalSnapshot = captureWorktreeSnapshot(canonicalRepoRoot);
|
|
612
859
|
let finalScopeResult;
|
|
613
860
|
try {
|
|
614
861
|
finalScopeResult = validateScopeSnapshot({
|
|
615
|
-
allowedFiles: executorContext.allowedFiles,
|
|
862
|
+
allowedFiles: uniqueList([...executorContext.allowedFiles, ...artifacts.files]),
|
|
616
863
|
beforeSnapshot,
|
|
617
864
|
afterSnapshot: finalSnapshot,
|
|
618
865
|
strict: true,
|
|
@@ -624,7 +871,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
624
871
|
let commitResult = null;
|
|
625
872
|
if (options.commit === true) {
|
|
626
873
|
try {
|
|
627
|
-
commitResult = commitSliceChanges(
|
|
874
|
+
commitResult = commitSliceChanges(canonicalRepoRoot, executorContext.slice, finalScopeResult.changedFiles, {
|
|
628
875
|
message: options.commitMessage,
|
|
629
876
|
});
|
|
630
877
|
} catch (error) {
|
|
@@ -649,12 +896,13 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
649
896
|
slice: executorContext.slice.sliceId,
|
|
650
897
|
specSlug: executorContext.slice.specSlug,
|
|
651
898
|
invocation,
|
|
652
|
-
result,
|
|
899
|
+
result: sanitizedResult,
|
|
653
900
|
beforeSnapshot,
|
|
654
901
|
afterSnapshot: finalSnapshot,
|
|
655
902
|
scopeResult: finalScopeResult,
|
|
656
903
|
validationResults,
|
|
657
904
|
commitResult,
|
|
905
|
+
artifacts,
|
|
658
906
|
};
|
|
659
907
|
}
|
|
660
908
|
|
|
@@ -672,6 +920,8 @@ module.exports = {
|
|
|
672
920
|
commitSliceChanges,
|
|
673
921
|
formatExecuteSliceDryRunReport,
|
|
674
922
|
formatExecuteSliceResult,
|
|
923
|
+
assertCorrectSliceWorktree,
|
|
924
|
+
writeExecutionArtifacts,
|
|
675
925
|
runValidationCommand,
|
|
676
926
|
runValidationCommands,
|
|
677
927
|
normalizeTimeout,
|