flanders 0.1.0 → 0.3.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 (58) hide show
  1. package/lib/ai/AiRunner.d.ts +24 -0
  2. package/lib/ai/AiRunner.js +100 -0
  3. package/lib/ai/AiSession.d.ts +32 -0
  4. package/lib/ai/AiSession.js +143 -0
  5. package/lib/ai/ClaudeAdapter.d.ts +12 -0
  6. package/lib/ai/ClaudeAdapter.js +345 -0
  7. package/lib/ai/CodexAdapter.d.ts +13 -0
  8. package/lib/ai/CodexAdapter.js +354 -0
  9. package/lib/ai/ToolAdapter.d.ts +52 -0
  10. package/lib/ai/ToolAdapter.js +2 -0
  11. package/lib/cli.js +56 -35
  12. package/lib/commands/Implement.d.ts +22 -8
  13. package/lib/commands/Implement.js +296 -171
  14. package/lib/commands/Install.d.ts +31 -3
  15. package/lib/commands/Install.js +625 -49
  16. package/lib/commands/InstallModelProbe.d.ts +11 -0
  17. package/lib/commands/InstallModelProbe.js +99 -0
  18. package/lib/contexts.d.ts +5 -4
  19. package/lib/index.d.ts +5 -3
  20. package/lib/{PlanFile.d.ts → plan/PlanFile.d.ts} +8 -1
  21. package/lib/{PlanFile.js → plan/PlanFile.js} +44 -5
  22. package/lib/prompts/prompts.d.ts +48 -0
  23. package/lib/prompts/prompts.js +328 -0
  24. package/lib/prompts/skills.d.ts +3 -0
  25. package/lib/prompts/skills.js +464 -0
  26. package/lib/{Git.d.ts → system/Git.d.ts} +4 -2
  27. package/lib/{Git.js → system/Git.js} +63 -3
  28. package/lib/{ScriptRunner.d.ts → system/ScriptRunner.d.ts} +1 -1
  29. package/lib/system/ShellScriptContext.d.ts +36 -0
  30. package/lib/system/ShellScriptContext.js +70 -0
  31. package/lib/{fsUtils.d.ts → system/fsUtils.d.ts} +1 -1
  32. package/lib/{wait.d.ts → system/wait.d.ts} +1 -1
  33. package/lib/ui/BottomBlock.d.ts +9 -1
  34. package/lib/ui/BottomBlock.js +30 -14
  35. package/lib/ui/PromptHelper.d.ts +12 -0
  36. package/lib/ui/PromptHelper.js +32 -0
  37. package/lib/ui/TerminalSizeSource.d.ts +19 -0
  38. package/lib/ui/TerminalSizeSource.js +77 -0
  39. package/lib/ui/formatters.d.ts +14 -0
  40. package/lib/ui/formatters.js +65 -2
  41. package/lib/workspace/FlandersConfig.d.ts +23 -0
  42. package/lib/workspace/FlandersConfig.js +90 -0
  43. package/lib/workspace/SpecDiscovery.d.ts +12 -0
  44. package/lib/workspace/SpecDiscovery.js +40 -0
  45. package/lib/{Workspace.d.ts → workspace/Workspace.d.ts} +10 -2
  46. package/lib/{Workspace.js → workspace/Workspace.js} +52 -6
  47. package/package.json +3 -2
  48. package/lib/Claude.d.ts +0 -129
  49. package/lib/Claude.js +0 -387
  50. package/lib/ClaudeSession.d.ts +0 -34
  51. package/lib/ClaudeSession.js +0 -292
  52. package/lib/prompts.d.ts +0 -18
  53. package/lib/prompts.js +0 -221
  54. package/lib/skills.d.ts +0 -3
  55. package/lib/skills.js +0 -267
  56. /package/lib/{ScriptRunner.js → system/ScriptRunner.js} +0 -0
  57. /package/lib/{fsUtils.js → system/fsUtils.js} +0 -0
  58. /package/lib/{wait.js → system/wait.js} +0 -0
