flanders 0.8.0 → 0.10.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.
@@ -6,6 +6,8 @@ const FlandersConfig_1 = require("../workspace/FlandersConfig");
6
6
  const PromptHelper_1 = require("../ui/PromptHelper");
7
7
  const skillArtifacts_1 = require("./skillArtifacts");
8
8
  const InstallModelProbe_1 = require("./InstallModelProbe");
9
+ const toolNames_1 = require("../toolNames");
10
+ const fastMode_1 = require("../fastMode");
9
11
  async function promptChoice(ask, args) {
10
12
  try {
11
13
  return await (0, PromptHelper_1.askChoice)(ask, args);
@@ -28,6 +30,17 @@ async function promptText(ask, args) {
28
30
  throw e;
29
31
  }
30
32
  }
33
+ async function promptMultiChoice(ask, args) {
34
+ try {
35
+ return await (0, PromptHelper_1.askMultiChoice)(ask, args);
36
+ }
37
+ catch (e) {
38
+ if (e instanceof Error && e.name === "AbortError") {
39
+ return null;
40
+ }
41
+ throw e;
42
+ }
43
+ }
31
44
  function extractFlagValue(rawArgs, flag) {
32
45
  const prefix = flag + "=";
33
46
  for (const arg of rawArgs) {
@@ -44,10 +57,43 @@ function validateClosedSet(value, allowed, flagName) {
44
57
  return `Invalid value for ${flagName}: "${value}". Allowed values: ${allowed.join(", ")}.\n`;
45
58
  }
46
59
  const CODEX_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
60
+ function validateEffortForTool(value, tool, flagName) {
61
+ if (value === "") {
62
+ return null;
63
+ }
64
+ if (tool === "codex") {
65
+ return validateClosedSet(value, CODEX_EFFORT_LEVELS, flagName);
66
+ }
67
+ return null;
68
+ }
69
+ function parseSkillsToolList(value) {
70
+ const invalid = {
71
+ ok: false,
72
+ diagnostic: `Invalid value for --skills-tool: "${value}". Expected a comma-separated list of distinct names from: ${toolNames_1.TOOL_NAMES.join(", ")}.\n`
73
+ };
74
+ const seen = new Set();
75
+ const tools = [];
76
+ for (const part of value.split(",")) {
77
+ if (!toolNames_1.TOOL_NAMES.includes(part) || seen.has(part)) {
78
+ return invalid;
79
+ }
80
+ seen.add(part);
81
+ tools.push(part);
82
+ }
83
+ return { ok: true, tools };
84
+ }
85
+ const TOOL_CHOICE_OPTIONS = [
86
+ { label: "claude", description: "Use Claude Code" },
87
+ { label: "codex", description: "Use Codex CLI" }
88
+ ];
89
+ const SKILLS_TOOL_DESTINATIONS = {
90
+ claude: { project: ".claude/skills/", global: "~/.claude/skills/" },
91
+ codex: { project: ".codex/prompts/", global: "~/.codex/prompts/" }
92
+ };
47
93
  const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
48
94
  const CLAUDE_MODEL_FAMILIES = [
49
95
  {
50
- family: "Opus",
96
+ name: "Opus",
51
97
  entries: [
52
98
  { label: "Latest Opus", value: "opus" },
53
99
  { label: "Latest Opus [1m context]", value: "opus[1m]" },
@@ -60,10 +106,11 @@ const CLAUDE_MODEL_FAMILIES = [
60
106
  ]
61
107
  },
62
108
  {
63
- family: "Sonnet",
109
+ name: "Sonnet",
64
110
  entries: [
65
111
  { label: "Latest Sonnet", value: "sonnet" },
66
112
  { label: "Latest Sonnet [1m context]", value: "sonnet[1m]" },
113
+ { label: "Sonnet 5", value: "claude-sonnet-5" },
67
114
  { label: "Sonnet 4.6", value: "claude-sonnet-4-6" },
68
115
  { label: "Sonnet 4.6 [1m context]", value: "claude-sonnet-4-6[1m]" },
69
116
  { label: "Sonnet 4.5", value: "claude-sonnet-4-5" },
@@ -71,14 +118,14 @@ const CLAUDE_MODEL_FAMILIES = [
71
118
  ]
72
119
  },
73
120
  {
74
- family: "Haiku",
121
+ name: "Haiku",
75
122
  entries: [
76
123
  { label: "Latest Haiku", value: "haiku" },
77
124
  { label: "Haiku 4.5", value: "claude-haiku-4-5-20251001" }
78
125
  ]
79
126
  },
80
127
  {
81
- family: "Fable",
128
+ name: "Fable",
82
129
  entries: [
83
130
  { label: "Latest Fable", value: "fable" },
84
131
  { label: "Fable 5", value: "claude-fable-5" }
@@ -88,9 +135,38 @@ const CLAUDE_MODEL_FAMILIES = [
88
135
  const CLAUDE_CROSS_FAMILY_ALIASES = [
89
136
  { label: "Best (auto-pick)", value: "best" }
90
137
  ];
91
- const CLAUDE_BACK_LABEL = "← back";
138
+ const MODEL_BACK_LABEL = "← back";
92
139
  const REVIEWER_INDEXED_RE = /^--reviewer-(\d+)-(tool|model|effort)=/;
93
140
  const REVIEWER_OPTIONAL_INDEXED_RE = /^--reviewer-(\d+)-optional$/;
141
+ const REVIEWER_FAST_INDEXED_RE = /^--reviewer-(\d+)-fast$/;
142
+ function validateFastFlagsForReviewerCount(fastReviewerIndices, reviewerCount) {
143
+ if (fastReviewerIndices !== undefined) {
144
+ for (const idx of fastReviewerIndices) {
145
+ if (idx > reviewerCount) {
146
+ return `Invalid reviewer flag: --reviewer-${idx}-fast references reviewer ${idx}, beyond the configured reviewer list of ${reviewerCount}.\n`;
147
+ }
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+ function fastFlagEligibilityError(tool, model, flagName) {
153
+ const reason = tool === "claude"
154
+ ? `the model "${model}" does not support Claude Code fast mode`
155
+ : `the ${tool} tool has no fast mode`;
156
+ return `Invalid flag ${flagName}: ${reason}.\n`;
157
+ }
158
+ function knownFastFlagError(tool, model, flagName) {
159
+ if (tool === undefined) {
160
+ return null;
161
+ }
162
+ if (tool !== "claude") {
163
+ return fastFlagEligibilityError(tool, "", flagName);
164
+ }
165
+ if (model === undefined) {
166
+ return null;
167
+ }
168
+ return (0, fastMode_1.modelSupportsFastMode)(model) ? null : fastFlagEligibilityError(tool, model, flagName);
169
+ }
94
170
  function validateWeightedFlagsForReviewerCount(reviewerMinimum, optionalReviewerIndices, reviewerCount) {
95
171
  if (reviewerCount === 1) {
96
172
  if (reviewerMinimum !== undefined) {
@@ -132,14 +208,14 @@ function parseInstallFlags(rawArgs) {
132
208
  answers.scope = "global";
133
209
  const skillsTool = extractFlagValue(rawArgs, "--skills-tool");
134
210
  if (skillsTool !== undefined) {
135
- const error = validateClosedSet(skillsTool, ["claude", "codex", "both"], "--skills-tool");
136
- if (error)
137
- return { ok: false, diagnostic: error };
138
- answers.skillsTool = skillsTool;
211
+ const result = parseSkillsToolList(skillsTool);
212
+ if (!result.ok)
213
+ return { ok: false, diagnostic: result.diagnostic };
214
+ answers.skillsTools = result.tools;
139
215
  }
140
216
  const workerTool = extractFlagValue(rawArgs, "--worker-tool");
141
217
  if (workerTool !== undefined) {
142
- const error = validateClosedSet(workerTool, ["claude", "codex"], "--worker-tool");
218
+ const error = validateClosedSet(workerTool, toolNames_1.TOOL_NAMES, "--worker-tool");
143
219
  if (error)
144
220
  return { ok: false, diagnostic: error };
145
221
  answers.workerTool = workerTool;
@@ -150,11 +226,9 @@ function parseInstallFlags(rawArgs) {
150
226
  }
151
227
  const workerEffort = extractFlagValue(rawArgs, "--worker-effort");
152
228
  if (workerEffort !== undefined) {
153
- if (workerEffort !== "" && answers.workerTool === "codex") {
154
- const error = validateClosedSet(workerEffort, CODEX_EFFORT_LEVELS, "--worker-effort");
155
- if (error)
156
- return { ok: false, diagnostic: error };
157
- }
229
+ const error = validateEffortForTool(workerEffort, answers.workerTool, "--worker-effort");
230
+ if (error)
231
+ return { ok: false, diagnostic: error };
158
232
  answers.workerEffort = workerEffort;
159
233
  }
160
234
  const reviewerIndices = new Map();
@@ -162,7 +236,7 @@ function parseInstallFlags(rawArgs) {
162
236
  const reviewer1Model = extractFlagValue(rawArgs, "--reviewer-model");
163
237
  const reviewer1Effort = extractFlagValue(rawArgs, "--reviewer-effort");
164
238
  if (reviewer1Tool !== undefined) {
165
- const error = validateClosedSet(reviewer1Tool, ["claude", "codex"], "--reviewer-tool");
239
+ const error = validateClosedSet(reviewer1Tool, toolNames_1.TOOL_NAMES, "--reviewer-tool");
166
240
  if (error)
167
241
  return { ok: false, diagnostic: error };
168
242
  if (!reviewerIndices.has(1))
@@ -176,11 +250,9 @@ function parseInstallFlags(rawArgs) {
176
250
  }
177
251
  if (reviewer1Effort !== undefined) {
178
252
  const tool1 = (_a = reviewerIndices.get(1)) === null || _a === void 0 ? void 0 : _a.tool;
179
- if (reviewer1Effort !== "" && tool1 === "codex") {
180
- const error = validateClosedSet(reviewer1Effort, CODEX_EFFORT_LEVELS, "--reviewer-effort");
181
- if (error)
182
- return { ok: false, diagnostic: error };
183
- }
253
+ const error = validateEffortForTool(reviewer1Effort, tool1, "--reviewer-effort");
254
+ if (error)
255
+ return { ok: false, diagnostic: error };
184
256
  if (!reviewerIndices.has(1))
185
257
  reviewerIndices.set(1, {});
186
258
  reviewerIndices.get(1).effort = reviewer1Effort;
@@ -199,7 +271,7 @@ function parseInstallFlags(rawArgs) {
199
271
  reviewerIndices.set(idx, {});
200
272
  const entry = reviewerIndices.get(idx);
201
273
  if (field === "tool") {
202
- const error = validateClosedSet(value, ["claude", "codex"], `--reviewer-${idx}-tool`);
274
+ const error = validateClosedSet(value, toolNames_1.TOOL_NAMES, `--reviewer-${idx}-tool`);
203
275
  if (error)
204
276
  return { ok: false, diagnostic: error };
205
277
  entry.tool = value;
@@ -212,8 +284,8 @@ function parseInstallFlags(rawArgs) {
212
284
  }
213
285
  }
214
286
  for (const [idx, entry] of reviewerIndices) {
215
- if (entry.effort !== undefined && entry.effort !== "" && entry.tool === "codex") {
216
- const error = validateClosedSet(entry.effort, CODEX_EFFORT_LEVELS, `--reviewer-${idx}-effort`);
287
+ if (entry.effort !== undefined) {
288
+ const error = validateEffortForTool(entry.effort, entry.tool, `--reviewer-${idx}-effort`);
217
289
  if (error)
218
290
  return { ok: false, diagnostic: error };
219
291
  }
@@ -243,14 +315,40 @@ function parseInstallFlags(rawArgs) {
243
315
  continue;
244
316
  }
245
317
  const idx = Number(match[1]);
246
- if (idx < 1) {
247
- return { ok: false, diagnostic: `Invalid reviewer flag: "${arg}". --reviewer-N-optional requires N >= 1.\n` };
318
+ if (idx < 2) {
319
+ return { ok: false, diagnostic: `Invalid reviewer flag: "${arg}". Reviewer 1 uses --reviewer-optional; --reviewer-N-optional requires N >= 2.\n` };
248
320
  }
249
321
  optionalIndices.add(idx);
250
322
  }
251
323
  if (optionalIndices.size > 0) {
252
324
  answers.optionalReviewerIndices = [...optionalIndices].sort((a, b) => a - b);
253
325
  }
326
+ if (rawArgs.includes("--worker-fast")) {
327
+ answers.workerFast = true;
328
+ const workerFastError = knownFastFlagError(answers.workerTool, answers.workerModel, "--worker-fast");
329
+ if (workerFastError !== null) {
330
+ return { ok: false, diagnostic: workerFastError };
331
+ }
332
+ }
333
+ const fastIndices = new Set();
334
+ for (const arg of rawArgs) {
335
+ if (arg === "--reviewer-fast") {
336
+ fastIndices.add(1);
337
+ continue;
338
+ }
339
+ const match = arg.match(REVIEWER_FAST_INDEXED_RE);
340
+ if (match === null) {
341
+ continue;
342
+ }
343
+ const idx = Number(match[1]);
344
+ if (idx < 2) {
345
+ return { ok: false, diagnostic: `Invalid reviewer flag: "${arg}". Reviewer 1 uses --reviewer-fast; --reviewer-N-fast requires N >= 2.\n` };
346
+ }
347
+ fastIndices.add(idx);
348
+ }
349
+ if (fastIndices.size > 0) {
350
+ answers.fastReviewerIndices = [...fastIndices].sort((a, b) => a - b);
351
+ }
254
352
  const minimumRaw = extractFlagValue(rawArgs, "--reviewer-minimum");
255
353
  if (minimumRaw !== undefined) {
256
354
  if (!/^\d+$/.test(minimumRaw)) {
@@ -263,6 +361,20 @@ function parseInstallFlags(rawArgs) {
263
361
  if (diagnostic !== null) {
264
362
  return { ok: false, diagnostic };
265
363
  }
364
+ const fastDiagnostic = validateFastFlagsForReviewerCount(answers.fastReviewerIndices, answers.reviewers.length);
365
+ if (fastDiagnostic !== null) {
366
+ return { ok: false, diagnostic: fastDiagnostic };
367
+ }
368
+ if (answers.fastReviewerIndices !== undefined) {
369
+ for (const idx of answers.fastReviewerIndices) {
370
+ const supplied = answers.reviewers[idx - 1];
371
+ const flagName = idx === 1 ? "--reviewer-fast" : `--reviewer-${idx}-fast`;
372
+ const reviewerFastError = knownFastFlagError(supplied.tool, supplied.model, flagName);
373
+ if (reviewerFastError !== null) {
374
+ return { ok: false, diagnostic: reviewerFastError };
375
+ }
376
+ }
377
+ }
266
378
  }
267
379
  return { ok: true, answers };
268
380
  }
@@ -325,27 +437,27 @@ class Install {
325
437
  }
326
438
  return option.label;
327
439
  }
328
- async _resolveClaudeModel(roleLabel, headerLabel, contexts, preselect) {
440
+ async _resolveGroupedModel(roleLabel, headerLabel, groups, crossAliases, contexts, preselect) {
329
441
  const question = `Which model should ${roleLabel} use?`;
330
442
  let topDefault;
331
443
  let customDefault;
332
- let preselectedFamily;
444
+ let preselectedGroup;
333
445
  let preselectedEntryLabel;
334
446
  if (preselect !== undefined) {
335
447
  if (preselect === "") {
336
448
  topDefault = "default configured model";
337
449
  }
338
450
  else {
339
- const alias = CLAUDE_CROSS_FAMILY_ALIASES.find(a => a.value === preselect);
451
+ const alias = crossAliases.find(a => a.value === preselect);
340
452
  if (alias) {
341
453
  topDefault = alias.label;
342
454
  }
343
455
  else {
344
- const family = CLAUDE_MODEL_FAMILIES.find(f => f.entries.some(e => e.value === preselect));
345
- if (family) {
346
- topDefault = family.family;
347
- preselectedFamily = family;
348
- preselectedEntryLabel = family.entries.find(e => e.value === preselect).label;
456
+ const group = groups.find(g => g.entries.some(e => e.value === preselect));
457
+ if (group) {
458
+ topDefault = group.name;
459
+ preselectedGroup = group;
460
+ preselectedEntryLabel = group.entries.find(e => e.value === preselect).label;
349
461
  }
350
462
  else {
351
463
  topDefault = "enter a custom value…";
@@ -356,8 +468,8 @@ class Install {
356
468
  }
357
469
  topLevel: for (;;) {
358
470
  const topOptions = [
359
- ...CLAUDE_MODEL_FAMILIES.map(family => ({ label: family.family })),
360
- ...CLAUDE_CROSS_FAMILY_ALIASES.map(alias => ({ label: alias.label })),
471
+ ...groups.map(group => ({ label: group.name })),
472
+ ...crossAliases.map(alias => ({ label: alias.label })),
361
473
  { label: "default configured model" },
362
474
  { label: "enter a custom value…" }
363
475
  ];
@@ -390,19 +502,19 @@ class Install {
390
502
  }
391
503
  return text;
392
504
  }
393
- const crossFamily = CLAUDE_CROSS_FAMILY_ALIASES.find(alias => alias.label === top.label);
394
- if (crossFamily) {
395
- return crossFamily.value;
505
+ const crossAlias = crossAliases.find(alias => alias.label === top.label);
506
+ if (crossAlias) {
507
+ return crossAlias.value;
396
508
  }
397
- const family = CLAUDE_MODEL_FAMILIES.find(f => f.family === top.label);
509
+ const group = groups.find(g => g.name === top.label);
398
510
  for (;;) {
399
- const familyOptions = family.entries.map(entry => ({ label: entry.label }));
400
- familyOptions.push({ label: CLAUDE_BACK_LABEL });
511
+ const groupOptions = group.entries.map(entry => ({ label: entry.label }));
512
+ groupOptions.push({ label: MODEL_BACK_LABEL });
401
513
  const choice = await promptChoice(contexts.ask, {
402
514
  header: headerLabel,
403
- question: `Which ${family.family} model should ${roleLabel} use?`,
404
- options: familyOptions,
405
- defaultLabel: family === preselectedFamily ? preselectedEntryLabel : undefined
515
+ question: `Which ${group.name} model should ${roleLabel} use?`,
516
+ options: groupOptions,
517
+ defaultLabel: group === preselectedGroup ? preselectedEntryLabel : undefined
406
518
  });
407
519
  if (!choice) {
408
520
  return null;
@@ -410,14 +522,28 @@ class Install {
410
522
  if (this._disposed) {
411
523
  return null;
412
524
  }
413
- if (choice.label === CLAUDE_BACK_LABEL) {
525
+ if (choice.label === MODEL_BACK_LABEL) {
414
526
  continue topLevel;
415
527
  }
416
- const entry = family.entries.find(e => e.label === choice.label);
528
+ const entry = group.entries.find(e => e.label === choice.label);
417
529
  return entry.value;
418
530
  }
419
531
  }
420
532
  }
533
+ async _resolveFreeTextModel(roleLabel, contexts, preselect) {
534
+ const text = await promptText(contexts.ask, {
535
+ question: `Which model should ${roleLabel} use?`,
536
+ placeholder: "leave empty for the default configured model",
537
+ default: preselect
538
+ });
539
+ if (text === null) {
540
+ return null;
541
+ }
542
+ if (this._disposed) {
543
+ return null;
544
+ }
545
+ return text;
546
+ }
421
547
  async _resolveRoleModel(roleLabel, headerLabel, tool, suppliedModel, contexts, preselect) {
422
548
  if (suppliedModel !== undefined) {
423
549
  return suppliedModel;
@@ -426,7 +552,7 @@ class Install {
426
552
  return null;
427
553
  }
428
554
  if (tool === "claude") {
429
- return await this._resolveClaudeModel(roleLabel, headerLabel, contexts, preselect);
555
+ return await this._resolveGroupedModel(roleLabel, headerLabel, CLAUDE_MODEL_FAMILIES, CLAUDE_CROSS_FAMILY_ALIASES, contexts, preselect);
430
556
  }
431
557
  if (!this._modelProbeCache.has(tool)) {
432
558
  const result = await (0, InstallModelProbe_1.probeModelList)(contexts.script);
@@ -465,18 +591,7 @@ class Install {
465
591
  }
466
592
  return option.label === "default configured model" ? "" : option.label;
467
593
  }
468
- const text = await promptText(contexts.ask, {
469
- question: `Which model should ${roleLabel} use?`,
470
- placeholder: "leave empty for the default configured model",
471
- default: preselect
472
- });
473
- if (text === null) {
474
- return null;
475
- }
476
- if (this._disposed) {
477
- return null;
478
- }
479
- return text;
594
+ return await this._resolveFreeTextModel(roleLabel, contexts, preselect);
480
595
  }
481
596
  async _resolveRoleEffort(roleLabel, headerLabel, tool, suppliedEffort, contexts, preselect) {
482
597
  if (suppliedEffort !== undefined) {
@@ -508,7 +623,36 @@ class Install {
508
623
  }
509
624
  return await this._resolveCuratedChoice(headerLabel, `What effort level should ${roleLabel} use?`, CLAUDE_EFFORT_LEVELS, "default configured effort", "enter a custom value…", "leave empty for the default configured effort", contexts, preselect);
510
625
  }
511
- async _resolveReviewer(idx, supplied, contexts, storedReviewer) {
626
+ async _resolveRoleFast(roleLabel, headerLabel, tool, model, fastFlag, flagName, contexts, storedFast) {
627
+ const eligible = tool === "claude" && (0, fastMode_1.modelSupportsFastMode)(model);
628
+ if (fastFlag) {
629
+ if (!eligible) {
630
+ contexts.output.writeError(fastFlagEligibilityError(tool, model, flagName));
631
+ return null;
632
+ }
633
+ return true;
634
+ }
635
+ if (!eligible) {
636
+ return false;
637
+ }
638
+ const option = await promptChoice(contexts.ask, {
639
+ header: headerLabel,
640
+ question: `Should ${roleLabel} run with Claude Code's fast mode enabled, neighbor?`,
641
+ options: [
642
+ { label: "no", description: "Standard mode — fast mode off" },
643
+ { label: "yes", description: "Fast mode — Claude Code's higher-speed, higher-cost configuration" }
644
+ ],
645
+ defaultLabel: storedFast === true ? "yes" : "no"
646
+ });
647
+ if (!option) {
648
+ return null;
649
+ }
650
+ if (this._disposed) {
651
+ return null;
652
+ }
653
+ return option.label === "yes";
654
+ }
655
+ async _resolveReviewer(idx, supplied, fastFlag, contexts, storedReviewer) {
512
656
  const ordinal = idx === 1 ? "" : ` ${idx}`;
513
657
  const roleLabel = `reviewer${ordinal}`;
514
658
  let tool;
@@ -522,10 +666,7 @@ class Install {
522
666
  const option = await promptChoice(contexts.ask, {
523
667
  header: `Reviewer${ordinal} tool`,
524
668
  question: `Which AI tool should ${roleLabel} use?`,
525
- options: [
526
- { label: "claude", description: "Use Claude Code" },
527
- { label: "codex", description: "Use Codex CLI" }
528
- ],
669
+ options: TOOL_CHOICE_OPTIONS,
529
670
  defaultLabel: storedReviewer === null || storedReviewer === void 0 ? void 0 : storedReviewer.tool
530
671
  });
531
672
  if (!option) {
@@ -536,6 +677,13 @@ class Install {
536
677
  }
537
678
  tool = option.label;
538
679
  }
680
+ if ((supplied === null || supplied === void 0 ? void 0 : supplied.effort) !== undefined) {
681
+ const effortError = validateEffortForTool(supplied.effort, tool, idx === 1 ? "--reviewer-effort" : `--reviewer-${idx}-effort`);
682
+ if (effortError !== null) {
683
+ contexts.output.writeError(effortError);
684
+ return null;
685
+ }
686
+ }
539
687
  const model = await this._resolveRoleModel(roleLabel, `Reviewer${ordinal} model`, tool, supplied === null || supplied === void 0 ? void 0 : supplied.model, contexts, storedReviewer === null || storedReviewer === void 0 ? void 0 : storedReviewer.model);
540
688
  if (model === null) {
541
689
  return null;
@@ -544,10 +692,14 @@ class Install {
544
692
  if (effort === null) {
545
693
  return null;
546
694
  }
547
- return { tool, model, effort };
695
+ const fast = await this._resolveRoleFast(roleLabel, `Reviewer${ordinal} fast`, tool, model, fastFlag, idx === 1 ? "--reviewer-fast" : `--reviewer-${idx}-fast`, contexts, storedReviewer === null || storedReviewer === void 0 ? void 0 : storedReviewer.fast);
696
+ if (fast === null) {
697
+ return null;
698
+ }
699
+ return { tool, model, effort, fast };
548
700
  }
549
701
  async _run(rawArgs, options, contexts) {
550
- var _a;
702
+ var _a, _b;
551
703
  try {
552
704
  const parsed = parseInstallFlags(rawArgs);
553
705
  if (!parsed.ok) {
@@ -555,30 +707,29 @@ class Install {
555
707
  return 1;
556
708
  }
557
709
  const answers = parsed.answers;
558
- let skillsTool;
559
- if (answers.skillsTool !== undefined) {
560
- skillsTool = answers.skillsTool;
710
+ let skillsTools;
711
+ if (answers.skillsTools !== undefined) {
712
+ skillsTools = answers.skillsTools;
561
713
  }
562
714
  else {
563
715
  if (this._disposed) {
564
716
  return 1;
565
717
  }
566
- const option = await promptChoice(contexts.ask, {
718
+ const picked = await promptMultiChoice(contexts.ask, {
567
719
  header: "Skills tool",
568
720
  question: "Which AI tool(s) should the skills be installed for, neighbor?",
569
721
  options: [
570
722
  { label: "claude", description: "Install skills for Claude Code" },
571
- { label: "codex", description: "Install skills for Codex CLI" },
572
- { label: "both", description: "Install skills for both Claude Code and Codex CLI" }
723
+ { label: "codex", description: "Install skills for Codex CLI" }
573
724
  ]
574
725
  });
575
- if (!option) {
726
+ if (!picked) {
576
727
  return 1;
577
728
  }
578
729
  if (this._disposed) {
579
730
  return 1;
580
731
  }
581
- skillsTool = option.label;
732
+ skillsTools = picked.map(o => o.label);
582
733
  }
583
734
  let mode;
584
735
  if (answers.scope) {
@@ -588,20 +739,8 @@ class Install {
588
739
  if (this._disposed) {
589
740
  return 1;
590
741
  }
591
- let projectDescription;
592
- let globalDescription;
593
- if (skillsTool === "claude") {
594
- projectDescription = "Install in .claude/skills/ relative to CWD";
595
- globalDescription = "Install in ~/.claude/skills/";
596
- }
597
- else if (skillsTool === "codex") {
598
- projectDescription = "Install in .codex/prompts/ relative to CWD";
599
- globalDescription = "Install in ~/.codex/prompts/";
600
- }
601
- else {
602
- projectDescription = "Install in .claude/skills/ and .codex/prompts/ relative to CWD";
603
- globalDescription = "Install in ~/.claude/skills/ and ~/.codex/prompts/";
604
- }
742
+ const projectDescription = `Install in ${skillsTools.map(t => SKILLS_TOOL_DESTINATIONS[t].project).join(" and ")} relative to CWD`;
743
+ const globalDescription = `Install in ${skillsTools.map(t => SKILLS_TOOL_DESTINATIONS[t].global).join(" and ")}`;
605
744
  const option = await promptChoice(contexts.ask, {
606
745
  header: "Install destination",
607
746
  question: "Where should Flanders skills be installed?",
@@ -637,10 +776,7 @@ class Install {
637
776
  const option = await promptChoice(contexts.ask, {
638
777
  header: "Worker tool, neighborino",
639
778
  question: "Which AI tool should the worker use?",
640
- options: [
641
- { label: "claude", description: "Use Claude Code" },
642
- { label: "codex", description: "Use Codex CLI" }
643
- ],
779
+ options: TOOL_CHOICE_OPTIONS,
644
780
  defaultLabel: storedConfig === null || storedConfig === void 0 ? void 0 : storedConfig.worker.tool
645
781
  });
646
782
  if (!option) {
@@ -651,6 +787,13 @@ class Install {
651
787
  }
652
788
  workerTool = option.label;
653
789
  }
790
+ if (answers.workerEffort !== undefined) {
791
+ const effortError = validateEffortForTool(answers.workerEffort, workerTool, "--worker-effort");
792
+ if (effortError !== null) {
793
+ contexts.output.writeError(effortError);
794
+ return 1;
795
+ }
796
+ }
654
797
  const workerModel = await this._resolveRoleModel("the worker", "Worker model", workerTool, answers.workerModel, contexts, storedConfig === null || storedConfig === void 0 ? void 0 : storedConfig.worker.model);
655
798
  if (workerModel === null) {
656
799
  return 1;
@@ -659,12 +802,17 @@ class Install {
659
802
  if (workerEffort === null) {
660
803
  return 1;
661
804
  }
805
+ const workerFast = await this._resolveRoleFast("the worker", "Worker fast", workerTool, workerModel, answers.workerFast === true, "--worker-fast", contexts, storedConfig === null || storedConfig === void 0 ? void 0 : storedConfig.worker.fast);
806
+ if (workerFast === null) {
807
+ return 1;
808
+ }
662
809
  const suppliedReviewers = answers.reviewers;
810
+ const fastReviewerSet = new Set((_a = answers.fastReviewerIndices) !== null && _a !== void 0 ? _a : []);
663
811
  const storedReviewers = storedConfig === null || storedConfig === void 0 ? void 0 : storedConfig.reviewers;
664
812
  const reviewers = [];
665
813
  if (suppliedReviewers && suppliedReviewers.length > 0) {
666
814
  for (let i = 0; i < suppliedReviewers.length; i++) {
667
- const reviewer = await this._resolveReviewer(i + 1, suppliedReviewers[i], contexts, storedReviewers === null || storedReviewers === void 0 ? void 0 : storedReviewers[i]);
815
+ const reviewer = await this._resolveReviewer(i + 1, suppliedReviewers[i], fastReviewerSet.has(i + 1), contexts, storedReviewers === null || storedReviewers === void 0 ? void 0 : storedReviewers[i]);
668
816
  if (reviewer === null) {
669
817
  return 1;
670
818
  }
@@ -674,7 +822,7 @@ class Install {
674
822
  else {
675
823
  let idx = 1;
676
824
  for (;;) {
677
- const reviewer = await this._resolveReviewer(idx, undefined, contexts, storedReviewers === null || storedReviewers === void 0 ? void 0 : storedReviewers[idx - 1]);
825
+ const reviewer = await this._resolveReviewer(idx, undefined, fastReviewerSet.has(idx), contexts, storedReviewers === null || storedReviewers === void 0 ? void 0 : storedReviewers[idx - 1]);
678
826
  if (reviewer === null) {
679
827
  return 1;
680
828
  }
@@ -712,6 +860,11 @@ class Install {
712
860
  contexts.output.writeError(diagnostic);
713
861
  return 1;
714
862
  }
863
+ const fastDiagnostic = validateFastFlagsForReviewerCount(answers.fastReviewerIndices, reviewers.length);
864
+ if (fastDiagnostic !== null) {
865
+ contexts.output.writeError(fastDiagnostic);
866
+ return 1;
867
+ }
715
868
  }
716
869
  let minimumReviews;
717
870
  const reviewerConfigs = [];
@@ -770,7 +923,7 @@ class Install {
770
923
  const reviewer = reviewers[i];
771
924
  const modelLabel = reviewer.model === "" ? "default configured model" : reviewer.model;
772
925
  const effortLabel = reviewer.effort === "" ? "default configured effort" : reviewer.effort;
773
- const optionalDefault = ((_a = storedReviewers === null || storedReviewers === void 0 ? void 0 : storedReviewers[i]) === null || _a === void 0 ? void 0 : _a.optional) === true ? "yes" : "no";
926
+ const optionalDefault = ((_b = storedReviewers === null || storedReviewers === void 0 ? void 0 : storedReviewers[i]) === null || _b === void 0 ? void 0 : _b.optional) === true ? "yes" : "no";
774
927
  const optionalOption = await promptChoice(contexts.ask, {
775
928
  header: `Reviewer ${i + 1} optional`,
776
929
  question: `Is reviewer ${i + 1} (${reviewer.tool} · ${modelLabel} · ${effortLabel}) optional?`,
@@ -800,14 +953,7 @@ class Install {
800
953
  ? contexts.platform.homedir()
801
954
  : options.projectRoot;
802
955
  const writtenPaths = [];
803
- const tools = [];
804
- if (skillsTool === "claude" || skillsTool === "both") {
805
- tools.push("claude");
806
- }
807
- if (skillsTool === "codex" || skillsTool === "both") {
808
- tools.push("codex");
809
- }
810
- for (const tool of tools) {
956
+ for (const tool of skillsTools) {
811
957
  const result = await (0, skillArtifacts_1.writeSkillArtifacts)(contexts.fs, scopeRoot, tool, () => this._disposed);
812
958
  if (!result.ok) {
813
959
  if (result.diagnostic !== null) {
@@ -821,7 +967,7 @@ class Install {
821
967
  return 1;
822
968
  }
823
969
  const config = {
824
- worker: { tool: workerTool, model: workerModel, effort: workerEffort },
970
+ worker: { tool: workerTool, model: workerModel, effort: workerEffort, fast: workerFast },
825
971
  reviewers: reviewerConfigs,
826
972
  minimumReviews
827
973
  };