create-quiver 0.14.1 → 0.15.0
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 +16 -0
- package/README.md +4 -0
- package/README_FOR_AI.md +8 -2
- package/ROADMAP.md +8 -2
- package/docs/CLI_UX_GUIDE.md +64 -4
- package/docs/COMMANDS.md.template +7 -1
- package/docs/reference/commands.md +4 -0
- package/package.json +1 -1
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/EVIDENCE_REPORT.md +213 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/EXECUTION_PLAN.md +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/SPEC.md +213 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/STATUS.md +31 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/pr.md +103 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/slice.json +71 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/slice.json +69 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/slice.json +81 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/slice.json +75 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/slice.json +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/slice.json +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/slice.json +81 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/slice.json +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/slice.json +95 -0
- package/src/create-quiver/commands/ai.js +364 -81
- package/src/create-quiver/commands/spec.js +106 -6
- package/src/create-quiver/index.js +533 -70
- package/src/create-quiver/lib/agent-profiles.js +111 -10
- package/src/create-quiver/lib/ai/execution-plan.js +106 -8
- package/src/create-quiver/lib/ai/executor.js +284 -28
- package/src/create-quiver/lib/ai/providers.js +71 -1
- package/src/create-quiver/lib/cli/selectors.js +107 -0
- package/src/create-quiver/lib/cli/theme.js +5 -2
- package/src/create-quiver/lib/cli/ux-flags.js +18 -0
- package/src/create-quiver/lib/cli/ux.js +100 -5
|
@@ -4,11 +4,20 @@ const cp = require('node:child_process');
|
|
|
4
4
|
|
|
5
5
|
const { buildContextPackMetadata, normalizeRole } = require('./context-packs');
|
|
6
6
|
const { buildProviderInvocation, runProvider } = require('./providers');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
getAgentProfile,
|
|
9
|
+
getAgentProfileById,
|
|
10
|
+
getAgentProfilesForRole,
|
|
11
|
+
resolveAgentProfileDisplayName,
|
|
12
|
+
resolveProfileProvider,
|
|
13
|
+
} = require('../agent-profiles');
|
|
14
|
+
const { selectOption } = require('../cli/selectors');
|
|
15
|
+
const { createUx } = require('../cli/ux');
|
|
8
16
|
const { currentBranch, runGit } = require('../git');
|
|
9
17
|
const { redactSecrets, truncateText } = require('../evidence');
|
|
10
18
|
const { captureWorktreeSnapshot, validateScopeSnapshot } = require('../scope');
|
|
11
19
|
const { resolveSliceContext } = require('../slice');
|
|
20
|
+
const { buildGraph, readAllSlices, topoSort } = require('../slice-graph');
|
|
12
21
|
const { validateProjectRelativePaths } = require('../paths');
|
|
13
22
|
|
|
14
23
|
const DEFAULT_EXECUTE_PROVIDER = 'codex';
|
|
@@ -49,6 +58,208 @@ function normalizeTimeout(timeoutMs) {
|
|
|
49
58
|
return parsed;
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
function resolveExecutorRuntimeProfile(repoRoot, role, options = {}) {
|
|
62
|
+
const explicitProvider = options.providerExplicit === true || (options.provider && options.providerExplicit !== false);
|
|
63
|
+
const explicitModel = String(options.model || '').trim();
|
|
64
|
+
|
|
65
|
+
if (explicitProvider) {
|
|
66
|
+
const provider = String(options.provider || DEFAULT_EXECUTE_PROVIDER).trim().toLowerCase();
|
|
67
|
+
return {
|
|
68
|
+
provider,
|
|
69
|
+
model: explicitModel,
|
|
70
|
+
displayName: explicitModel || provider,
|
|
71
|
+
profile: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const profile = options.executorProfile
|
|
76
|
+
? getAgentProfileById(repoRoot, role, options.executorProfile)
|
|
77
|
+
: getAgentProfile(repoRoot, role);
|
|
78
|
+
const provider = profile?.provider || resolveProfileProvider(repoRoot, role, DEFAULT_EXECUTE_PROVIDER);
|
|
79
|
+
const model = explicitModel || profile?.model || '';
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
provider,
|
|
83
|
+
model,
|
|
84
|
+
displayName: profile ? resolveAgentProfileDisplayName(profile) : (model || provider),
|
|
85
|
+
profile,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createCommandUx(options = {}) {
|
|
90
|
+
if (options.ux) {
|
|
91
|
+
return options.ux;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return createUx({
|
|
95
|
+
env: options.env || process.env,
|
|
96
|
+
interactive: options.interactive,
|
|
97
|
+
json: options.json,
|
|
98
|
+
noColor: options.noColor,
|
|
99
|
+
prompts: options.prompts,
|
|
100
|
+
spinner: options.spinner,
|
|
101
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
102
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
103
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
104
|
+
write: options.write,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function selectorOptions(options = {}) {
|
|
109
|
+
return {
|
|
110
|
+
defaultValue: options.defaultValue,
|
|
111
|
+
env: options.env || process.env,
|
|
112
|
+
flag: options.flag,
|
|
113
|
+
interactive: options.interactive,
|
|
114
|
+
promptSelect: options.promptSelect,
|
|
115
|
+
prompts: options.prompts,
|
|
116
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
117
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
118
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function resolveInteractiveExecutorProfile(repoRoot, role, options = {}) {
|
|
123
|
+
if (options.providerExplicit === true || options.provider || options.executorProfile || options.interactive !== true) {
|
|
124
|
+
return options.executorProfile || '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const profiles = getAgentProfilesForRole(repoRoot, role);
|
|
128
|
+
if (profiles.length <= 1) {
|
|
129
|
+
return options.executorProfile || '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const selected = await selectOption(
|
|
133
|
+
'¿Qué Executor querés usar?',
|
|
134
|
+
profiles.map((profile) => ({
|
|
135
|
+
label: resolveAgentProfileDisplayName(profile),
|
|
136
|
+
value: profile.id,
|
|
137
|
+
hint: `${profile.provider}${profile.model ? ` / ${profile.model}` : ''}`,
|
|
138
|
+
})),
|
|
139
|
+
selectorOptions({
|
|
140
|
+
...options,
|
|
141
|
+
defaultValue: profiles.find((profile) => profile.default)?.id || profiles[0].id,
|
|
142
|
+
flag: '--executor',
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return selected.value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isCompletedStatus(status) {
|
|
150
|
+
return ['completed', 'done', 'skipped', 'cancelled'].includes(String(status || '').trim().toLowerCase());
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readySliceOptions(repoRoot) {
|
|
154
|
+
const allSlices = readAllSlices(repoRoot);
|
|
155
|
+
const graph = buildGraph(allSlices);
|
|
156
|
+
topoSort(graph);
|
|
157
|
+
const pending = graph.nodes.filter((node) => !isCompletedStatus(node.status));
|
|
158
|
+
const pendingRefs = new Set(pending.map((node) => node.ref));
|
|
159
|
+
const completedRefs = new Set(graph.nodes.filter((node) => isCompletedStatus(node.status)).map((node) => node.ref));
|
|
160
|
+
const pendingFoundationBySpec = new Set(pending
|
|
161
|
+
.filter((node) => node.sliceId === 'slice-00' || String(node.sliceId || '').startsWith('slice-00-'))
|
|
162
|
+
.map((node) => node.specSlug));
|
|
163
|
+
const incomingByRef = new Map();
|
|
164
|
+
|
|
165
|
+
for (const edge of graph.edges) {
|
|
166
|
+
if (!incomingByRef.has(edge.to)) {
|
|
167
|
+
incomingByRef.set(edge.to, []);
|
|
168
|
+
}
|
|
169
|
+
incomingByRef.get(edge.to).push(edge.from);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return pending
|
|
173
|
+
.filter((node) => {
|
|
174
|
+
if (pendingFoundationBySpec.has(node.specSlug) && !(node.sliceId === 'slice-00' || String(node.sliceId || '').startsWith('slice-00-'))) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const incoming = incomingByRef.get(node.ref) || [];
|
|
178
|
+
return incoming.every((depRef) => completedRefs.has(depRef) || !pendingRefs.has(depRef));
|
|
179
|
+
})
|
|
180
|
+
.sort((left, right) => String(left.ref).localeCompare(String(right.ref)))
|
|
181
|
+
.map((node) => ({
|
|
182
|
+
label: `${node.ref} [${node.status || 'draft'}]`,
|
|
183
|
+
value: toRelativePath(repoRoot, node.slicePath),
|
|
184
|
+
hint: node.title || node.sliceId,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function resolveInteractiveSliceInput(repoRoot, options = {}) {
|
|
189
|
+
if (options.slice) {
|
|
190
|
+
return options.slice;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (options.interactive !== true) {
|
|
194
|
+
throw new Error(formatError('missing required --slice path for ai execute-slice'));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const optionsForSlices = readySliceOptions(repoRoot);
|
|
198
|
+
if (optionsForSlices.length === 0) {
|
|
199
|
+
throw new Error(formatError('no ready slices found for interactive execution. Run `npx create-quiver ai execute-plan --dry-run` to inspect dependencies and blockers.'));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const selected = await selectOption(
|
|
203
|
+
'¿Qué slice querés ejecutar?',
|
|
204
|
+
optionsForSlices,
|
|
205
|
+
selectorOptions({
|
|
206
|
+
...options,
|
|
207
|
+
defaultValue: optionsForSlices.length === 1 ? optionsForSlices[0].value : undefined,
|
|
208
|
+
flag: '--slice',
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
return selected.value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function shouldShowHumanProgress(ux, options = {}) {
|
|
215
|
+
return options.progress !== false
|
|
216
|
+
&& options.dryRun !== true
|
|
217
|
+
&& ux?.mode?.decoration === true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function writeProgressChecks(ux, enabled, title, checks = []) {
|
|
221
|
+
if (!enabled) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
ux.heading(title);
|
|
225
|
+
for (const check of checks) {
|
|
226
|
+
ux.check(check);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runWithProgress({ ux, enabled, message, successMessage, failureMessage, run }) {
|
|
231
|
+
if (!enabled) {
|
|
232
|
+
return run();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return ux.withSpinner(message, run, {
|
|
236
|
+
successMessage,
|
|
237
|
+
failureMessage,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function runProviderWithProgress({ ux, enabled, run }) {
|
|
242
|
+
async function runAndFailOnProviderResult() {
|
|
243
|
+
const result = await run();
|
|
244
|
+
if (result && result.ok === false) {
|
|
245
|
+
const error = new Error(result.error?.message || 'provider run failed');
|
|
246
|
+
error.code = result.error?.code || 'AI_PROVIDER_RUN_FAILED';
|
|
247
|
+
error.providerResult = result;
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return runWithProgress({
|
|
254
|
+
ux,
|
|
255
|
+
enabled,
|
|
256
|
+
message: 'Ejecutando agente...',
|
|
257
|
+
successMessage: 'Agente finalizado',
|
|
258
|
+
failureMessage: 'Fallo ejecutando agente',
|
|
259
|
+
run: runAndFailOnProviderResult,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
52
263
|
function toRelativePath(repoRoot, absolutePath) {
|
|
53
264
|
return path.relative(repoRoot, absolutePath).split(path.sep).join('/');
|
|
54
265
|
}
|
|
@@ -378,11 +589,17 @@ function formatExecuteSliceDryRunReport({ provider, role, contextPack, slice, br
|
|
|
378
589
|
`Prompt transport: ${invocation.promptTransport.mode}`,
|
|
379
590
|
`Prompt length: ${invocation.promptLength} bytes`,
|
|
380
591
|
`Commit after validation: ${commitEnabled ? 'enabled' : 'disabled'}`,
|
|
592
|
+
];
|
|
593
|
+
if (invocation.modelSelection && invocation.modelSelection.model) {
|
|
594
|
+
lines.push(`Model: ${invocation.modelSelection.model}`);
|
|
595
|
+
lines.push(`Model support: ${invocation.modelSelection.supported ? 'supported' : 'unsupported'} (${invocation.modelSelection.reason})`);
|
|
596
|
+
}
|
|
597
|
+
lines.push(
|
|
381
598
|
'Allowed files:',
|
|
382
599
|
...formatList(allowedFiles),
|
|
383
600
|
'Validation commands:',
|
|
384
601
|
...formatList(validationCommands),
|
|
385
|
-
|
|
602
|
+
);
|
|
386
603
|
|
|
387
604
|
return `${lines.join('\n')}\n`;
|
|
388
605
|
}
|
|
@@ -711,19 +928,21 @@ function writeExecutionArtifacts(repoRoot, executorContext, details) {
|
|
|
711
928
|
async function runExecuteSlice(repoRoot, options = {}) {
|
|
712
929
|
const canonicalRepoRoot = canonicalizeRepoRoot(repoRoot);
|
|
713
930
|
const role = normalizeRole(options.role || DEFAULT_EXECUTE_ROLE);
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
931
|
+
const selectedSlice = await resolveInteractiveSliceInput(canonicalRepoRoot, options);
|
|
932
|
+
const selectedExecutorProfile = await resolveInteractiveExecutorProfile(canonicalRepoRoot, role, options);
|
|
933
|
+
const resolvedOptions = {
|
|
934
|
+
...options,
|
|
935
|
+
executorProfile: selectedExecutorProfile || options.executorProfile,
|
|
936
|
+
slice: selectedSlice,
|
|
937
|
+
};
|
|
938
|
+
const runtimeProfile = resolveExecutorRuntimeProfile(canonicalRepoRoot, role, resolvedOptions);
|
|
939
|
+
const provider = runtimeProfile.provider;
|
|
717
940
|
const context = options.context || DEFAULT_EXECUTE_CONTEXT;
|
|
718
941
|
const timeoutMs = normalizeTimeout(options.timeout);
|
|
719
942
|
|
|
720
|
-
if (!options.slice) {
|
|
721
|
-
throw new Error(formatError('missing required --slice path for ai execute-slice'));
|
|
722
|
-
}
|
|
723
|
-
|
|
724
943
|
const executorContext = buildExecuteSliceContext({
|
|
725
944
|
repoRoot: canonicalRepoRoot,
|
|
726
|
-
slicePath:
|
|
945
|
+
slicePath: resolvedOptions.slice,
|
|
727
946
|
role,
|
|
728
947
|
context,
|
|
729
948
|
});
|
|
@@ -736,6 +955,8 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
736
955
|
prompt,
|
|
737
956
|
cwd: canonicalRepoRoot,
|
|
738
957
|
timeoutMs,
|
|
958
|
+
model: runtimeProfile.model,
|
|
959
|
+
enforceModelSelection: false,
|
|
739
960
|
});
|
|
740
961
|
} catch (error) {
|
|
741
962
|
throw annotateProviderError(error, 'execute-slice');
|
|
@@ -753,6 +974,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
753
974
|
allowedFiles: executorContext.allowedFiles,
|
|
754
975
|
validationCommands: executorContext.validationCommands,
|
|
755
976
|
commitEnabled: options.commit === true,
|
|
977
|
+
profile: runtimeProfile,
|
|
756
978
|
};
|
|
757
979
|
process.stdout.write(formatExecuteSliceDryRunReport({
|
|
758
980
|
provider,
|
|
@@ -782,21 +1004,40 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
782
1004
|
throw appendRecovery(new Error(formatError(`ai execute-slice requires a clean worktree before running. Commit or stash first: ${beforeSnapshot.files.join(', ')}`)), executorContext.slice);
|
|
783
1005
|
}
|
|
784
1006
|
|
|
1007
|
+
const ux = createCommandUx(options);
|
|
1008
|
+
const showProgress = shouldShowHumanProgress(ux, options);
|
|
1009
|
+
writeProgressChecks(
|
|
1010
|
+
ux,
|
|
1011
|
+
showProgress,
|
|
1012
|
+
`Ejecutando slice con ${runtimeProfile.displayName}`,
|
|
1013
|
+
['Leyendo slice', 'Validando worktree', 'Preparando prompt'],
|
|
1014
|
+
);
|
|
1015
|
+
|
|
785
1016
|
let result;
|
|
786
1017
|
try {
|
|
787
|
-
result = await (
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1018
|
+
result = await runProviderWithProgress({
|
|
1019
|
+
ux,
|
|
1020
|
+
enabled: showProgress,
|
|
1021
|
+
run: () => (options.runProviderFn || runProvider)(provider, {
|
|
1022
|
+
prompt,
|
|
1023
|
+
cwd: canonicalRepoRoot,
|
|
1024
|
+
timeoutMs,
|
|
1025
|
+
dryRun: false,
|
|
1026
|
+
probe: options.probe,
|
|
1027
|
+
spawn: options.spawn,
|
|
1028
|
+
tempRoot: options.tempRoot,
|
|
1029
|
+
tempFileName: options.tempFileName,
|
|
1030
|
+
tempFilePrefix: options.tempFilePrefix,
|
|
1031
|
+
model: runtimeProfile.model,
|
|
1032
|
+
enforceModelSelection: Boolean(runtimeProfile.model),
|
|
1033
|
+
}),
|
|
797
1034
|
});
|
|
798
1035
|
} catch (error) {
|
|
799
|
-
|
|
1036
|
+
if (error.providerResult) {
|
|
1037
|
+
result = error.providerResult;
|
|
1038
|
+
} else {
|
|
1039
|
+
throw appendRecovery(annotateProviderError(error, 'execute-slice'), executorContext.slice);
|
|
1040
|
+
}
|
|
800
1041
|
}
|
|
801
1042
|
|
|
802
1043
|
if (result.stdout) {
|
|
@@ -840,11 +1081,18 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
840
1081
|
|
|
841
1082
|
let validationResults = [];
|
|
842
1083
|
try {
|
|
843
|
-
validationResults =
|
|
844
|
-
|
|
845
|
-
executorContext.validationCommands,
|
|
846
|
-
|
|
847
|
-
|
|
1084
|
+
validationResults = await runWithProgress({
|
|
1085
|
+
ux,
|
|
1086
|
+
enabled: showProgress && executorContext.validationCommands.length > 0,
|
|
1087
|
+
message: 'Ejecutando validaciones...',
|
|
1088
|
+
successMessage: 'Validaciones completadas',
|
|
1089
|
+
failureMessage: 'Fallaron las validaciones',
|
|
1090
|
+
run: () => runValidationCommands(
|
|
1091
|
+
canonicalRepoRoot,
|
|
1092
|
+
executorContext.validationCommands,
|
|
1093
|
+
options.runValidationCommandFn,
|
|
1094
|
+
),
|
|
1095
|
+
});
|
|
848
1096
|
} catch (error) {
|
|
849
1097
|
throw appendRecovery(error, executorContext.slice);
|
|
850
1098
|
}
|
|
@@ -872,8 +1120,15 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
872
1120
|
let commitResult = null;
|
|
873
1121
|
if (options.commit === true) {
|
|
874
1122
|
try {
|
|
875
|
-
commitResult =
|
|
876
|
-
|
|
1123
|
+
commitResult = await runWithProgress({
|
|
1124
|
+
ux,
|
|
1125
|
+
enabled: showProgress,
|
|
1126
|
+
message: 'Creando commit del slice...',
|
|
1127
|
+
successMessage: 'Commit del slice creado',
|
|
1128
|
+
failureMessage: 'Fallo creando commit del slice',
|
|
1129
|
+
run: () => commitSliceChanges(canonicalRepoRoot, executorContext.slice, finalScopeResult.changedFiles, {
|
|
1130
|
+
message: options.commitMessage,
|
|
1131
|
+
}),
|
|
877
1132
|
});
|
|
878
1133
|
} catch (error) {
|
|
879
1134
|
throw appendRecovery(error, executorContext.slice);
|
|
@@ -894,6 +1149,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
|
|
|
894
1149
|
provider,
|
|
895
1150
|
role,
|
|
896
1151
|
contextPack: executorContext.context.packName,
|
|
1152
|
+
profile: runtimeProfile,
|
|
897
1153
|
slice: executorContext.slice.sliceId,
|
|
898
1154
|
specSlug: executorContext.slice.specSlug,
|
|
899
1155
|
invocation,
|
|
@@ -11,6 +11,8 @@ const PROVIDERS = {
|
|
|
11
11
|
id: 'codex',
|
|
12
12
|
command: 'codex',
|
|
13
13
|
args: ['exec'],
|
|
14
|
+
supportsModelSelection: true,
|
|
15
|
+
modelArgBuilder: (model) => ['--model', model],
|
|
14
16
|
timeoutMs: 10 * 60 * 1000,
|
|
15
17
|
installHint: 'Install the Codex CLI and make sure it is available on PATH.',
|
|
16
18
|
},
|
|
@@ -18,6 +20,8 @@ const PROVIDERS = {
|
|
|
18
20
|
id: 'claude',
|
|
19
21
|
command: 'claude',
|
|
20
22
|
args: ['-p'],
|
|
23
|
+
supportsModelSelection: true,
|
|
24
|
+
modelArgBuilder: (model) => ['--model', model],
|
|
21
25
|
timeoutMs: 10 * 60 * 1000,
|
|
22
26
|
installHint: 'Install the Claude CLI and make sure it is available on PATH.',
|
|
23
27
|
},
|
|
@@ -25,6 +29,8 @@ const PROVIDERS = {
|
|
|
25
29
|
id: 'gemini',
|
|
26
30
|
command: 'gemini',
|
|
27
31
|
args: ['--prompt', ''],
|
|
32
|
+
supportsModelSelection: true,
|
|
33
|
+
modelArgBuilder: (model) => ['--model', model],
|
|
28
34
|
timeoutMs: 10 * 60 * 1000,
|
|
29
35
|
installHint: 'Install the Gemini CLI and make sure it is available on PATH.',
|
|
30
36
|
},
|
|
@@ -61,9 +67,68 @@ function getProviderDefinition(providerId) {
|
|
|
61
67
|
return PROVIDERS[normalized];
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
function normalizeProviderModel(model) {
|
|
71
|
+
const value = String(model || '').trim();
|
|
72
|
+
return value || '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildProviderModelArgs(providerId, model, options = {}) {
|
|
76
|
+
const provider = getProviderDefinition(providerId);
|
|
77
|
+
const normalizedModel = normalizeProviderModel(model);
|
|
78
|
+
const enforce = options.enforce === true;
|
|
79
|
+
|
|
80
|
+
if (!normalizedModel) {
|
|
81
|
+
return {
|
|
82
|
+
model: '',
|
|
83
|
+
supported: Boolean(provider.supportsModelSelection),
|
|
84
|
+
enforced: enforce,
|
|
85
|
+
args: [],
|
|
86
|
+
reason: 'no model selected',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (provider.supportsModelSelection !== true || typeof provider.modelArgBuilder !== 'function') {
|
|
91
|
+
if (enforce) {
|
|
92
|
+
throw new ProviderRunnerError(
|
|
93
|
+
'UNSUPPORTED_PROVIDER_MODEL_SELECTION',
|
|
94
|
+
`Provider '${provider.id}' cannot receive model '${normalizedModel}' through its Quiver adapter. Remove the model selection or update the provider adapter before live execution.`,
|
|
95
|
+
{
|
|
96
|
+
provider: provider.id,
|
|
97
|
+
model: normalizedModel,
|
|
98
|
+
nextSteps: [
|
|
99
|
+
`Run without a model override for provider '${provider.id}'.`,
|
|
100
|
+
`Update the '${provider.id}' provider adapter with model argument support.`,
|
|
101
|
+
'Use --dry-run to inspect the invocation before live execution.',
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
model: normalizedModel,
|
|
109
|
+
supported: false,
|
|
110
|
+
enforced: false,
|
|
111
|
+
args: [],
|
|
112
|
+
reason: 'provider adapter does not support model arguments',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const args = provider.modelArgBuilder(normalizedModel);
|
|
117
|
+
return {
|
|
118
|
+
model: normalizedModel,
|
|
119
|
+
supported: true,
|
|
120
|
+
enforced: enforce,
|
|
121
|
+
args: Array.isArray(args) ? args.map((arg) => String(arg)) : [],
|
|
122
|
+
reason: 'model argument supported',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
64
126
|
function buildProviderInvocation(providerId, options = {}) {
|
|
65
127
|
const provider = getProviderDefinition(providerId);
|
|
66
128
|
const extraArgs = Array.isArray(options.args) ? options.args.map((arg) => String(arg)) : [];
|
|
129
|
+
const modelSelection = buildProviderModelArgs(provider.id, options.model, {
|
|
130
|
+
enforce: options.enforceModelSelection === true,
|
|
131
|
+
});
|
|
67
132
|
const prompt = String(options.prompt ?? '');
|
|
68
133
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? Number(options.timeoutMs) : provider.timeoutMs;
|
|
69
134
|
const cwd = options.cwd ? String(options.cwd) : process.cwd();
|
|
@@ -72,10 +137,11 @@ function buildProviderInvocation(providerId, options = {}) {
|
|
|
72
137
|
return {
|
|
73
138
|
provider: provider.id,
|
|
74
139
|
command: provider.command,
|
|
75
|
-
args: provider.args.concat(extraArgs),
|
|
140
|
+
args: provider.args.concat(modelSelection.args, extraArgs),
|
|
76
141
|
cwd,
|
|
77
142
|
timeoutMs,
|
|
78
143
|
promptLength: Buffer.byteLength(prompt, 'utf8'),
|
|
144
|
+
modelSelection,
|
|
79
145
|
promptTransport: {
|
|
80
146
|
mode: transportMode,
|
|
81
147
|
promptLength: Buffer.byteLength(prompt, 'utf8'),
|
|
@@ -93,6 +159,7 @@ function createDryRunResult(invocation) {
|
|
|
93
159
|
args: invocation.args.slice(),
|
|
94
160
|
cwd: invocation.cwd,
|
|
95
161
|
timeoutMs: invocation.timeoutMs,
|
|
162
|
+
modelSelection: invocation.modelSelection,
|
|
96
163
|
promptTransport: invocation.promptTransport,
|
|
97
164
|
exitCode: 0,
|
|
98
165
|
stdout: '',
|
|
@@ -256,6 +323,7 @@ async function runProvider(providerId, options = {}) {
|
|
|
256
323
|
args: invocation.args.slice(),
|
|
257
324
|
cwd: invocation.cwd,
|
|
258
325
|
timeoutMs: invocation.timeoutMs,
|
|
326
|
+
modelSelection: invocation.modelSelection,
|
|
259
327
|
promptTransport: invocation.promptTransport,
|
|
260
328
|
exitCode: null,
|
|
261
329
|
stdout: '',
|
|
@@ -292,6 +360,7 @@ async function runProvider(providerId, options = {}) {
|
|
|
292
360
|
args: invocation.args.slice(),
|
|
293
361
|
cwd: invocation.cwd,
|
|
294
362
|
timeoutMs: invocation.timeoutMs,
|
|
363
|
+
modelSelection: invocation.modelSelection,
|
|
295
364
|
promptTransport: describePromptTransport(transport),
|
|
296
365
|
exitCode: execution.exitCode,
|
|
297
366
|
signal: execution.signal,
|
|
@@ -309,6 +378,7 @@ module.exports = {
|
|
|
309
378
|
SUPPORTED_PROVIDERS,
|
|
310
379
|
ProviderRunnerError,
|
|
311
380
|
assertSupportedProvider,
|
|
381
|
+
buildProviderModelArgs,
|
|
312
382
|
buildProviderInvocation,
|
|
313
383
|
formatProviderList,
|
|
314
384
|
getProviderDefinition,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const { resolveUxMode } = require('./ux');
|
|
2
|
+
|
|
3
|
+
function formatError(message) {
|
|
4
|
+
return `create-quiver: ${message}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function normalizeSelectorOptions(options = []) {
|
|
8
|
+
return options.map((option) => {
|
|
9
|
+
if (typeof option === 'string') {
|
|
10
|
+
return {
|
|
11
|
+
label: option,
|
|
12
|
+
value: option,
|
|
13
|
+
hint: '',
|
|
14
|
+
default: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
label: String(option.label || option.name || option.id || option.value || ''),
|
|
19
|
+
value: String(option.value || option.id || option.path || option.label || ''),
|
|
20
|
+
hint: option.hint || option.description || '',
|
|
21
|
+
default: option.default === true || option.recommended === true,
|
|
22
|
+
raw: option.raw || option,
|
|
23
|
+
};
|
|
24
|
+
}).filter((option) => option.label && option.value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findOption(options, value) {
|
|
28
|
+
const normalized = String(value || '').trim();
|
|
29
|
+
if (!normalized) return null;
|
|
30
|
+
return options.find((option) => option.value === normalized || option.label === normalized) || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveDefaultOption(options, defaultValue) {
|
|
34
|
+
return findOption(options, defaultValue) || options.find((option) => option.default) || (options.length === 1 ? options[0] : null);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function loadClack(injected) {
|
|
38
|
+
if (injected) return injected;
|
|
39
|
+
return import('@clack/prompts');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function selectOption(message, rawOptions, options = {}) {
|
|
43
|
+
const normalizedOptions = normalizeSelectorOptions(rawOptions);
|
|
44
|
+
if (normalizedOptions.length === 0) {
|
|
45
|
+
throw new Error(formatError(`${options.name || 'selection'} has no available options.`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const explicit = findOption(normalizedOptions, options.value);
|
|
49
|
+
if (explicit) return explicit;
|
|
50
|
+
if (options.value) {
|
|
51
|
+
throw new Error(formatError(`${options.name || 'selection'} option '${options.value}' was not found.`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mode = resolveUxMode({
|
|
55
|
+
interactive: options.interactive === true,
|
|
56
|
+
json: options.json,
|
|
57
|
+
noColor: options.noColor,
|
|
58
|
+
stdinIsTTY: options.stdinIsTTY,
|
|
59
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
60
|
+
stderrIsTTY: options.stderrIsTTY,
|
|
61
|
+
}, options.env || process.env, {
|
|
62
|
+
input: options.input || process.stdin,
|
|
63
|
+
output: options.output || process.stdout,
|
|
64
|
+
error: options.error || process.stderr,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (mode.usePrompts) {
|
|
68
|
+
if (typeof options.promptSelect === 'function') {
|
|
69
|
+
const selected = await options.promptSelect(message, normalizedOptions);
|
|
70
|
+
const match = findOption(normalizedOptions, selected);
|
|
71
|
+
if (!match) {
|
|
72
|
+
throw new Error(formatError(`${options.name || 'selection'} prompt returned unknown option '${selected}'.`));
|
|
73
|
+
}
|
|
74
|
+
return match;
|
|
75
|
+
}
|
|
76
|
+
const clack = await loadClack(options.prompts);
|
|
77
|
+
const selected = await clack.select({
|
|
78
|
+
message,
|
|
79
|
+
options: normalizedOptions.map((option) => ({
|
|
80
|
+
label: option.label,
|
|
81
|
+
value: option.value,
|
|
82
|
+
hint: option.hint || undefined,
|
|
83
|
+
})),
|
|
84
|
+
initialValue: resolveDefaultOption(normalizedOptions, options.defaultValue)?.value,
|
|
85
|
+
});
|
|
86
|
+
if (clack.isCancel && clack.isCancel(selected)) {
|
|
87
|
+
throw new Error(formatError(`${options.name || 'selection'} was canceled.`));
|
|
88
|
+
}
|
|
89
|
+
return findOption(normalizedOptions, selected);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fallback = resolveDefaultOption(normalizedOptions, options.defaultValue);
|
|
93
|
+
if (fallback) return fallback;
|
|
94
|
+
|
|
95
|
+
const flag = options.flag || '--value';
|
|
96
|
+
const available = normalizedOptions.map((option) => option.value).join(', ');
|
|
97
|
+
throw new Error(formatError([
|
|
98
|
+
`${options.name || 'selection'} requires an explicit choice.`,
|
|
99
|
+
`Available options: ${available}.`,
|
|
100
|
+
`Use ${flag} <value> or rerun with --interactive.`,
|
|
101
|
+
].join('\n')));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
normalizeSelectorOptions,
|
|
106
|
+
selectOption,
|
|
107
|
+
};
|
|
@@ -9,8 +9,11 @@ const QUIVER_COLORS = Object.freeze({
|
|
|
9
9
|
const STATUS_COLORS = Object.freeze({
|
|
10
10
|
info: 'sky',
|
|
11
11
|
command: 'blue',
|
|
12
|
+
success: 'blue',
|
|
12
13
|
planner: 'violet',
|
|
13
14
|
review: 'magenta',
|
|
15
|
+
warning: 'magenta',
|
|
16
|
+
error: 'magenta',
|
|
14
17
|
risk: 'magenta',
|
|
15
18
|
muted: 'periwinkle',
|
|
16
19
|
});
|
|
@@ -70,8 +73,8 @@ function resolveTheme(options = {}, env = process.env, tty = {}) {
|
|
|
70
73
|
const useColor = shouldUseColor(options, env, tty);
|
|
71
74
|
const useUnicode = shouldUseUnicode(options, env, tty);
|
|
72
75
|
const symbols = useUnicode
|
|
73
|
-
? { start: '◇', success: '✓', warning: '!', error: '✖', bullet: '•' }
|
|
74
|
-
: { start: '*', success: 'OK', warning: 'WARN', error: 'ERR', bullet: '-' };
|
|
76
|
+
? { start: '◇', section: '◆', success: '✓', warning: '!', error: '✖', bullet: '•' }
|
|
77
|
+
: { start: '*', section: '*', success: 'OK', warning: 'WARN', error: 'ERR', bullet: '-' };
|
|
75
78
|
const theme = {
|
|
76
79
|
colors: QUIVER_COLORS,
|
|
77
80
|
statusColors: STATUS_COLORS,
|
|
@@ -5,6 +5,12 @@ const UX_FLAG_OPTIONS = Object.freeze({
|
|
|
5
5
|
});
|
|
6
6
|
|
|
7
7
|
const UX_FLAG_MATRIX = Object.freeze({
|
|
8
|
+
init: Object.freeze({
|
|
9
|
+
withPlanner: false,
|
|
10
|
+
interactive: true,
|
|
11
|
+
review: false,
|
|
12
|
+
note: 'guided project onboarding choices',
|
|
13
|
+
}),
|
|
8
14
|
'ai prepare-context': Object.freeze({
|
|
9
15
|
withPlanner: true,
|
|
10
16
|
interactive: true,
|
|
@@ -29,6 +35,18 @@ const UX_FLAG_MATRIX = Object.freeze({
|
|
|
29
35
|
review: true,
|
|
30
36
|
note: 'PR body review and interactive PR inputs',
|
|
31
37
|
}),
|
|
38
|
+
'ai execute-slice': Object.freeze({
|
|
39
|
+
withPlanner: false,
|
|
40
|
+
interactive: true,
|
|
41
|
+
review: false,
|
|
42
|
+
note: 'executor profile and ready-slice selection',
|
|
43
|
+
}),
|
|
44
|
+
'ai execute-plan': Object.freeze({
|
|
45
|
+
withPlanner: false,
|
|
46
|
+
interactive: true,
|
|
47
|
+
review: false,
|
|
48
|
+
note: 'execution strategy confirmation and future selectors',
|
|
49
|
+
}),
|
|
32
50
|
});
|
|
33
51
|
|
|
34
52
|
function formatError(message) {
|