@@ -1,82 +1,673 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Install = void 0;
4
- const fsUtils_1 = require("../fsUtils");
5
- const skills_1 = require("../skills");
4
+ exports.stripYamlFrontmatter = stripYamlFrontmatter;
5
+ exports.parseInstallFlags = parseInstallFlags;
6
+ const FlandersConfig_1 = require("../workspace/FlandersConfig");
7
+ const PromptHelper_1 = require("../ui/PromptHelper");
8
+ const fsUtils_1 = require("../system/fsUtils");
9
+ const skills_1 = require("../prompts/skills");
10
+ const InstallModelProbe_1 = require("./InstallModelProbe");
6
11
  const SKILLS = [
7
- { name: "flanders-contract", folder: "flanders-contract", body: skills_1.contractSkillBody },
8
- { name: "flanders-plan", folder: "flanders-plan", body: skills_1.planSkillBody },
9
- { name: "flanders-rule", folder: "flanders-rule", body: skills_1.ruleSkillBody }
12
+ { name: "flanders-spec", body: skills_1.specSkillBody },
13
+ { name: "flanders-plan", body: skills_1.planSkillBody },
14
+ { name: "flanders-work", body: skills_1.workSkillBody }
10
15
  ];
16
+ function stripYamlFrontmatter(body) {
17
+ if (!body.startsWith("---\n") && !body.startsWith("---\r\n")) {
18
+ return body;
19
+ }
20
+ const newlineAfterOpener = body.indexOf("\n") + 1;
21
+ const closerIndex = body.indexOf("\n---\n", newlineAfterOpener);
22
+ if (closerIndex === -1) {
23
+ const closerCrlf = body.indexOf("\n---\r\n", newlineAfterOpener);
24
+ if (closerCrlf === -1) {
25
+ return body;
26
+ }
27
+ return body.slice(closerCrlf + "\n---\r\n".length);
28
+ }
29
+ return body.slice(closerIndex + "\n---\n".length);
30
+ }
31
+ async function promptChoice(ask, args) {
32
+ try {
33
+ return await (0, PromptHelper_1.askChoice)(ask, args);
34
+ }
35
+ catch (e) {
36
+ if (e instanceof Error && e.name === "AbortError") {
37
+ return null;
38
+ }
39
+ throw e;
40
+ }
41
+ }
42
+ async function promptText(ask, args) {
43
+ try {
44
+ return await (0, PromptHelper_1.askText)(ask, args);
45
+ }
46
+ catch (e) {
47
+ if (e instanceof Error && e.name === "AbortError") {
48
+ return null;
49
+ }
50
+ throw e;
51
+ }
52
+ }
53
+ function extractFlagValue(rawArgs, flag) {
54
+ const prefix = flag + "=";
55
+ for (const arg of rawArgs) {
56
+ if (arg.startsWith(prefix)) {
57
+ return arg.slice(prefix.length);
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ function validateClosedSet(value, allowed, flagName) {
63
+ if (allowed.includes(value)) {
64
+ return null;
65
+ }
66
+ return `Invalid value for ${flagName}: "${value}". Allowed values: ${allowed.join(", ")}.\n`;
67
+ }
68
+ const CODEX_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
69
+ const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
70
+ const CLAUDE_MODEL_FAMILIES = [
71
+ {
72
+ family: "Opus",
73
+ entries: [
74
+ { label: "Latest Opus", value: "opus" },
75
+ { label: "Latest Opus [1m context]", value: "opus[1m]" },
76
+ { label: "Opus 4.8", value: "claude-opus-4-8" },
77
+ { label: "Opus 4.8 [1m context]", value: "claude-opus-4-8[1m]" },
78
+ { label: "Opus 4.7", value: "claude-opus-4-7" },
79
+ { label: "Opus 4.7 [1m context]", value: "claude-opus-4-7[1m]" },
80
+ { label: "Opus 4.6", value: "claude-opus-4-6" },
81
+ { label: "Opus 4.6 [1m context]", value: "claude-opus-4-6[1m]" }
82
+ ]
83
+ },
84
+ {
85
+ family: "Sonnet",
86
+ entries: [
87
+ { label: "Latest Sonnet", value: "sonnet" },
88
+ { label: "Latest Sonnet [1m context]", value: "sonnet[1m]" },
89
+ { label: "Sonnet 4.6", value: "claude-sonnet-4-6" },
90
+ { label: "Sonnet 4.6 [1m context]", value: "claude-sonnet-4-6[1m]" },
91
+ { label: "Sonnet 4.5", value: "claude-sonnet-4-5" },
92
+ { label: "Sonnet 4.5 [1m context]", value: "claude-sonnet-4-5[1m]" }
93
+ ]
94
+ },
95
+ {
96
+ family: "Haiku",
97
+ entries: [
98
+ { label: "Latest Haiku", value: "haiku" },
99
+ { label: "Haiku 4.5", value: "claude-haiku-4-5-20251001" }
100
+ ]
101
+ },
102
+ {
103
+ family: "Fable",
104
+ entries: [
105
+ { label: "Latest Fable", value: "fable" },
106
+ { label: "Fable 5", value: "claude-fable-5" }
107
+ ]
108
+ }
109
+ ];
110
+ const CLAUDE_CROSS_FAMILY_ALIASES = [
111
+ { label: "Best (auto-pick)", value: "best" }
112
+ ];
113
+ const CLAUDE_BACK_LABEL = "← back";
114
+ const REVIEWER_INDEXED_RE = /^--reviewer-(\d+)-(tool|model|effort)=/;
115
+ function parseInstallFlags(rawArgs) {
116
+ var _a;
117
+ const hasGlobal = rawArgs.includes("--global");
118
+ const hasProject = rawArgs.includes("--project");
119
+ if (hasGlobal && hasProject) {
120
+ return { ok: false, diagnostic: "Conflicting flags: --global and --project cannot be used together.\n" };
121
+ }
122
+ const answers = {};
123
+ if (hasProject)
124
+ answers.scope = "project";
125
+ if (hasGlobal)
126
+ answers.scope = "global";
127
+ const skillsTool = extractFlagValue(rawArgs, "--skills-tool");
128
+ if (skillsTool !== undefined) {
129
+ const error = validateClosedSet(skillsTool, ["claude", "codex", "both"], "--skills-tool");
130
+ if (error)
131
+ return { ok: false, diagnostic: error };
132
+ answers.skillsTool = skillsTool;
133
+ }
134
+ const workerTool = extractFlagValue(rawArgs, "--worker-tool");
135
+ if (workerTool !== undefined) {
136
+ const error = validateClosedSet(workerTool, ["claude", "codex"], "--worker-tool");
137
+ if (error)
138
+ return { ok: false, diagnostic: error };
139
+ answers.workerTool = workerTool;
140
+ }
141
+ const workerModel = extractFlagValue(rawArgs, "--worker-model");
142
+ if (workerModel !== undefined) {
143
+ answers.workerModel = workerModel;
144
+ }
145
+ const workerEffort = extractFlagValue(rawArgs, "--worker-effort");
146
+ if (workerEffort !== undefined) {
147
+ if (workerEffort !== "" && answers.workerTool === "codex") {
148
+ const error = validateClosedSet(workerEffort, CODEX_EFFORT_LEVELS, "--worker-effort");
149
+ if (error)
150
+ return { ok: false, diagnostic: error };
151
+ }
152
+ answers.workerEffort = workerEffort;
153
+ }
154
+ const reviewerIndices = new Map();
155
+ const reviewer1Tool = extractFlagValue(rawArgs, "--reviewer-tool");
156
+ const reviewer1Model = extractFlagValue(rawArgs, "--reviewer-model");
157
+ const reviewer1Effort = extractFlagValue(rawArgs, "--reviewer-effort");
158
+ if (reviewer1Tool !== undefined) {
159
+ const error = validateClosedSet(reviewer1Tool, ["claude", "codex"], "--reviewer-tool");
160
+ if (error)
161
+ return { ok: false, diagnostic: error };
162
+ if (!reviewerIndices.has(1))
163
+ reviewerIndices.set(1, {});
164
+ reviewerIndices.get(1).tool = reviewer1Tool;
165
+ }
166
+ if (reviewer1Model !== undefined) {
167
+ if (!reviewerIndices.has(1))
168
+ reviewerIndices.set(1, {});
169
+ reviewerIndices.get(1).model = reviewer1Model;
170
+ }
171
+ if (reviewer1Effort !== undefined) {
172
+ const tool1 = (_a = reviewerIndices.get(1)) === null || _a === void 0 ? void 0 : _a.tool;
173
+ if (reviewer1Effort !== "" && tool1 === "codex") {
174
+ const error = validateClosedSet(reviewer1Effort, CODEX_EFFORT_LEVELS, "--reviewer-effort");
175
+ if (error)
176
+ return { ok: false, diagnostic: error };
177
+ }
178
+ if (!reviewerIndices.has(1))
179
+ reviewerIndices.set(1, {});
180
+ reviewerIndices.get(1).effort = reviewer1Effort;
181
+ }
182
+ for (const arg of rawArgs) {
183
+ const match = arg.match(REVIEWER_INDEXED_RE);
184
+ if (!match)
185
+ continue;
186
+ const idx = Number(match[1]);
187
+ if (idx < 2) {
188
+ return { ok: false, diagnostic: `Invalid reviewer flag: "${arg}". Reviewer 1 uses --reviewer-tool/-model/-effort; --reviewer-N-* requires N >= 2.\n` };
189
+ }
190
+ const field = match[2];
191
+ const value = arg.slice(arg.indexOf("=") + 1);
192
+ if (!reviewerIndices.has(idx))
193
+ reviewerIndices.set(idx, {});
194
+ const entry = reviewerIndices.get(idx);
195
+ if (field === "tool") {
196
+ const error = validateClosedSet(value, ["claude", "codex"], `--reviewer-${idx}-tool`);
197
+ if (error)
198
+ return { ok: false, diagnostic: error };
199
+ entry.tool = value;
200
+ }
201
+ else if (field === "model") {
202
+ entry.model = value;
203
+ }
204
+ else {
205
+ entry.effort = value;
206
+ }
207
+ }
208
+ for (const [idx, entry] of reviewerIndices) {
209
+ if (entry.effort !== undefined && entry.effort !== "" && entry.tool === "codex") {
210
+ const error = validateClosedSet(entry.effort, CODEX_EFFORT_LEVELS, `--reviewer-${idx}-effort`);
211
+ if (error)
212
+ return { ok: false, diagnostic: error };
213
+ }
214
+ }
215
+ if (reviewerIndices.size > 0) {
216
+ const sortedIndices = [...reviewerIndices.keys()].sort((a, b) => a - b);
217
+ const maxIdx = sortedIndices[sortedIndices.length - 1];
218
+ for (let i = 1; i <= maxIdx; i++) {
219
+ if (!reviewerIndices.has(i)) {
220
+ return { ok: false, diagnostic: `Reviewer flag indices are not contiguous: missing reviewer ${i}. Indexed reviewer flags must form a contiguous run starting at reviewer 1.\n` };
221
+ }
222
+ }
223
+ const list = [];
224
+ for (let i = 1; i <= maxIdx; i++) {
225
+ list.push(reviewerIndices.get(i));
226
+ }
227
+ answers.reviewers = list;
228
+ }
229
+ return { ok: true, answers };
230
+ }
11
231
  class Install {
12
232
  constructor(rawArgs, _options, _contexts) {
13
233
  this._disposed = false;
234
+ this._modelProbeCache = new Map();
14
235
  this._runPromise = this._run(rawArgs, _options, _contexts);
15
236
  this._runPromise.catch(() => { });
16
237
  }
17
238
  result() {
18
239
  return this._runPromise;
19
240
  }
241
+ async _resolveCuratedChoice(headerLabel, question, curatedValues, defaultLabel, customLabel, customPlaceholder, contexts) {
242
+ const options = curatedValues.map(v => ({ label: v }));
243
+ options.push({ label: defaultLabel });
244
+ options.push({ label: customLabel });
245
+ const option = await promptChoice(contexts.ask, {
246
+ header: headerLabel,
247
+ question,
248
+ options
249
+ });
250
+ if (!option) {
251
+ return null;
252
+ }
253
+ if (this._disposed) {
254
+ return null;
255
+ }
256
+ if (option.label === defaultLabel) {
257
+ return "";
258
+ }
259
+ if (option.label === customLabel) {
260
+ const text = await promptText(contexts.ask, {
261
+ question,
262
+ placeholder: customPlaceholder
263
+ });
264
+ if (text === null) {
265
+ return null;
266
+ }
267
+ if (this._disposed) {
268
+ return null;
269
+ }
270
+ return text;
271
+ }
272
+ return option.label;
273
+ }
274
+ async _resolveClaudeModel(roleLabel, headerLabel, contexts) {
275
+ const question = `Which model should ${roleLabel} use?`;
276
+ topLevel: for (;;) {
277
+ const topOptions = [
278
+ ...CLAUDE_MODEL_FAMILIES.map(family => ({ label: family.family })),
279
+ ...CLAUDE_CROSS_FAMILY_ALIASES.map(alias => ({ label: alias.label })),
280
+ { label: "default configured model" },
281
+ { label: "enter a custom value…" }
282
+ ];
283
+ const top = await promptChoice(contexts.ask, {
284
+ header: headerLabel,
285
+ question,
286
+ options: topOptions
287
+ });
288
+ if (!top) {
289
+ return null;
290
+ }
291
+ if (this._disposed) {
292
+ return null;
293
+ }
294
+ if (top.label === "default configured model") {
295
+ return "";
296
+ }
297
+ if (top.label === "enter a custom value…") {
298
+ const text = await promptText(contexts.ask, {
299
+ question,
300
+ placeholder: "leave empty for the default configured model"
301
+ });
302
+ if (text === null) {
303
+ return null;
304
+ }
305
+ if (this._disposed) {
306
+ return null;
307
+ }
308
+ return text;
309
+ }
310
+ const crossFamily = CLAUDE_CROSS_FAMILY_ALIASES.find(alias => alias.label === top.label);
311
+ if (crossFamily) {
312
+ return crossFamily.value;
313
+ }
314
+ const family = CLAUDE_MODEL_FAMILIES.find(f => f.family === top.label);
315
+ for (;;) {
316
+ const familyOptions = family.entries.map(entry => ({ label: entry.label }));
317
+ familyOptions.push({ label: CLAUDE_BACK_LABEL });
318
+ const choice = await promptChoice(contexts.ask, {
319
+ header: headerLabel,
320
+ question: `Which ${family.family} model should ${roleLabel} use?`,
321
+ options: familyOptions
322
+ });
323
+ if (!choice) {
324
+ return null;
325
+ }
326
+ if (this._disposed) {
327
+ return null;
328
+ }
329
+ if (choice.label === CLAUDE_BACK_LABEL) {
330
+ continue topLevel;
331
+ }
332
+ const entry = family.entries.find(e => e.label === choice.label);
333
+ return entry.value;
334
+ }
335
+ }
336
+ }
337
+ async _resolveRoleModel(roleLabel, headerLabel, tool, suppliedModel, contexts) {
338
+ if (suppliedModel !== undefined) {
339
+ return suppliedModel;
340
+ }
341
+ if (this._disposed) {
342
+ return null;
343
+ }
344
+ if (tool === "claude") {
345
+ return await this._resolveClaudeModel(roleLabel, headerLabel, contexts);
346
+ }
347
+ if (!this._modelProbeCache.has(tool)) {
348
+ const result = await (0, InstallModelProbe_1.probeModelList)(contexts.script);
349
+ if (this._disposed) {
350
+ return null;
351
+ }
352
+ this._modelProbeCache.set(tool, result);
353
+ if (result.kind === "not-started") {
354
+ contexts.output.writeError(`Could not start codex to list models: ${result.reason}\n`);
355
+ }
356
+ }
357
+ const probeResult = this._modelProbeCache.get(tool);
358
+ if (probeResult.kind === "list") {
359
+ const options = probeResult.models.map(m => ({ label: m }));
360
+ options.push({ label: "default configured model" });
361
+ const option = await promptChoice(contexts.ask, {
362
+ header: headerLabel,
363
+ question: `Which model should ${roleLabel} use?`,
364
+ options
365
+ });
366
+ if (!option) {
367
+ return null;
368
+ }
369
+ if (this._disposed) {
370
+ return null;
371
+ }
372
+ return option.label === "default configured model" ? "" : option.label;
373
+ }
374
+ const text = await promptText(contexts.ask, {
375
+ question: `Which model should ${roleLabel} use?`,
376
+ placeholder: "leave empty for the default configured model"
377
+ });
378
+ if (text === null) {
379
+ return null;
380
+ }
381
+ if (this._disposed) {
382
+ return null;
383
+ }
384
+ return text;
385
+ }
386
+ async _resolveRoleEffort(roleLabel, headerLabel, tool, suppliedEffort, contexts) {
387
+ if (suppliedEffort !== undefined) {
388
+ return suppliedEffort;
389
+ }
390
+ if (this._disposed) {
391
+ return null;
392
+ }
393
+ if (tool === "codex") {
394
+ const options = CODEX_EFFORT_LEVELS.map(e => ({ label: e }));
395
+ options.push({ label: "default configured effort" });
396
+ const option = await promptChoice(contexts.ask, {
397
+ header: headerLabel,
398
+ question: `What effort level should ${roleLabel} use?`,
399
+ options
400
+ });
401
+ if (!option) {
402
+ return null;
403
+ }
404
+ if (this._disposed) {
405
+ return null;
406
+ }
407
+ return option.label === "default configured effort" ? "" : option.label;
408
+ }
409
+ 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);
410
+ }
411
+ async _resolveReviewer(idx, supplied, contexts) {
412
+ const ordinal = idx === 1 ? "" : ` ${idx}`;
413
+ const roleLabel = `reviewer${ordinal}`;
414
+ let tool;
415
+ if ((supplied === null || supplied === void 0 ? void 0 : supplied.tool) !== undefined) {
416
+ tool = supplied.tool;
417
+ }
418
+ else {
419
+ if (this._disposed) {
420
+ return null;
421
+ }
422
+ const option = await promptChoice(contexts.ask, {
423
+ header: `Reviewer${ordinal} tool`,
424
+ question: `Which AI tool should ${roleLabel} use?`,
425
+ options: [
426
+ { label: "claude", description: "Use Claude Code" },
427
+ { label: "codex", description: "Use Codex CLI" }
428
+ ]
429
+ });
430
+ if (!option) {
431
+ return null;
432
+ }
433
+ if (this._disposed) {
434
+ return null;
435
+ }
436
+ tool = option.label;
437
+ }
438
+ const model = await this._resolveRoleModel(roleLabel, `Reviewer${ordinal} model`, tool, supplied === null || supplied === void 0 ? void 0 : supplied.model, contexts);
439
+ if (model === null) {
440
+ return null;
441
+ }
442
+ const effort = await this._resolveRoleEffort(roleLabel, `Reviewer${ordinal} effort`, tool, supplied === null || supplied === void 0 ? void 0 : supplied.effort, contexts);
443
+ if (effort === null) {
444
+ return null;
445
+ }
446
+ return { tool, model, effort };
447
+ }
20
448
  async _run(rawArgs, options, contexts) {
21
449
  try {
22
- const hasGlobal = rawArgs.includes("--global");
23
- const hasProject = rawArgs.includes("--project");
24
- if (hasGlobal && hasProject) {
25
- contexts.output.writeError("Conflicting flags: --global and --project cannot be used together.\n");
450
+ const parsed = parseInstallFlags(rawArgs);
451
+ if (!parsed.ok) {
452
+ contexts.output.writeError(parsed.diagnostic);
26
453
  return 1;
27
454
  }
28
- let mode;
29
- if (hasGlobal) {
30
- mode = "global";
455
+ const answers = parsed.answers;
456
+ let skillsTool;
457
+ if (answers.skillsTool !== undefined) {
458
+ skillsTool = answers.skillsTool;
31
459
  }
32
- else if (hasProject) {
33
- mode = "project";
460
+ else {
461
+ if (this._disposed) {
462
+ return 1;
463
+ }
464
+ const option = await promptChoice(contexts.ask, {
465
+ header: "Skills tool",
466
+ question: "Which AI tool(s) should the skills be installed for?",
467
+ options: [
468
+ { label: "claude", description: "Install skills for Claude Code" },
469
+ { label: "codex", description: "Install skills for Codex CLI" },
470
+ { label: "both", description: "Install skills for both Claude Code and Codex CLI" }
471
+ ]
472
+ });
473
+ if (!option) {
474
+ return 1;
475
+ }
476
+ if (this._disposed) {
477
+ return 1;
478
+ }
479
+ skillsTool = option.label;
480
+ }
481
+ let mode;
482
+ if (answers.scope) {
483
+ mode = answers.scope;
34
484
  }
35
485
  else {
36
486
  if (this._disposed) {
37
487
  return 1;
38
488
  }
39
- const picked = await this._promptDestination(contexts);
489
+ let projectDescription;
490
+ let globalDescription;
491
+ if (skillsTool === "claude") {
492
+ projectDescription = "Install in .claude/skills/ relative to CWD";
493
+ globalDescription = "Install in ~/.claude/skills/";
494
+ }
495
+ else if (skillsTool === "codex") {
496
+ projectDescription = "Install in .codex/prompts/ relative to CWD";
497
+ globalDescription = "Install in ~/.codex/prompts/";
498
+ }
499
+ else {
500
+ projectDescription = "Install in .claude/skills/ and .codex/prompts/ relative to CWD";
501
+ globalDescription = "Install in ~/.claude/skills/ and ~/.codex/prompts/";
502
+ }
503
+ const option = await promptChoice(contexts.ask, {
504
+ header: "Install destination",
505
+ question: "Where should Flanders skills be installed?",
506
+ options: [
507
+ { label: "project", description: projectDescription },
508
+ { label: "global", description: globalDescription }
509
+ ]
510
+ });
511
+ if (!option) {
512
+ return 1;
513
+ }
514
+ if (this._disposed) {
515
+ return 1;
516
+ }
517
+ mode = option.label;
518
+ }
519
+ let workerTool;
520
+ if (answers.workerTool !== undefined) {
521
+ workerTool = answers.workerTool;
522
+ }
523
+ else {
40
524
  if (this._disposed) {
41
525
  return 1;
42
526
  }
43
- if (!picked) {
527
+ const option = await promptChoice(contexts.ask, {
528
+ header: "Worker tool",
529
+ question: "Which AI tool should the worker use?",
530
+ options: [
531
+ { label: "claude", description: "Use Claude Code" },
532
+ { label: "codex", description: "Use Codex CLI" }
533
+ ]
534
+ });
535
+ if (!option) {
44
536
  return 1;
45
537
  }
46
- mode = picked;
538
+ if (this._disposed) {
539
+ return 1;
540
+ }
541
+ workerTool = option.label;
542
+ }
543
+ const workerModel = await this._resolveRoleModel("the worker", "Worker model", workerTool, answers.workerModel, contexts);
544
+ if (workerModel === null) {
545
+ return 1;
546
+ }
547
+ const workerEffort = await this._resolveRoleEffort("the worker", "Worker effort", workerTool, answers.workerEffort, contexts);
548
+ if (workerEffort === null) {
549
+ return 1;
550
+ }
551
+ const suppliedReviewers = answers.reviewers;
552
+ const reviewers = [];
553
+ if (suppliedReviewers && suppliedReviewers.length > 0) {
554
+ for (let i = 0; i < suppliedReviewers.length; i++) {
555
+ const reviewer = await this._resolveReviewer(i + 1, suppliedReviewers[i], contexts);
556
+ if (reviewer === null) {
557
+ return 1;
558
+ }
559
+ reviewers.push(reviewer);
560
+ }
47
561
  }
48
- const skillsRoot = mode === "global"
49
- ? (0, fsUtils_1.joinPath)(contexts.platform.homedir(), ".claude/skills")
50
- : (0, fsUtils_1.joinPath)(options.projectRoot, ".claude/skills");
562
+ else {
563
+ let idx = 1;
564
+ for (;;) {
565
+ const reviewer = await this._resolveReviewer(idx, undefined, contexts);
566
+ if (reviewer === null) {
567
+ return 1;
568
+ }
569
+ reviewers.push(reviewer);
570
+ if (this._disposed) {
571
+ return 1;
572
+ }
573
+ const more = await promptChoice(contexts.ask, {
574
+ header: "Configure another reviewer?",
575
+ question: "Configure another reviewer?",
576
+ options: [
577
+ { label: "no", description: "Stop adding reviewers" },
578
+ { label: "yes", description: "Configure another reviewer in the ordered list" }
579
+ ]
580
+ });
581
+ if (!more) {
582
+ return 1;
583
+ }
584
+ if (this._disposed) {
585
+ return 1;
586
+ }
587
+ if (more.label === "no") {
588
+ break;
589
+ }
590
+ idx++;
591
+ }
592
+ }
593
+ const scopeRoot = mode === "global"
594
+ ? contexts.platform.homedir()
595
+ : options.projectRoot;
51
596
  for (const skill of SKILLS) {
52
597
  if (!skill.body) {
53
598
  contexts.output.writeError(`Skill "${skill.name}" has no content.\n`);
54
599
  return 1;
55
600
  }
56
601
  }
602
+ const writeClaude = skillsTool === "claude" || skillsTool === "both";
603
+ const writeCodex = skillsTool === "codex" || skillsTool === "both";
57
604
  const writtenPaths = [];
58
- for (const skill of SKILLS) {
59
- if (this._disposed) {
60
- return 1;
605
+ if (writeClaude) {
606
+ const claudeSkillsRoot = (0, fsUtils_1.joinPath)(scopeRoot, ".claude/skills");
607
+ for (const skill of SKILLS) {
608
+ if (this._disposed) {
609
+ return 1;
610
+ }
611
+ const skillFolder = (0, fsUtils_1.joinPath)(claudeSkillsRoot, skill.name);
612
+ try {
613
+ await contexts.fs.mkdir(skillFolder, { recursive: true });
614
+ }
615
+ catch {
616
+ contexts.output.writeError(`Cannot create destination: ${skillFolder}\n`);
617
+ return 1;
618
+ }
619
+ const filePath = (0, fsUtils_1.joinPath)(skillFolder, "SKILL.md");
620
+ try {
621
+ await contexts.fs.writeFile(filePath, skill.body);
622
+ writtenPaths.push(filePath);
623
+ }
624
+ catch {
625
+ contexts.output.writeError(`Cannot write file: ${filePath}\n`);
626
+ return 1;
627
+ }
61
628
  }
62
- const skillFolder = (0, fsUtils_1.joinPath)(skillsRoot, skill.folder);
629
+ }
630
+ if (writeCodex) {
631
+ const codexPromptsRoot = (0, fsUtils_1.joinPath)(scopeRoot, ".codex/prompts");
63
632
  try {
64
- await contexts.fs.mkdir(skillFolder, { recursive: true });
633
+ await contexts.fs.mkdir(codexPromptsRoot, { recursive: true });
65
634
  }
66
635
  catch {
67
- contexts.output.writeError(`Cannot create destination: ${skillFolder}\n`);
636
+ contexts.output.writeError(`Cannot create destination: ${codexPromptsRoot}\n`);
68
637
  return 1;
69
638
  }
70
- const filePath = (0, fsUtils_1.joinPath)(skillFolder, "SKILL.md");
71
- try {
72
- await contexts.fs.writeFile(filePath, skill.body);
73
- writtenPaths.push(filePath);
74
- }
75
- catch {
76
- contexts.output.writeError(`Cannot write file: ${filePath}\n`);
77
- return 1;
639
+ for (const skill of SKILLS) {
640
+ if (this._disposed) {
641
+ return 1;
642
+ }
643
+ const filePath = (0, fsUtils_1.joinPath)(codexPromptsRoot, `${skill.name}.md`);
644
+ try {
645
+ await contexts.fs.writeFile(filePath, stripYamlFrontmatter(skill.body));
646
+ writtenPaths.push(filePath);
647
+ }
648
+ catch {
649
+ contexts.output.writeError(`Cannot write file: ${filePath}\n`);
650
+ return 1;
651
+ }
78
652
  }
79
653
  }
654
+ if (this._disposed) {
655
+ return 1;
656
+ }
657
+ const config = {
658
+ worker: { tool: workerTool, model: workerModel, effort: workerEffort },
659
+ reviewers
660
+ };
661
+ const configWrittenPath = await (0, FlandersConfig_1.write)(contexts.fs, {
662
+ scope: mode,
663
+ projectRoot: options.projectRoot,
664
+ homeDir: contexts.platform.homedir(),
665
+ config
666
+ });
667
+ if (this._disposed) {
668
+ return 1;
669
+ }
670
+ writtenPaths.push(configWrittenPath);
80
671
  for (const p of writtenPaths) {
81
672
  contexts.output.write(`${p}\n`);
82
673
  }
@@ -89,21 +680,6 @@ class Install {
89
680
  return 1;
90
681
  }
91
682
  }
92
- async _promptDestination(contexts) {
93
- const [answer] = await contexts.ask.askChoices([{
94
- header: "Install destination",
95
- question: "Where should Flanders skills be installed?",
96
- options: [
97
- { label: "project", description: "Install in .claude/skills/ relative to CWD" },
98
- { label: "global", description: "Install in ~/.claude/skills/" }
99
- ],
100
- multiSelect: false
101
- }]);
102
- if (!answer || answer.picked.length === 0) {
103
- return null;
104
- }
105
- return answer.picked[0].label;
106
- }
107
683
  async dispose() {
108
684
  if (this._disposed) {
109
685
  try {