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.
Files changed (51) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +4 -0
  3. package/README_FOR_AI.md +8 -2
  4. package/ROADMAP.md +8 -2
  5. package/docs/CLI_UX_GUIDE.md +64 -4
  6. package/docs/COMMANDS.md.template +7 -1
  7. package/docs/reference/commands.md +4 -0
  8. package/package.json +1 -1
  9. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EVIDENCE_REPORT.md +213 -0
  10. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EXECUTION_PLAN.md +85 -0
  11. package/specs/quiver-v30-interactive-cli-ux-agent-selection/SPEC.md +213 -0
  12. package/specs/quiver-v30-interactive-cli-ux-agent-selection/STATUS.md +31 -0
  13. package/specs/quiver-v30-interactive-cli-ux-agent-selection/pr.md +103 -0
  14. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +33 -0
  15. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +56 -0
  16. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/slice.json +71 -0
  17. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/CLOSURE_BRIEF.md +31 -0
  18. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/EXECUTION_BRIEF.md +54 -0
  19. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/slice.json +69 -0
  20. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/CLOSURE_BRIEF.md +33 -0
  21. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/EXECUTION_BRIEF.md +56 -0
  22. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/slice.json +81 -0
  23. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/CLOSURE_BRIEF.md +32 -0
  24. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/EXECUTION_BRIEF.md +54 -0
  25. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/slice.json +75 -0
  26. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/CLOSURE_BRIEF.md +32 -0
  27. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/EXECUTION_BRIEF.md +57 -0
  28. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/slice.json +85 -0
  29. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/CLOSURE_BRIEF.md +33 -0
  30. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/EXECUTION_BRIEF.md +57 -0
  31. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/slice.json +85 -0
  32. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/CLOSURE_BRIEF.md +35 -0
  33. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/EXECUTION_BRIEF.md +55 -0
  34. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/slice.json +81 -0
  35. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/CLOSURE_BRIEF.md +34 -0
  36. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/EXECUTION_BRIEF.md +55 -0
  37. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/slice.json +85 -0
  38. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/CLOSURE_BRIEF.md +34 -0
  39. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/EXECUTION_BRIEF.md +59 -0
  40. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/slice.json +95 -0
  41. package/src/create-quiver/commands/ai.js +364 -81
  42. package/src/create-quiver/commands/spec.js +106 -6
  43. package/src/create-quiver/index.js +533 -70
  44. package/src/create-quiver/lib/agent-profiles.js +111 -10
  45. package/src/create-quiver/lib/ai/execution-plan.js +106 -8
  46. package/src/create-quiver/lib/ai/executor.js +284 -28
  47. package/src/create-quiver/lib/ai/providers.js +71 -1
  48. package/src/create-quiver/lib/cli/selectors.js +107 -0
  49. package/src/create-quiver/lib/cli/theme.js +5 -2
  50. package/src/create-quiver/lib/cli/ux-flags.js +18 -0
  51. 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 { resolveProfileProvider } = require('../agent-profiles');
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 provider = options.providerExplicit === true || (options.provider && options.providerExplicit !== false)
715
- ? String(options.provider || DEFAULT_EXECUTE_PROVIDER).trim().toLowerCase()
716
- : resolveProfileProvider(canonicalRepoRoot, role, DEFAULT_EXECUTE_PROVIDER);
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: options.slice,
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 (options.runProviderFn || runProvider)(provider, {
788
- prompt,
789
- cwd: canonicalRepoRoot,
790
- timeoutMs,
791
- dryRun: false,
792
- probe: options.probe,
793
- spawn: options.spawn,
794
- tempRoot: options.tempRoot,
795
- tempFileName: options.tempFileName,
796
- tempFilePrefix: options.tempFilePrefix,
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
- throw appendRecovery(annotateProviderError(error, 'execute-slice'), executorContext.slice);
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 = runValidationCommands(
844
- canonicalRepoRoot,
845
- executorContext.validationCommands,
846
- options.runValidationCommandFn,
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 = commitSliceChanges(canonicalRepoRoot, executorContext.slice, finalScopeResult.changedFiles, {
876
- message: options.commitMessage,
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) {