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
@@ -5,7 +5,7 @@ const { assertSupportedProvider, formatProviderList } = require('./ai/providers'
5
5
  const { quiverInternalPaths } = require('./init-layout');
6
6
 
7
7
  const AGENT_PROFILE_ROLES = Object.freeze(['planner', 'executor', 'reviewer', 'doctor']);
8
- const PROFILE_STATE_VERSION = 1;
8
+ const PROFILE_STATE_VERSION = 2;
9
9
 
10
10
  function formatError(message) {
11
11
  return `create-quiver: ${message}`;
@@ -56,9 +56,46 @@ function emptyProfilesState() {
56
56
  return {
57
57
  version: PROFILE_STATE_VERSION,
58
58
  profiles: {},
59
+ profile_sets: {},
59
60
  };
60
61
  }
61
62
 
63
+ function profileSetKey(role) {
64
+ return `${normalizeAgentProfileRole(role)}s`;
65
+ }
66
+
67
+ function normalizeProfileId(value, fallback = 'default') {
68
+ const source = String(value || fallback || 'default').trim().toLowerCase();
69
+ const normalized = source
70
+ .replace(/[^a-z0-9]+/g, '-')
71
+ .replace(/^-+|-+$/g, '');
72
+ return normalized || 'default';
73
+ }
74
+
75
+ function resolveAgentProfileDisplayName(profile = {}) {
76
+ return profile.displayName || profile.model || profile.provider || profile.role || '';
77
+ }
78
+
79
+ function normalizeStoredProfile(profile, role, fallbackId = 'default') {
80
+ const normalizedRole = normalizeAgentProfileRole(role || profile?.role);
81
+ const provider = profile?.provider ? assertSupportedProvider(profile.provider) : '';
82
+ const normalized = {
83
+ id: normalizeProfileId(profile?.id || profile?.label, fallbackId),
84
+ role: normalizedRole,
85
+ provider,
86
+ model: normalizeOptionalText(profile?.model, 'model'),
87
+ label: normalizeOptionalText(profile?.label, 'label'),
88
+ displayName: normalizeOptionalText(profile?.displayName || profile?.display_name, 'displayName'),
89
+ context: normalizeOptionalText(profile?.context, 'context'),
90
+ default: profile?.default === true,
91
+ updated_at: profile?.updated_at || '',
92
+ };
93
+ if (!normalized.displayName) {
94
+ normalized.displayName = resolveAgentProfileDisplayName(normalized);
95
+ }
96
+ return normalized;
97
+ }
98
+
62
99
  function readAgentProfiles(projectRoot) {
63
100
  const filePath = agentProfilesPath(projectRoot);
64
101
  if (!fs.existsSync(filePath)) {
@@ -69,6 +106,8 @@ function readAgentProfiles(projectRoot) {
69
106
  return {
70
107
  version: state.version || PROFILE_STATE_VERSION,
71
108
  profiles: state.profiles && typeof state.profiles === 'object' ? state.profiles : {},
109
+ profile_sets: state.profile_sets && typeof state.profile_sets === 'object' ? state.profile_sets : {},
110
+ updated_at: state.updated_at || undefined,
72
111
  };
73
112
  }
74
113
 
@@ -80,18 +119,53 @@ function writeAgentProfiles(projectRoot, state) {
80
119
  }
81
120
 
82
121
  function getAgentProfile(projectRoot, role) {
122
+ const normalizedRole = normalizeAgentProfileRole(role);
123
+ const profiles = getAgentProfilesForRole(projectRoot, normalizedRole);
124
+ if (profiles.length === 0) return null;
125
+ return profiles.find((profile) => profile.default) || profiles[0];
126
+ }
127
+
128
+ function getAgentProfileById(projectRoot, role, profileId) {
129
+ const normalizedRole = normalizeAgentProfileRole(role);
130
+ const normalizedId = normalizeProfileId(profileId);
131
+ return getAgentProfilesForRole(projectRoot, normalizedRole)
132
+ .find((profile) => profile.id === normalizedId) || null;
133
+ }
134
+
135
+ function getAgentProfilesForRole(projectRoot, role) {
83
136
  const normalizedRole = normalizeAgentProfileRole(role);
84
137
  const state = readAgentProfiles(projectRoot);
85
- return state.profiles[normalizedRole] || null;
138
+ const setKey = profileSetKey(normalizedRole);
139
+ const fromSet = Array.isArray(state.profile_sets[setKey])
140
+ ? state.profile_sets[setKey].map((profile, index) => normalizeStoredProfile(profile, normalizedRole, index === 0 ? 'default' : `${normalizedRole}-${index + 1}`))
141
+ : [];
142
+ const legacy = state.profiles[normalizedRole]
143
+ ? normalizeStoredProfile({ ...state.profiles[normalizedRole], default: true }, normalizedRole, 'default')
144
+ : null;
145
+ const merged = new Map();
146
+
147
+ if (legacy) merged.set(legacy.id, legacy);
148
+ for (const profile of fromSet) {
149
+ merged.set(profile.id, profile);
150
+ }
151
+
152
+ const profiles = Array.from(merged.values()).filter((profile) => profile.provider);
153
+ if (profiles.length > 0 && !profiles.some((profile) => profile.default)) {
154
+ profiles[0].default = true;
155
+ }
156
+ return profiles;
86
157
  }
87
158
 
88
159
  function listAgentProfiles(projectRoot) {
89
- const state = readAgentProfiles(projectRoot);
90
- return AGENT_PROFILE_ROLES.map((role) => ({
91
- role,
92
- configured: Boolean(state.profiles[role]),
93
- profile: state.profiles[role] || null,
94
- }));
160
+ return AGENT_PROFILE_ROLES.map((role) => {
161
+ const profiles = getAgentProfilesForRole(projectRoot, role);
162
+ return {
163
+ role,
164
+ configured: profiles.length > 0,
165
+ profile: profiles.find((profile) => profile.default) || profiles[0] || null,
166
+ profiles,
167
+ };
168
+ });
95
169
  }
96
170
 
97
171
  function setAgentProfile(projectRoot, role, options = {}) {
@@ -109,23 +183,47 @@ function buildAgentProfileState(projectRoot, role, options = {}) {
109
183
  const provider = assertSupportedProvider(options.provider);
110
184
  const model = normalizeOptionalText(options.model, 'model');
111
185
  const label = normalizeOptionalText(options.label, 'label');
186
+ const displayName = normalizeOptionalText(options.displayName || options.display_name, 'displayName');
112
187
  const context = normalizeOptionalText(options.context, 'context');
113
188
  const state = readAgentProfiles(projectRoot);
114
- const current = state.profiles[normalizedRole] || {};
189
+ const currentProfiles = getAgentProfilesForRole(projectRoot, normalizedRole);
190
+ const currentDefault = currentProfiles.find((profile) => profile.default) || currentProfiles[0] || {};
191
+ const id = normalizeProfileId(options.id || currentDefault.id || label || model || provider);
192
+ const current = currentProfiles.find((profile) => profile.id === id) || {};
115
193
  const now = options.now instanceof Date ? options.now.toISOString() : new Date().toISOString();
194
+ const shouldBeDefault = options.default === true || currentProfiles.length === 0 || (options.default !== false && current.default === true);
116
195
  const profile = {
196
+ id,
117
197
  role: normalizedRole,
118
198
  provider,
119
199
  model: model || current.model || '',
120
200
  label: label || current.label || '',
201
+ displayName: displayName || current.displayName || model || label || provider,
121
202
  context: context || current.context || '',
203
+ default: shouldBeDefault,
122
204
  updated_at: now,
123
205
  };
124
206
 
125
207
  state.version = PROFILE_STATE_VERSION;
208
+ const setKey = profileSetKey(normalizedRole);
209
+ const nextProfiles = currentProfiles
210
+ .filter((item) => item.id !== id)
211
+ .map((item) => ({
212
+ ...item,
213
+ default: shouldBeDefault ? false : item.default === true,
214
+ }))
215
+ .concat(profile);
216
+ if (!nextProfiles.some((item) => item.default)) {
217
+ nextProfiles[0].default = true;
218
+ }
219
+ state.profile_sets = {
220
+ ...state.profile_sets,
221
+ [setKey]: nextProfiles,
222
+ };
223
+ const defaultProfile = nextProfiles.find((item) => item.default) || profile;
126
224
  state.profiles = {
127
225
  ...state.profiles,
128
- [normalizedRole]: profile,
226
+ [normalizedRole]: defaultProfile,
129
227
  };
130
228
  state.updated_at = now;
131
229
 
@@ -149,6 +247,9 @@ module.exports = {
149
247
  AGENT_PROFILE_ROLES,
150
248
  PROFILE_STATE_VERSION,
151
249
  agentProfilesPath,
250
+ getAgentProfileById,
251
+ getAgentProfilesForRole,
252
+ resolveAgentProfileDisplayName,
152
253
  formatProviderList,
153
254
  buildAgentProfileState,
154
255
  getAgentProfile,
@@ -1,7 +1,13 @@
1
1
  const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
 
4
- const { resolveProfileProvider } = require('../agent-profiles');
4
+ const {
5
+ getAgentProfile,
6
+ getAgentProfileById,
7
+ resolveAgentProfileDisplayName,
8
+ resolveProfileProvider,
9
+ } = require('../agent-profiles');
10
+ const { createUx } = require('../cli/ux');
5
11
  const { branchDelete, runGit, statusPorcelain, worktreeAdd, worktreePrune, worktreeRemove } = require('../git');
6
12
  const { withLock } = require('../locks');
7
13
  const { safeBranchName, worktreesRootForRepo } = require('../slice');
@@ -19,6 +25,71 @@ function toRelativePath(repoRoot, filePath) {
19
25
  return path.relative(repoRoot, filePath).split(path.sep).join('/');
20
26
  }
21
27
 
28
+ function resolveExecutorRuntimeProfile(repoRoot, options = {}) {
29
+ const explicitProvider = options.providerExplicit === true || (options.provider && options.providerExplicit !== false);
30
+ const explicitModel = String(options.model || '').trim();
31
+
32
+ if (explicitProvider) {
33
+ const provider = String(options.provider || 'codex').trim().toLowerCase();
34
+ return {
35
+ provider,
36
+ model: explicitModel,
37
+ displayName: explicitModel || provider,
38
+ profile: null,
39
+ };
40
+ }
41
+
42
+ const profile = options.executorProfile
43
+ ? getAgentProfileById(repoRoot, 'executor', options.executorProfile)
44
+ : getAgentProfile(repoRoot, 'executor');
45
+ const provider = profile?.provider || resolveProfileProvider(repoRoot, options.role || 'executor', 'codex');
46
+ const model = explicitModel || profile?.model || '';
47
+
48
+ return {
49
+ provider,
50
+ model,
51
+ displayName: profile ? resolveAgentProfileDisplayName(profile) : (model || provider),
52
+ profile,
53
+ };
54
+ }
55
+
56
+ function createCommandUx(options = {}) {
57
+ if (options.ux) {
58
+ return options.ux;
59
+ }
60
+
61
+ return createUx({
62
+ env: options.env || process.env,
63
+ interactive: options.interactive,
64
+ json: options.json,
65
+ noColor: options.noColor,
66
+ prompts: options.prompts,
67
+ spinner: options.spinner,
68
+ stdinIsTTY: options.stdinIsTTY,
69
+ stdoutIsTTY: options.stdoutIsTTY,
70
+ stderrIsTTY: options.stderrIsTTY,
71
+ write: options.write,
72
+ });
73
+ }
74
+
75
+ function shouldShowHumanProgress(ux, options = {}) {
76
+ return options.progress !== false
77
+ && options.dryRun !== true
78
+ && options.json !== true
79
+ && ux?.mode?.decoration === true;
80
+ }
81
+
82
+ async function runWithProgress({ ux, enabled, message, successMessage, failureMessage, run }) {
83
+ if (!enabled) {
84
+ return run();
85
+ }
86
+
87
+ return ux.withSpinner(message, run, {
88
+ successMessage,
89
+ failureMessage,
90
+ });
91
+ }
92
+
22
93
  function normalizeExecutionMode(mode) {
23
94
  const value = String(mode || 'auto').trim().toLowerCase() || 'auto';
24
95
  if (!EXECUTION_MODES.has(value)) {
@@ -316,12 +387,14 @@ function formatHumanExecutionPlan(report) {
316
387
 
317
388
  function formatExecutePlanDryRun(report, options = {}) {
318
389
  const provider = options.resolvedProvider || options.provider || 'codex';
390
+ const model = options.resolvedModel || options.model || '';
319
391
  const commitEnabled = options.commit === true;
320
392
  const executionMode = normalizeExecutionMode(options.mode || options.executionMode);
321
393
  const lines = [
322
394
  'AI execute-plan dry-run',
323
395
  `Execution mode: ${executionMode}`,
324
396
  `Provider: ${provider}`,
397
+ model ? `Model: ${model}` : '',
325
398
  `Commit after each slice: ${commitEnabled ? 'enabled' : 'disabled'}`,
326
399
  `Total slices: ${report.summary.total_slices}`,
327
400
  '',
@@ -334,6 +407,9 @@ function formatExecutePlanDryRun(report, options = {}) {
334
407
  `--slice ${JSON.stringify(slice.slice_path)}`,
335
408
  `--provider ${provider}`,
336
409
  ];
410
+ if (model) {
411
+ parts.push(`--model ${JSON.stringify(model)}`);
412
+ }
337
413
  if (commitEnabled) {
338
414
  parts.push('--commit');
339
415
  }
@@ -363,7 +439,7 @@ function formatExecutePlanDryRun(report, options = {}) {
363
439
  }
364
440
  }
365
441
 
366
- return `${lines.join('\n')}\n`;
442
+ return `${lines.filter(Boolean).join('\n')}\n`;
367
443
  }
368
444
 
369
445
  function buildRecoveryGuidance(ref, workspaces = []) {
@@ -455,8 +531,11 @@ async function runSequentialGroup(repoRoot, level, group, options = {}) {
455
531
  commit: true,
456
532
  context: options.context,
457
533
  dryRun: false,
534
+ executorProfile: options.executorProfile,
458
535
  provider: options.provider,
459
536
  providerExplicit: options.providerExplicit,
537
+ progress: false,
538
+ model: options.model,
460
539
  role: options.role,
461
540
  slice: slice.slice_path,
462
541
  skipWorktreeBranchCheck: true,
@@ -507,8 +586,11 @@ async function runParallelGroupInWorktrees(repoRoot, level, group, options = {})
507
586
  commit: true,
508
587
  context: options.context,
509
588
  dryRun: false,
589
+ executorProfile: options.executorProfile,
510
590
  provider: options.provider,
511
591
  providerExplicit: options.providerExplicit,
592
+ progress: false,
593
+ model: options.model,
512
594
  role: options.role,
513
595
  slice: workspace.slice.slice_path,
514
596
  skipWorktreeBranchCheck: true,
@@ -557,13 +639,15 @@ async function runExecutePlan(repoRoot, options = {}) {
557
639
  const report = collectExecutionPlan(repoRoot, options);
558
640
  const execute = options.execute === true;
559
641
  const executionMode = normalizeExecutionMode(options.mode || options.executionMode);
560
- const provider = options.providerExplicit === true || (options.provider && options.providerExplicit !== false)
561
- ? options.provider
562
- : resolveProfileProvider(repoRoot, options.role || 'executor', 'codex');
642
+ const runtimeProfile = resolveExecutorRuntimeProfile(repoRoot, options);
643
+ const provider = runtimeProfile.provider;
563
644
  const resolvedOptions = {
564
645
  ...options,
565
646
  mode: executionMode,
647
+ model: runtimeProfile.model,
566
648
  provider,
649
+ profile: runtimeProfile,
650
+ resolvedModel: runtimeProfile.model,
567
651
  resolvedProvider: provider,
568
652
  };
569
653
 
@@ -586,13 +670,27 @@ async function runExecutePlan(repoRoot, options = {}) {
586
670
  }
587
671
 
588
672
  const results = [];
673
+ const ux = createCommandUx(options);
674
+ const showProgress = shouldShowHumanProgress(ux, options);
675
+ if (showProgress) {
676
+ ux.heading(`Ejecutando plan de slices con ${runtimeProfile.displayName}`);
677
+ ux.check(`Plan cargado: ${report.summary.total_slices} slice${report.summary.total_slices === 1 ? '' : 's'}`);
678
+ ux.check(`Modo: ${executionMode}`);
679
+ }
589
680
 
590
681
  for (const level of report.ready_levels) {
591
682
  for (const group of level.execution_groups) {
592
683
  try {
593
- const groupResults = executionMode === 'delegated' && group.mode === 'parallel' && group.slice_refs.length > 1
594
- ? await runParallelGroupInWorktrees(repoRoot, level, group, resolvedOptions)
595
- : await runSequentialGroup(repoRoot, level, group, resolvedOptions);
684
+ const groupResults = await runWithProgress({
685
+ ux,
686
+ enabled: showProgress,
687
+ message: `Ejecutando wave ${level.index}: ${group.slice_refs.join(', ')}`,
688
+ successMessage: `Wave ${level.index} completada`,
689
+ failureMessage: `Falló wave ${level.index}`,
690
+ run: () => (executionMode === 'delegated' && group.mode === 'parallel' && group.slice_refs.length > 1
691
+ ? runParallelGroupInWorktrees(repoRoot, level, group, resolvedOptions)
692
+ : runSequentialGroup(repoRoot, level, group, resolvedOptions)),
693
+ });
596
694
  results.push(...groupResults);
597
695
  } catch (error) {
598
696
  const wrapped = new Error(formatError(`ai execute-plan stopped at wave ${level.index} group ${group.slice_refs.join(', ')}: ${error.message || error}`));