codedeck 0.2.0 → 0.4.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 (125) hide show
  1. package/README.md +45 -16
  2. package/dist/cli/charts/bar-chart.js +104 -0
  3. package/dist/cli/charts/bar-chart.js.map +1 -0
  4. package/dist/cli/charts/sparkline.js +18 -0
  5. package/dist/cli/charts/sparkline.js.map +1 -0
  6. package/dist/cli/charts/stacked-bar.js +59 -0
  7. package/dist/cli/charts/stacked-bar.js.map +1 -0
  8. package/dist/cli/cli-name.js +20 -0
  9. package/dist/cli/cli-name.js.map +1 -0
  10. package/dist/cli/commands/claims.js +170 -0
  11. package/dist/cli/commands/claims.js.map +1 -0
  12. package/dist/cli/commands/doctor.js +22 -1
  13. package/dist/cli/commands/doctor.js.map +1 -1
  14. package/dist/cli/commands/models.js +5 -5
  15. package/dist/cli/commands/models.js.map +1 -1
  16. package/dist/cli/commands/open.js +309 -315
  17. package/dist/cli/commands/open.js.map +1 -1
  18. package/dist/cli/commands/ps.js +226 -23
  19. package/dist/cli/commands/ps.js.map +1 -1
  20. package/dist/cli/commands/rename.js +29 -0
  21. package/dist/cli/commands/rename.js.map +1 -0
  22. package/dist/cli/commands/run.js +50 -18
  23. package/dist/cli/commands/run.js.map +1 -1
  24. package/dist/cli/commands/setup.js +989 -72
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/usage.js +192 -0
  27. package/dist/cli/commands/usage.js.map +1 -0
  28. package/dist/cli/commands/wait.js +3 -2
  29. package/dist/cli/commands/wait.js.map +1 -1
  30. package/dist/cli/commands/web.js +224 -0
  31. package/dist/cli/commands/web.js.map +1 -0
  32. package/dist/cli/index.js +36 -21
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/picker-state.js +75 -8
  35. package/dist/cli/picker-state.js.map +1 -1
  36. package/dist/cli/picker.js +13 -3
  37. package/dist/cli/picker.js.map +1 -1
  38. package/dist/cli/ui.js +54 -4
  39. package/dist/cli/ui.js.map +1 -1
  40. package/dist/cli/usage/dashboard.js +330 -0
  41. package/dist/cli/usage/dashboard.js.map +1 -0
  42. package/dist/cli/usage/snapshot.js +112 -0
  43. package/dist/cli/usage/snapshot.js.map +1 -0
  44. package/dist/config/config.js +326 -5
  45. package/dist/config/config.js.map +1 -1
  46. package/dist/config/orchestrator-mode.js +72 -0
  47. package/dist/config/orchestrator-mode.js.map +1 -0
  48. package/dist/config/paths.js +28 -9
  49. package/dist/config/paths.js.map +1 -1
  50. package/dist/core/autocompact.js +77 -0
  51. package/dist/core/autocompact.js.map +1 -0
  52. package/dist/core/driver.js.map +1 -1
  53. package/dist/core/models.js +166 -3
  54. package/dist/core/models.js.map +1 -1
  55. package/dist/core/pricing.js +83 -0
  56. package/dist/core/pricing.js.map +1 -0
  57. package/dist/core/roles.js +14 -3
  58. package/dist/core/roles.js.map +1 -1
  59. package/dist/core/run-usage.js +46 -0
  60. package/dist/core/run-usage.js.map +1 -0
  61. package/dist/core/session.js +18 -0
  62. package/dist/core/session.js.map +1 -1
  63. package/dist/daemon/daemon.js +286 -8
  64. package/dist/daemon/daemon.js.map +1 -1
  65. package/dist/daemon/ipc.js +10 -3
  66. package/dist/daemon/ipc.js.map +1 -1
  67. package/dist/drivers/antigravity/driver.js +158 -0
  68. package/dist/drivers/antigravity/driver.js.map +1 -0
  69. package/dist/drivers/antigravity/parser.js +168 -0
  70. package/dist/drivers/antigravity/parser.js.map +1 -0
  71. package/dist/drivers/claude/driver.js +13 -3
  72. package/dist/drivers/claude/driver.js.map +1 -1
  73. package/dist/drivers/opencode/driver.js +1 -114
  74. package/dist/drivers/opencode/driver.js.map +1 -1
  75. package/dist/drivers/opencode/parser.js +86 -0
  76. package/dist/drivers/opencode/parser.js.map +1 -0
  77. package/dist/drivers/registry.js +2 -0
  78. package/dist/drivers/registry.js.map +1 -1
  79. package/dist/drivers/session-driver.js +6 -4
  80. package/dist/drivers/session-driver.js.map +1 -1
  81. package/dist/drivers/session-runtime.js +1 -0
  82. package/dist/drivers/session-runtime.js.map +1 -1
  83. package/dist/git/worktree.js +1 -1
  84. package/dist/git/worktree.js.map +1 -1
  85. package/dist/open/contract.js +116 -0
  86. package/dist/open/contract.js.map +1 -0
  87. package/dist/open/injection.js +60 -0
  88. package/dist/open/injection.js.map +1 -0
  89. package/dist/open/launchers/claude.js +215 -0
  90. package/dist/open/launchers/claude.js.map +1 -0
  91. package/dist/open/launchers/opencode.js +213 -0
  92. package/dist/open/launchers/opencode.js.map +1 -0
  93. package/dist/open/orchestrator-prose.js +12 -0
  94. package/dist/open/orchestrator-prose.js.map +1 -0
  95. package/dist/open/pty.js +281 -0
  96. package/dist/open/pty.js.map +1 -0
  97. package/dist/open/runtime.js +632 -0
  98. package/dist/open/runtime.js.map +1 -0
  99. package/dist/plugin/agents/auditor.md +7 -1
  100. package/dist/plugin/agents/general.md +12 -1
  101. package/dist/plugin/agents/orchestrator-edit.md +67 -0
  102. package/dist/plugin/agents/orchestrator-read.md +67 -0
  103. package/dist/plugin/agents/orchestrator.md +74 -17
  104. package/dist/plugin/agents/reviewer.md +3 -0
  105. package/dist/plugin/commands/autonomous.md +58 -0
  106. package/dist/plugin/hooks/hooks.json +24 -0
  107. package/dist/plugin/hooks/session-id.sh +23 -0
  108. package/dist/plugin/hooks/session-name-lib.mjs +40 -0
  109. package/dist/plugin/hooks/session-name-worker.mjs +124 -0
  110. package/dist/plugin/hooks/session-name.mjs +66 -0
  111. package/dist/plugin/hooks/session-name.sh +7 -0
  112. package/dist/plugin/pty-shim.mjs +114 -0
  113. package/dist/plugin/statusline.sh +214 -22
  114. package/dist/plugin/themes/codedeck-rage.json +74 -0
  115. package/dist/plugin/themes/codedeck-ultra.json +52 -44
  116. package/dist/store/claims.js +152 -0
  117. package/dist/store/claims.js.map +1 -0
  118. package/dist/store/database.js +50 -15
  119. package/dist/store/database.js.map +1 -1
  120. package/dist/store/sessions.js +234 -3
  121. package/dist/store/sessions.js.map +1 -1
  122. package/dist/web/canvas-page.js +921 -0
  123. package/dist/web/canvas-page.js.map +1 -0
  124. package/package.json +3 -2
  125. package/dist/plugin/settings.json +0 -7
@@ -1,8 +1,270 @@
1
- import { getCachedOrDiscoverModels, } from "../../core/models.js";
1
+ import { AUTOCOMPACT_DEFAULT_CAP, AUTOCOMPACT_DEFAULT_PERCENT, } from "../../core/autocompact.js";
2
+ import { getBatchModels, getCachedOrDiscoverModels, } from "../../core/models.js";
3
+ import { isAgentId } from "../../core/session.js";
4
+ import { itemKey } from "../picker-state.js";
2
5
  import { runScreens } from "../picker.js";
3
6
  import { colors, readDimensions } from "../ui.js";
7
+ import { getCliName } from "../cli-name.js";
4
8
  import { getRegistry } from "../../drivers/registry.js";
5
- import { loadConfig, saveConfig, } from "../../config/config.js";
9
+ import { ROLES } from "../../core/roles.js";
10
+ import { getPaths } from "../../config/paths.js";
11
+ import { BALANCED_PRESET, DISPATCHER_PRESET, EXPLORER_PRESET, ORCHESTRATOR_PRESETS, createSetupConfigStore, DEFAULT_CONFIG, isOrchestratorMode, loadConfig, orchestratorModeLabel, resolveOrchestratorMode, saveConfig, serializeConfig, } from "../../config/config.js";
12
+ const ORCHESTRATOR_SCREEN_ROLE = "orchestrator-mode";
13
+ const ORCHESTRATOR_PICKER_GROUP = "orchestrator";
14
+ const PARALLELISM_NONE = "none";
15
+ const SANDBOX_SCREEN_ROLE = "sandbox";
16
+ const SANDBOX_PICKER_GROUP = "sandbox";
17
+ const SANDBOX_OFF = "workspace-write";
18
+ const SANDBOX_ON = "danger-full-access";
19
+ const AUTOCOMPACT_SCREEN_ROLE = "autocompact";
20
+ const AUTOCOMPACT_PICKER_GROUP = "autocompact";
21
+ const AUTOCOMPACT_OFF = "off";
22
+ const AUTOCOMPACT_ON = "on";
23
+ const AUTOCOMPACT_DESCRIPTION = `Native autocompact applies to Claude and OpenCode sessions. Claude's default window is min(${AUTOCOMPACT_DEFAULT_CAP / 1000}k tokens, ${AUTOCOMPACT_DEFAULT_PERCENT * 100}% of the context window); OpenCode uses its internal context trigger.`;
24
+ const ORCHESTRATOR_PARALLELISM_NOTE_LINES = [
25
+ "parallelism is advisory. Version 1 puts the requested cap in the",
26
+ "orchestrator prompt for review verification and does not enforce the",
27
+ "cap in code.",
28
+ ];
29
+ export const ORCHESTRATOR_PARALLELISM_NOTE = ORCHESTRATOR_PARALLELISM_NOTE_LINES.join(" ");
30
+ const ORCHESTRATOR_PARAMETERS = [
31
+ "investigate",
32
+ "selfWork",
33
+ "tools",
34
+ "parallelism",
35
+ ];
36
+ function copyOrchestratorMode(mode) {
37
+ return {
38
+ investigate: mode.investigate,
39
+ selfWork: mode.selfWork,
40
+ tools: mode.tools,
41
+ ...(mode.parallelism === undefined ? {} : { parallelism: mode.parallelism }),
42
+ };
43
+ }
44
+ function selectionId(selection) {
45
+ return typeof selection === "string" ? selection : selection.id;
46
+ }
47
+ /**
48
+ * Turns a preset or a complete custom answer into the config block. The
49
+ * returned object contains parameters only, so labels and preset names cannot
50
+ * leak into the saved JSON.
51
+ */
52
+ export function orchestratorConfigFromSelection(selection, parameters) {
53
+ const id = selectionId(selection);
54
+ if (Object.hasOwn(ORCHESTRATOR_PRESETS, id)) {
55
+ return copyOrchestratorMode(ORCHESTRATOR_PRESETS[id]);
56
+ }
57
+ if (id !== "custom" || parameters === undefined)
58
+ return undefined;
59
+ const mode = {
60
+ investigate: parameters.investigate,
61
+ selfWork: parameters.selfWork,
62
+ tools: parameters.tools,
63
+ ...(parameters.parallelism === undefined ? {} : { parallelism: parameters.parallelism }),
64
+ };
65
+ return isOrchestratorMode(mode) ? mode : undefined;
66
+ }
67
+ export function orchestratorDisplayLabel(config) {
68
+ return orchestratorModeLabel(resolveOrchestratorMode(config));
69
+ }
70
+ function modeValue(mode, parameter) {
71
+ if (parameter === "parallelism")
72
+ return mode.parallelism === undefined ? PARALLELISM_NONE : String(mode.parallelism);
73
+ return mode[parameter];
74
+ }
75
+ function parameterItems(parameter, mode) {
76
+ const choices = parameter === "investigate"
77
+ ? ["none", "read", "free"]
78
+ : parameter === "selfWork"
79
+ ? ["none", "trivial", "small"]
80
+ : parameter === "tools"
81
+ ? ["dispatch", "read", "edit"]
82
+ : [PARALLELISM_NONE, "1", "2", "3", "4", "5", "6", "8", "12"];
83
+ const current = modeValue(mode, parameter);
84
+ const ordered = [current, ...choices.filter((choice) => choice !== current)];
85
+ return ordered.map((choice) => ({
86
+ id: choice,
87
+ label: parameter === "parallelism" && choice === PARALLELISM_NONE ? "sem limite" : choice,
88
+ group: ORCHESTRATOR_PICKER_GROUP,
89
+ harness: ORCHESTRATOR_PICKER_GROUP,
90
+ ...(choice === current ? { note: "atual" } : {}),
91
+ }));
92
+ }
93
+ export function buildOrchestratorScreen(config = {}, index = 0, total = 1) {
94
+ const mode = resolveOrchestratorMode(config);
95
+ const presets = [
96
+ ["dispatcher", DISPATCHER_PRESET],
97
+ ["balanced", BALANCED_PRESET],
98
+ ["explorer", EXPLORER_PRESET],
99
+ ];
100
+ const presetItems = presets.map(([name, preset]) => ({
101
+ id: name,
102
+ label: name,
103
+ group: ORCHESTRATOR_PICKER_GROUP,
104
+ harness: ORCHESTRATOR_PICKER_GROUP,
105
+ note: `${preset.investigate} / ${preset.selfWork} / ${preset.tools}`,
106
+ }));
107
+ return {
108
+ role: ORCHESTRATOR_SCREEN_ROLE,
109
+ title: `orchestrator (${orchestratorModeLabel(mode)})`,
110
+ counter: `agente ${index + 1} de ${total}`,
111
+ description: ORCHESTRATOR_PARALLELISM_NOTE_LINES,
112
+ items: [
113
+ ...presetItems,
114
+ {
115
+ id: "custom",
116
+ label: "custom",
117
+ group: ORCHESTRATOR_PICKER_GROUP,
118
+ harness: ORCHESTRATOR_PICKER_GROUP,
119
+ note: "ajustar os quatro parametros",
120
+ },
121
+ ],
122
+ pinned: false,
123
+ known: new Set([
124
+ ...presetItems.map((item) => itemKey(item.harness, item.id)),
125
+ itemKey(ORCHESTRATOR_PICKER_GROUP, "custom"),
126
+ ]),
127
+ harnesses: new Set([ORCHESTRATOR_PICKER_GROUP]),
128
+ next: (result) => result.kind === "picked" && result.id === "custom"
129
+ ? [buildOrchestratorParameterScreen(ORCHESTRATOR_PARAMETERS[0], mode, 0)]
130
+ : [],
131
+ };
132
+ }
133
+ export function buildSandboxScreen(config = {}, index = 0, total = 1) {
134
+ const selected = config.defaultSandbox === SANDBOX_ON ? SANDBOX_ON : SANDBOX_OFF;
135
+ const values = selected === SANDBOX_ON ? [SANDBOX_ON, SANDBOX_OFF] : [SANDBOX_OFF, SANDBOX_ON];
136
+ const items = values.map((value) => ({
137
+ id: value,
138
+ label: value === SANDBOX_ON ? "ON" : "OFF",
139
+ group: SANDBOX_PICKER_GROUP,
140
+ harness: SANDBOX_PICKER_GROUP,
141
+ ...(value === selected ? { note: "atual" } : {}),
142
+ }));
143
+ return {
144
+ role: SANDBOX_SCREEN_ROLE,
145
+ title: "Danger full access",
146
+ counter: `configuração ${index + 1} de ${total}`,
147
+ description: ["Danger full access applies to new Codex sessions."],
148
+ items,
149
+ pinned: true,
150
+ known: new Set(items.map((item) => itemKey(item.harness, item.id))),
151
+ harnesses: new Set([SANDBOX_PICKER_GROUP]),
152
+ next: () => [],
153
+ };
154
+ }
155
+ export function buildAutocompactScreen(config = {}, index = 0, total = 1) {
156
+ // A hand-tuned block without `enabled` still resolves as active in the
157
+ // Claude driver (only `enabled: false` disables), so show it as ON rather
158
+ // than let an accepted OFF silently switch off a working setup.
159
+ const selected = config.autocompact === undefined || config.autocompact.enabled === false
160
+ ? AUTOCOMPACT_OFF
161
+ : AUTOCOMPACT_ON;
162
+ const values = selected === AUTOCOMPACT_ON
163
+ ? [AUTOCOMPACT_ON, AUTOCOMPACT_OFF]
164
+ : [AUTOCOMPACT_OFF, AUTOCOMPACT_ON];
165
+ const items = values.map((value) => ({
166
+ id: value,
167
+ label: value.toUpperCase(),
168
+ group: AUTOCOMPACT_PICKER_GROUP,
169
+ harness: AUTOCOMPACT_PICKER_GROUP,
170
+ ...(value === selected ? { note: "atual" } : {}),
171
+ }));
172
+ return {
173
+ role: AUTOCOMPACT_SCREEN_ROLE,
174
+ title: "Claude native autocompact",
175
+ counter: `configuração ${index + 1} de ${total}`,
176
+ description: [AUTOCOMPACT_DESCRIPTION],
177
+ items,
178
+ pinned: true,
179
+ known: new Set(items.map((item) => itemKey(item.harness, item.id))),
180
+ harnesses: new Set([AUTOCOMPACT_PICKER_GROUP]),
181
+ next: () => [],
182
+ };
183
+ }
184
+ function modeAfterParameter(mode, result) {
185
+ const parameters = collectOrchestratorParameters([result], mode);
186
+ return orchestratorConfigFromSelection("custom", parameters) ?? mode;
187
+ }
188
+ function buildOrchestratorParameterScreen(parameter, mode, index, error) {
189
+ const items = parameterItems(parameter, mode);
190
+ const description = parameter === "parallelism"
191
+ ? [...ORCHESTRATOR_PARALLELISM_NOTE_LINES, "Type orchestrator:N for another positive cap."]
192
+ : ORCHESTRATOR_PARALLELISM_NOTE_LINES;
193
+ return {
194
+ role: `${ORCHESTRATOR_SCREEN_ROLE}.${parameter}`,
195
+ title: `orchestrator (${orchestratorModeLabel(mode)}) · ${parameter}`,
196
+ counter: `parametro ${index + 1} de ${ORCHESTRATOR_PARAMETERS.length}`,
197
+ ...(error === undefined ? {} : { error }),
198
+ description,
199
+ items,
200
+ pinned: false,
201
+ known: new Set(items.map((item) => itemKey(item.harness, item.id))),
202
+ harnesses: new Set([ORCHESTRATOR_PICKER_GROUP]),
203
+ next: (result) => {
204
+ if (parameter === "parallelism" &&
205
+ result.kind === "picked" &&
206
+ result.id !== PARALLELISM_NONE &&
207
+ numericParallelism(result.id) === undefined) {
208
+ return [
209
+ buildOrchestratorParameterScreen(parameter, mode, index, "parallelism must be a positive finite number"),
210
+ ];
211
+ }
212
+ if (index + 1 >= ORCHESTRATOR_PARAMETERS.length)
213
+ return [];
214
+ const nextIndex = index + 1;
215
+ return [
216
+ buildOrchestratorParameterScreen(ORCHESTRATOR_PARAMETERS[nextIndex], modeAfterParameter(mode, result), nextIndex),
217
+ ];
218
+ },
219
+ };
220
+ }
221
+ function numericParallelism(value) {
222
+ const parsed = Number(value);
223
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
224
+ }
225
+ /** Keeps skipped parameter screens at the mode shown when customization began. */
226
+ export function collectOrchestratorParameters(results, existing) {
227
+ const parameters = {
228
+ investigate: existing.investigate,
229
+ selfWork: existing.selfWork,
230
+ tools: existing.tools,
231
+ ...(existing.parallelism === undefined ? {} : { parallelism: existing.parallelism }),
232
+ };
233
+ for (const result of results) {
234
+ if (result.kind !== "picked")
235
+ continue;
236
+ const parameter = result.role.startsWith(`${ORCHESTRATOR_SCREEN_ROLE}.`)
237
+ ? result.role.slice(`${ORCHESTRATOR_SCREEN_ROLE}.`.length)
238
+ : "";
239
+ if (parameter === "investigate" && ["none", "read", "free"].includes(result.id)) {
240
+ parameters.investigate = result.id;
241
+ }
242
+ else if (parameter === "selfWork" && ["none", "trivial", "small"].includes(result.id)) {
243
+ parameters.selfWork = result.id;
244
+ }
245
+ else if (parameter === "tools" && ["dispatch", "read", "edit"].includes(result.id)) {
246
+ parameters.tools = result.id;
247
+ }
248
+ else if (parameter === "parallelism") {
249
+ if (result.id === PARALLELISM_NONE)
250
+ delete parameters.parallelism;
251
+ else {
252
+ const parsed = numericParallelism(result.id);
253
+ if (parsed !== undefined)
254
+ parameters.parallelism = parsed;
255
+ }
256
+ }
257
+ }
258
+ return parameters;
259
+ }
260
+ export function collectOrchestratorSelection(selection, parameters, existing = {}) {
261
+ if (selection?.kind !== "picked")
262
+ return undefined;
263
+ if (selection.id !== "custom")
264
+ return orchestratorConfigFromSelection(selection);
265
+ const current = resolveOrchestratorMode(existing);
266
+ return orchestratorConfigFromSelection("custom", collectOrchestratorParameters(parameters, current));
267
+ }
6
268
  /**
7
269
  * Both streams have to be a terminal, not just stdout. A piped stdin leaves
8
270
  * `question` waiting on input that can never arrive, and `tail -f /dev/null |
@@ -18,67 +280,122 @@ export function isInteractiveTerminal() {
18
280
  export function needsModelSetup(config, isTTY = isInteractiveTerminal()) {
19
281
  if (!isTTY)
20
282
  return false;
21
- return config == null || config.models == null;
283
+ // `agents` is what setup writes now. A config carrying only the older
284
+ // per-harness `models` has never answered the per-role question, so it still
285
+ // counts as unset.
286
+ return config == null || config.agents == null;
22
287
  }
23
- const AGENT_LABELS = { claude: "Claude Code", codex: "Codex", omp: "omp" };
24
- function agentLabel(agent) {
25
- return AGENT_LABELS[agent] ?? agent;
288
+ /**
289
+ * The catalog of every installed harness, flattened into one list.
290
+ *
291
+ * The group is the bare agent id rather than a prettier label because it is
292
+ * also what free text has to type as a prefix ("codex:gpt-5.7"). A header
293
+ * reading "Codex" over rows you reach by typing "codex" would hide that.
294
+ */
295
+ /** One normalization for every id, so the catalog and the pin agree on a key. */
296
+ function modelId(raw) {
297
+ return typeof raw === "string" ? raw.trim() : "";
298
+ }
299
+ function catalogItems(installed) {
300
+ const known = new Set();
301
+ const items = [];
302
+ for (const harness of installed) {
303
+ for (const provider of harness.providers) {
304
+ for (const model of provider.models) {
305
+ // A driver reporting a blank id used to become a selectable blank row
306
+ // that Enter would save as the model. The line wizard guarded this and
307
+ // the port dropped the guard.
308
+ const id = modelId(model.id);
309
+ if (!id)
310
+ continue;
311
+ // Keyed by both halves: two harnesses listing the same id are two real
312
+ // choices, and only a repeat within one harness is a duplicate.
313
+ const key = itemKey(harness.agent, id);
314
+ if (known.has(key))
315
+ continue;
316
+ known.add(key);
317
+ // The name is carried for the filter, not for the row: most of the
318
+ // catalog spells a version readably there ("Claude Opus 5") and only in
319
+ // the id the way it is typed.
320
+ const name = typeof model.name === "string" ? model.name.trim() : "";
321
+ items.push({
322
+ id,
323
+ label: id,
324
+ group: harness.agent,
325
+ harness: harness.agent,
326
+ ...(name && name !== id ? { name } : {}),
327
+ });
328
+ }
329
+ }
330
+ }
331
+ return { items, known };
26
332
  }
27
- function pinnedFor(harness, ids, configured) {
333
+ function pinnedFor(installed, known, configured, fallbackHarness) {
28
334
  if (configured) {
335
+ const inCatalog = known.has(itemKey(configured.harness, configured.model));
29
336
  // A saved id the catalog no longer lists still gets shown. Hiding it is how
30
337
  // the wizard used to answer `open`'s own "that model is gone, run setup":
31
338
  // the user arrived here and could not see which model it meant. Marking it
32
339
  // synthetic makes keeping it cost the same second Enter as typing it.
33
- return ids.includes(configured)
34
- ? { id: configured, label: configured, note: "atual" }
35
- : { id: configured, label: configured, note: "atual, fora do catalogo", synthetic: true };
340
+ return {
341
+ id: configured.model,
342
+ label: configured.model,
343
+ group: configured.harness,
344
+ harness: configured.harness,
345
+ note: inCatalog ? `atual · ${configured.harness}` : `atual · ${configured.harness}, fora do catalogo`,
346
+ ...(inCatalog ? {} : { synthetic: true }),
347
+ };
36
348
  }
349
+ // Nothing saved for this role, so the suggestion comes from the harness that
350
+ // runs everything else by default, and only if that harness names a default.
351
+ const harness = installed.find((candidate) => candidate.agent === fallbackHarness);
352
+ if (!harness)
353
+ return undefined;
37
354
  for (const provider of harness.providers) {
38
- const real = provider.models.find((model) => model.isDefault && ids.includes(model.id));
39
- if (real)
40
- return { id: real.id, label: real.id, note: "padrao" };
355
+ // Trimmed the same way the catalog trims, or the lookup misses its own row
356
+ // and a harness that pads its ids ends up pinning nothing.
357
+ const real = provider.models
358
+ .map((model) => ({ id: modelId(model.id), isDefault: model.isDefault }))
359
+ .find((model) => model.isDefault && known.has(itemKey(harness.agent, model.id)));
360
+ if (real) {
361
+ return {
362
+ id: real.id,
363
+ label: real.id,
364
+ group: harness.agent,
365
+ harness: harness.agent,
366
+ note: `padrao · ${harness.agent}`,
367
+ };
368
+ }
41
369
  }
42
- // With no config and no isDefault, ids[0] is just alphabetical order. Hoisting
43
- // that and calling it the default would stamp a coin flip as a recommendation.
370
+ // With no config and no isDefault, the first row is just catalog order.
371
+ // Hoisting that and calling it the default would stamp a coin flip as a
372
+ // recommendation.
44
373
  }
45
- export function buildAgentScreen(harness, index, total, configured) {
46
- const seen = new Set();
47
- const grouped = [];
48
- for (const provider of harness.providers) {
49
- for (const model of provider.models) {
50
- // A driver reporting a blank id used to become a selectable blank row
51
- // that Enter would save as the model. The line wizard guarded this and
52
- // the port dropped the guard.
53
- const id = typeof model.id === "string" ? model.id.trim() : "";
54
- if (!id || seen.has(id))
55
- continue;
56
- seen.add(id);
57
- // The name is carried for the filter, not for the row: most of the
58
- // catalog spells a version readably there ("Claude Opus 5") and only in
59
- // the id the way it is typed.
60
- const name = typeof model.name === "string" ? model.name.trim() : "";
61
- grouped.push({
62
- id,
63
- label: id,
64
- group: provider.provider,
65
- ...(name && name !== id ? { name } : {}),
66
- });
67
- }
68
- }
69
- const pinned = pinnedFor(harness, [...seen], configured);
70
- const items = pinned ? [pinned, ...grouped.filter((item) => item.id !== pinned.id)] : grouped;
374
+ export function buildRoleScreen(role, installed, index, total, configured, fallbackHarness = "claude") {
375
+ const { items: catalog, known } = catalogItems(installed);
376
+ const pinned = pinnedFor(installed, known, configured, fallbackHarness);
377
+ const pinnedKey = pinned ? itemKey(pinned.harness, pinned.id) : undefined;
378
+ const items = pinned
379
+ ? [pinned, ...catalog.filter((item) => itemKey(item.harness, item.id) !== pinnedKey)]
380
+ : catalog;
381
+ // One screen now spans every harness, so a harness that answered with an
382
+ // error no longer owns the error line by itself. Naming each one keeps a
383
+ // missing catalog readable as "codex is why codex has no rows".
384
+ const errors = installed
385
+ .filter((harness) => harness.error)
386
+ .map((harness) => `${harness.agent}: ${harness.error}`);
71
387
  return {
72
- agent: harness.agent,
73
- title: agentLabel(harness.agent),
388
+ role,
389
+ title: role,
74
390
  counter: `agente ${index + 1} de ${total}`,
75
- error: harness.error,
391
+ ...(errors.length > 0 ? { error: errors.join(" · ") } : {}),
76
392
  items,
77
393
  pinned: pinned !== undefined,
78
- known: seen,
394
+ known,
395
+ harnesses: new Set(installed.map((harness) => harness.agent)),
79
396
  };
80
397
  }
81
- export function buildScreens(harnesses, configured) {
398
+ export function buildScreens(roles, harnesses, configured, fallbackHarness = "claude") {
82
399
  const seen = new Set();
83
400
  // A merged disk cache can hand back the same agent twice.
84
401
  const installed = harnesses.filter((harness) => {
@@ -87,18 +404,21 @@ export function buildScreens(harnesses, configured) {
87
404
  seen.add(harness.agent);
88
405
  return true;
89
406
  });
90
- return installed.map((harness, index) => buildAgentScreen(harness, index, installed.length, configured[harness.agent]));
407
+ if (installed.length === 0)
408
+ return [];
409
+ return roles.map((role, index) => buildRoleScreen(role, installed, index, roles.length, configured[role], fallbackHarness));
91
410
  }
92
411
  export function collectSelections(results, existing, shown) {
93
412
  // Starts from what was already saved: skipping has to leave it intact, and a
94
413
  // brand new map would wipe a choice made on an earlier run.
95
- const models = { ...(existing ?? {}) };
414
+ const agents = { ...(existing ?? {}) };
96
415
  for (const result of results) {
97
- if (result.kind === "picked")
98
- models[result.agent] = result.id;
416
+ if (result.kind !== "picked")
417
+ continue;
418
+ agents[result.role] = { harness: result.harness, model: result.id };
99
419
  }
100
420
  const aborted = results.some((result) => result.kind === "aborted");
101
- return { models, write: shown > 0 && !aborted };
421
+ return { agents, write: shown > 0 && !aborted };
102
422
  }
103
423
  /** Below this no list survives once the chrome is placed. */
104
424
  const MIN_ROWS = 8;
@@ -131,11 +451,11 @@ export async function runModelSetupWizard(options = {}) {
131
451
  console.error(`Warning: Could not discover models: ${error instanceof Error ? error.message : String(error)}`);
132
452
  harnesses = [];
133
453
  }
134
- const screens = buildScreens(harnesses, config.models ?? {});
135
- // Writing `models` is what marks first-run setup as done. Doing that after
454
+ const roleScreens = buildScreens(ROLES, harnesses, config.agents ?? {}, config.defaultAgent ?? "claude");
455
+ // Writing `agents` is what marks first-run setup as done. Doing that after
136
456
  // showing nothing would spend the single prompt the user ever gets.
137
- if (screens.length === 0) {
138
- console.error("Warning: No installed agent reported any model; skipping model setup.");
457
+ if (roleScreens.length === 0) {
458
+ console.error("Warning: No installed harness reported any model; skipping model setup.");
139
459
  return config;
140
460
  }
141
461
  const dim = options.dimensions ?? readDimensions(output);
@@ -144,39 +464,636 @@ export async function runModelSetupWizard(options = {}) {
144
464
  return config;
145
465
  }
146
466
  const paint = colors(Boolean(output.isTTY) && !process.env.NO_COLOR);
147
- const results = await runScreens(screens, { input, output, onResize: watchResize }, paint);
148
- const { models, write } = collectSelections(results, config.models, screens.length);
467
+ const screens = [
468
+ ...roleScreens,
469
+ buildOrchestratorScreen(config, roleScreens.length, roleScreens.length + 1),
470
+ ];
471
+ const total = screens.length + 2;
472
+ const sandbox = buildSandboxScreen(config, screens.length, total);
473
+ const autocompact = buildAutocompactScreen(config, screens.length + 1, total);
474
+ const results = await runScreens([...screens, sandbox, autocompact], { input, output, onResize: watchResize }, paint);
475
+ if (results.some((result) => result.kind === "aborted")) {
476
+ console.error("Warning: Model setup was interrupted; nothing was saved.");
477
+ return config;
478
+ }
479
+ const answeredResults = results.filter((result) => result.kind !== "aborted");
480
+ const agentResults = answeredResults.filter((result) => roleScreens.some((screen) => screen.role === result.role));
481
+ const { agents, write } = collectSelections(agentResults, config.agents, roleScreens.length);
149
482
  if (!write) {
150
483
  console.error("Warning: Model setup was interrupted; nothing was saved.");
151
484
  return config;
152
485
  }
153
- const updatedConfig = { ...config, models };
486
+ const orchestratorSelection = answeredResults.find((result) => result.role === ORCHESTRATOR_SCREEN_ROLE);
487
+ if (orchestratorSelection === undefined) {
488
+ console.error("Warning: Orchestrator setup was interrupted; nothing was saved.");
489
+ return config;
490
+ }
491
+ let orchestrator;
492
+ if (orchestratorSelection.kind === "picked" && orchestratorSelection.id === "custom") {
493
+ const parameterResults = answeredResults.filter((result) => result.role.startsWith(`${ORCHESTRATOR_SCREEN_ROLE}.`));
494
+ orchestrator = collectOrchestratorSelection(orchestratorSelection, parameterResults, config);
495
+ if (orchestrator === undefined) {
496
+ console.error("Warning: Orchestrator setup was invalid; nothing was saved.");
497
+ return config;
498
+ }
499
+ }
500
+ else if (orchestratorSelection.kind === "picked") {
501
+ orchestrator = collectOrchestratorSelection(orchestratorSelection, [], config);
502
+ if (orchestrator === undefined) {
503
+ console.error("Warning: Orchestrator setup was invalid; nothing was saved.");
504
+ return config;
505
+ }
506
+ }
507
+ const updatedConfig = { ...config, agents };
508
+ if (orchestrator !== undefined)
509
+ updatedConfig.orchestrator = orchestrator;
510
+ const sandboxSelection = answeredResults.find((result) => result.role === SANDBOX_SCREEN_ROLE);
511
+ if (sandboxSelection?.kind === "picked") {
512
+ if (sandboxSelection.id === SANDBOX_OFF || sandboxSelection.id === SANDBOX_ON) {
513
+ updatedConfig.defaultSandbox = sandboxSelection.id;
514
+ }
515
+ }
516
+ const autocompactSelection = answeredResults.find((result) => result.role === AUTOCOMPACT_SCREEN_ROLE);
517
+ if (autocompactSelection?.kind === "picked") {
518
+ if (autocompactSelection.id === AUTOCOMPACT_ON) {
519
+ updatedConfig.autocompact = { ...config.autocompact, enabled: true };
520
+ }
521
+ else if (autocompactSelection.id === AUTOCOMPACT_OFF && config.autocompact !== undefined) {
522
+ updatedConfig.autocompact = { ...config.autocompact, enabled: false };
523
+ }
524
+ }
525
+ let saved = false;
154
526
  try {
155
527
  (options.save ?? saveConfig)(updatedConfig);
528
+ saved = true;
156
529
  }
157
530
  catch (error) {
158
531
  console.error(`Warning: Could not save config: ${error instanceof Error ? error.message : String(error)}`);
159
532
  }
160
- const summary = screens
161
- .map((screen) => `${screen.title} ${models[screen.agent] ?? "unset"}`)
533
+ if (!saved)
534
+ return updatedConfig;
535
+ const summary = roleScreens
536
+ .map((screen) => {
537
+ const binding = agents[screen.role];
538
+ return `${screen.role} ${binding ? `${binding.harness}:${binding.model}` : "unset"}`;
539
+ })
162
540
  .join(" · ");
163
- output.write(`\n saved: ${summary}\n\n`);
541
+ const modeSummary = orchestrator === undefined ? "" : ` · orchestrator ${orchestratorModeLabel(orchestrator)}`;
542
+ output.write(`\n saved: ${summary}${modeSummary}\n\n`);
164
543
  return updatedConfig;
165
544
  }
166
- export function registerSetupCommand(program) {
545
+ export class SetupUsageError extends Error {
546
+ constructor(message) {
547
+ super(message);
548
+ this.name = "SetupUsageError";
549
+ }
550
+ }
551
+ function invalidBindMessage(value) {
552
+ return `Invalid --bind "${value}": expected role=harness:model (role: general|orchestrator|reviewer|auditor; harness: claude|codex|opencode|omp; model: non-empty and without whitespace, control characters or '=')`;
553
+ }
554
+ export function parseBind(value) {
555
+ const equals = value.indexOf("=");
556
+ const roleValue = equals < 0 ? "" : value.slice(0, equals);
557
+ const right = equals < 0 ? "" : value.slice(equals + 1);
558
+ const colon = right.indexOf(":");
559
+ const harnessValue = colon < 0 ? "" : right.slice(0, colon);
560
+ const model = colon < 0 ? "" : right.slice(colon + 1);
561
+ const role = ROLES.includes(roleValue) ? roleValue : undefined;
562
+ const harness = isAgentId(harnessValue) ? harnessValue : undefined;
563
+ const modelPattern = /^[^\p{White_Space}\p{Cc}\p{Cf}=]+$/u;
564
+ if (!role || !harness || !modelPattern.test(model))
565
+ throw new SetupUsageError(invalidBindMessage(value));
566
+ return { role, binding: { harness, model } };
567
+ }
568
+ export const parseSetupBind = parseBind;
569
+ function parseError(message, json) {
570
+ return { ok: false, message, json };
571
+ }
572
+ function isSetupFlag(arg) {
573
+ return arg.startsWith("-");
574
+ }
575
+ export function parseSetupArgs(args) {
576
+ const json = args.some((arg) => arg === "--json" || arg.startsWith("--json="));
577
+ const binds = [];
578
+ let refresh = false;
579
+ let nonInteractive = false;
580
+ let dryRun = false;
581
+ for (let index = 0; index < args.length; index += 1) {
582
+ const arg = args[index];
583
+ if (arg === "--refresh") {
584
+ refresh = true;
585
+ continue;
586
+ }
587
+ if (arg === "--non-interactive") {
588
+ nonInteractive = true;
589
+ continue;
590
+ }
591
+ if (arg === "--dry-run") {
592
+ dryRun = true;
593
+ continue;
594
+ }
595
+ if (arg === "--json") {
596
+ continue;
597
+ }
598
+ if (arg === "--bind") {
599
+ const value = args[index + 1];
600
+ if (value === undefined || isSetupFlag(value)) {
601
+ return parseError('Option "--bind" expects role=harness:model.', json);
602
+ }
603
+ index += 1;
604
+ try {
605
+ binds.push(parseBind(value));
606
+ }
607
+ catch (error) {
608
+ return parseError(error instanceof Error ? error.message : String(error), json);
609
+ }
610
+ continue;
611
+ }
612
+ if (arg.startsWith("--bind=")) {
613
+ try {
614
+ binds.push(parseBind(arg.slice("--bind=".length)));
615
+ }
616
+ catch (error) {
617
+ return parseError(error instanceof Error ? error.message : String(error), json);
618
+ }
619
+ continue;
620
+ }
621
+ if (arg.startsWith("-"))
622
+ return parseError(`Unknown option "${arg}".`, json);
623
+ return parseError(`Unexpected argument "${arg}".`, json);
624
+ }
625
+ return {
626
+ ok: true,
627
+ options: {
628
+ refresh,
629
+ nonInteractive,
630
+ json,
631
+ dryRun,
632
+ binds,
633
+ batch: nonInteractive || json || dryRun || binds.length > 0,
634
+ },
635
+ };
636
+ }
637
+ function writeLine(stream, message) {
638
+ if (stream) {
639
+ stream.write(`${message}\n`);
640
+ }
641
+ else {
642
+ console.error(message);
643
+ }
644
+ }
645
+ function configValidation(read) {
646
+ return {
647
+ status: read.status,
648
+ source: read.source,
649
+ path: read.path,
650
+ message: read.status === "invalid" ? read.message : null,
651
+ };
652
+ }
653
+ function notNeededCatalog() {
654
+ return { status: "not-needed", source: "none", ageMs: null, message: null };
655
+ }
656
+ function emptyEnvelope(message, code, path) {
657
+ return {
658
+ proposta: null,
659
+ validacoes: {
660
+ config: { status: "not-run", source: "none", path, message: "not-run" },
661
+ catalogo: { status: "not-needed", source: "none", ageMs: null, message: "not-run" },
662
+ bindings: [],
663
+ },
664
+ mudancas: [],
665
+ resultado: { status: "error", code, saved: false, message },
666
+ };
667
+ }
668
+ function jsonObject(value) {
669
+ return typeof value === "object" && value !== null && !Array.isArray(value);
670
+ }
671
+ function jsonValue(value) {
672
+ if (value === undefined)
673
+ return null;
674
+ if (Array.isArray(value))
675
+ return value.map(jsonValue);
676
+ if (jsonObject(value)) {
677
+ return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right)).map((key) => [key, jsonValue(value[key])]));
678
+ }
679
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
680
+ return value;
681
+ }
682
+ return null;
683
+ }
684
+ function jsonEqual(left, right) {
685
+ return JSON.stringify(jsonValue(left)) === JSON.stringify(jsonValue(right));
686
+ }
687
+ function pointerPart(value) {
688
+ return value.replaceAll("~", "~0").replaceAll("/", "~1");
689
+ }
690
+ function diffAt(output, pathValue, beforePresent, before, afterPresent, after, expandObjectChildren = false) {
691
+ const beforeObject = beforePresent && jsonObject(before) ? before : undefined;
692
+ const afterObject = afterPresent && jsonObject(after) ? after : undefined;
693
+ if (beforeObject !== undefined && afterObject !== undefined) {
694
+ const keys = [...new Set([...Object.keys(beforeObject), ...Object.keys(afterObject)])].sort((left, right) => left.localeCompare(right));
695
+ if (keys.length === 0)
696
+ return;
697
+ for (const key of keys) {
698
+ diffAt(output, `${pathValue}/${pointerPart(key)}`, Object.hasOwn(beforeObject, key), beforeObject[key], Object.hasOwn(afterObject, key), afterObject[key]);
699
+ }
700
+ return;
701
+ }
702
+ if (!beforePresent && afterObject !== undefined) {
703
+ if (expandObjectChildren) {
704
+ const keys = Object.keys(afterObject).sort((left, right) => left.localeCompare(right));
705
+ if (keys.length === 0) {
706
+ output.push({
707
+ path: pathValue,
708
+ beforePresent: false,
709
+ before: null,
710
+ afterPresent: true,
711
+ after: jsonValue(afterObject),
712
+ });
713
+ return;
714
+ }
715
+ for (const key of keys) {
716
+ const value = afterObject[key];
717
+ const childPath = `${pathValue}/${pointerPart(key)}`;
718
+ if (jsonObject(value)) {
719
+ output.push({
720
+ path: childPath,
721
+ beforePresent: false,
722
+ before: null,
723
+ afterPresent: true,
724
+ after: jsonValue(value),
725
+ });
726
+ }
727
+ else {
728
+ diffAt(output, childPath, false, undefined, true, value);
729
+ }
730
+ }
731
+ return;
732
+ }
733
+ output.push({
734
+ path: pathValue,
735
+ beforePresent: false,
736
+ before: null,
737
+ afterPresent: true,
738
+ after: jsonValue(afterObject),
739
+ });
740
+ return;
741
+ }
742
+ if (beforeObject !== undefined && !afterPresent) {
743
+ if (expandObjectChildren) {
744
+ const keys = Object.keys(beforeObject).sort((left, right) => left.localeCompare(right));
745
+ if (keys.length === 0) {
746
+ output.push({
747
+ path: pathValue,
748
+ beforePresent: true,
749
+ before: jsonValue(beforeObject),
750
+ afterPresent: false,
751
+ after: null,
752
+ });
753
+ return;
754
+ }
755
+ for (const key of keys) {
756
+ const value = beforeObject[key];
757
+ const childPath = `${pathValue}/${pointerPart(key)}`;
758
+ if (jsonObject(value)) {
759
+ output.push({
760
+ path: childPath,
761
+ beforePresent: true,
762
+ before: jsonValue(value),
763
+ afterPresent: false,
764
+ after: null,
765
+ });
766
+ }
767
+ else {
768
+ diffAt(output, childPath, true, value, false, undefined);
769
+ }
770
+ }
771
+ return;
772
+ }
773
+ output.push({
774
+ path: pathValue,
775
+ beforePresent: true,
776
+ before: jsonValue(beforeObject),
777
+ afterPresent: false,
778
+ after: null,
779
+ });
780
+ return;
781
+ }
782
+ if (beforePresent === afterPresent && (!beforePresent || jsonEqual(before, after)))
783
+ return;
784
+ output.push({
785
+ path: pathValue,
786
+ beforePresent,
787
+ before: jsonValue(before),
788
+ afterPresent,
789
+ after: jsonValue(after),
790
+ });
791
+ }
792
+ export function diffConfig(before, after) {
793
+ const output = [];
794
+ const beforeObject = jsonObject(before) ? before : {};
795
+ const afterObject = jsonObject(after) ? after : {};
796
+ const keys = [...new Set([...Object.keys(beforeObject), ...Object.keys(afterObject)])].sort((left, right) => left.localeCompare(right));
797
+ for (const key of keys) {
798
+ diffAt(output, `/${pointerPart(key)}`, Object.hasOwn(beforeObject, key), beforeObject[key], Object.hasOwn(afterObject, key), afterObject[key], true);
799
+ }
800
+ return output.sort((left, right) => left.path.localeCompare(right.path));
801
+ }
802
+ function catalogContains(catalog, model) {
803
+ return catalog.providers.some((provider) => provider.models.some((candidate) => candidate.id === model || (candidate.aliases !== undefined && candidate.aliases.some((alias) => alias === model))));
804
+ }
805
+ function validateBindings(bindings, catalog) {
806
+ const byAgent = new Map(catalog.models.map((item) => [item.agent, item]));
807
+ const entries = bindings.map(({ role, binding }) => {
808
+ const found = byAgent.get(binding.harness);
809
+ if (found?.available === false) {
810
+ return {
811
+ role,
812
+ harness: binding.harness,
813
+ model: binding.model,
814
+ status: "harness-unavailable",
815
+ message: `Cannot apply binding for role "${role}": harness "${binding.harness}" is unavailable.`,
816
+ };
817
+ }
818
+ if (catalog.status === "unavailable" || found === undefined) {
819
+ return {
820
+ role,
821
+ harness: binding.harness,
822
+ model: binding.model,
823
+ status: "unverified",
824
+ message: `Cannot validate model "${binding.model}" for harness "${binding.harness}": catalog unavailable.`,
825
+ };
826
+ }
827
+ if (catalog.status === "offline" && !catalogContains(found, binding.model)) {
828
+ return {
829
+ role,
830
+ harness: binding.harness,
831
+ model: binding.model,
832
+ status: "unverified",
833
+ message: `Cannot validate model "${binding.model}" for harness "${binding.harness}": the catalog is stale. Re-run with --refresh.`,
834
+ };
835
+ }
836
+ if (!catalogContains(found, binding.model)) {
837
+ return {
838
+ role,
839
+ harness: binding.harness,
840
+ model: binding.model,
841
+ status: "unknown-model",
842
+ message: `Model "${binding.model}" is not in the ${binding.harness} catalog for role "${role}".`,
843
+ };
844
+ }
845
+ return { role, harness: binding.harness, model: binding.model, status: "accepted", message: "" };
846
+ });
847
+ const failed = entries.find((entry) => entry.status !== "accepted");
848
+ const code = failed === undefined
849
+ ? undefined
850
+ : failed.status === "harness-unavailable"
851
+ ? 11
852
+ : failed.status === "unknown-model"
853
+ ? 12
854
+ : 13;
855
+ let message = null;
856
+ if (catalog.status === "offline") {
857
+ message = failed?.status === "unverified" ? failed.message : "Catalog is stale; using offline cache.";
858
+ }
859
+ else if (catalog.status === "unavailable") {
860
+ message = failed?.message ?? "Catalog unavailable.";
861
+ }
862
+ return { entries, code, message };
863
+ }
864
+ function catalogValidation(catalog, message) {
865
+ if (catalog === undefined)
866
+ return notNeededCatalog();
867
+ return {
868
+ status: catalog.status,
869
+ source: catalog.source,
870
+ ageMs: catalog.ageMs,
871
+ message,
872
+ };
873
+ }
874
+ function configSummary(config) {
875
+ const agents = config.agents ?? {};
876
+ return ROLES.map((role) => {
877
+ const binding = agents[role];
878
+ return `${role} ${binding ? `${binding.harness}:${binding.model}` : "unset"}`;
879
+ }).join(" · ");
880
+ }
881
+ function errorResult(read, proposal, catalog, bindings, changes, code, message) {
882
+ return {
883
+ code,
884
+ envelope: {
885
+ proposta: proposal,
886
+ validacoes: { config: configValidation(read), catalogo: catalog, bindings },
887
+ mudancas: changes,
888
+ resultado: { status: "error", code, saved: false, message },
889
+ },
890
+ };
891
+ }
892
+ export async function runSetupBatch(options, dependencies = {}) {
893
+ const stderr = dependencies.stderr;
894
+ const store = dependencies.configStore ?? createSetupConfigStore();
895
+ let read;
896
+ try {
897
+ read = store.read();
898
+ }
899
+ catch (error) {
900
+ const path = getPaths().configFile;
901
+ const message = `Cannot save config "${path}": ${error instanceof Error ? error.message : String(error)}.`;
902
+ writeLine(stderr, message);
903
+ return {
904
+ code: 15,
905
+ envelope: emptyEnvelope(message, 15, path),
906
+ };
907
+ }
908
+ if (read.status === "invalid") {
909
+ const message = read.readError
910
+ ? `Cannot save config "${getPaths().configFile}": ${read.message}.`
911
+ : read.message ?? `Config file "${read.path}" contains invalid JSON; no changes were written. Repair or move it and retry.`;
912
+ writeLine(stderr, message);
913
+ return errorResult(read, null, notNeededCatalog(), [], [], read.readError ? 15 : 14, message);
914
+ }
915
+ const current = { ...DEFAULT_CONFIG, ...(read.config ?? {}) };
916
+ const lastByRole = new Map();
917
+ options.binds.forEach((binding, index) => lastByRole.set(binding.role, index));
918
+ const winning = options.binds.filter((binding, index) => lastByRole.get(binding.role) === index);
919
+ const existingAgents = jsonObject(current.agents) ? current.agents : {};
920
+ const proposed = options.binds.length === 0
921
+ ? { ...current }
922
+ : {
923
+ ...current,
924
+ agents: {
925
+ ...existingAgents,
926
+ ...Object.fromEntries(winning.map(({ role, binding }) => [role, binding])),
927
+ },
928
+ };
929
+ const changes = diffConfig(current, proposed);
930
+ let catalog;
931
+ let bindingValidations = [];
932
+ let catalogMessage = null;
933
+ if (winning.length > 0 || options.refresh) {
934
+ const agents = winning.length > 0
935
+ ? [...new Set(winning.map(({ binding }) => binding.harness))]
936
+ : undefined;
937
+ catalog = await getBatchModels(dependencies.registry ?? getRegistry(), {
938
+ agents,
939
+ refresh: options.refresh,
940
+ allowNetwork: !options.dryRun || options.refresh,
941
+ now: dependencies.now,
942
+ timeoutMs: dependencies.timeoutMs,
943
+ discover: dependencies.discoverModels,
944
+ saveCache: dependencies.saveCache,
945
+ onDiscoveryStart: () => writeLine(stderr, "discovering models..."),
946
+ });
947
+ if (catalog.discoveryError !== undefined) {
948
+ writeLine(stderr, `Warning: ${catalog.discoveryError}.`);
949
+ }
950
+ if (catalog.cacheWriteFailed)
951
+ writeLine(stderr, "Warning: Could not update models cache.");
952
+ if (winning.length > 0) {
953
+ const validation = validateBindings(winning, catalog);
954
+ bindingValidations = validation.entries;
955
+ catalogMessage = validation.message;
956
+ if (validation.code !== undefined) {
957
+ for (const entry of bindingValidations) {
958
+ if (entry.status !== "accepted")
959
+ writeLine(stderr, entry.message);
960
+ }
961
+ return errorResult(read, proposed, catalogValidation(catalog, catalogMessage), bindingValidations, changes, validation.code, bindingValidations.find((entry) => entry.status !== "accepted")?.message ?? "Setup validation failed.");
962
+ }
963
+ }
964
+ else if (catalog.status === "unavailable") {
965
+ catalogMessage = "Catalog unavailable.";
966
+ const message = "Model catalog unavailable.";
967
+ writeLine(stderr, message);
968
+ return errorResult(read, proposed, catalogValidation(catalog, catalogMessage), [], changes, 13, message);
969
+ }
970
+ else if (catalog.status === "offline") {
971
+ catalogMessage = "Catalog is stale; using offline cache.";
972
+ writeLine(stderr, catalogMessage);
973
+ }
974
+ }
975
+ const validations = {
976
+ config: configValidation(read),
977
+ catalogo: catalogValidation(catalog, catalogMessage),
978
+ bindings: bindingValidations,
979
+ };
980
+ if (winning.length === 0) {
981
+ const message = "Configuration unchanged.";
982
+ if (!options.json)
983
+ writeLine(stderr, message);
984
+ return {
985
+ code: 0,
986
+ envelope: { proposta: proposed, validacoes: validations, mudancas: changes, resultado: { status: "unchanged", code: 0, saved: false, message } },
987
+ };
988
+ }
989
+ if (options.dryRun) {
990
+ const message = "Dry run; configuration not written.";
991
+ if (!options.json)
992
+ writeLine(stderr, message);
993
+ return {
994
+ code: 0,
995
+ envelope: { proposta: proposed, validacoes: validations, mudancas: changes, resultado: { status: "dry-run", code: 0, saved: false, message } },
996
+ };
997
+ }
998
+ const serialized = serializeConfig(proposed);
999
+ let saved = false;
1000
+ try {
1001
+ const canonicalRaw = read.source === "canonical" ? read.raw : null;
1002
+ if (canonicalRaw !== null && canonicalRaw === serialized) {
1003
+ saved = false;
1004
+ }
1005
+ else {
1006
+ saved = store.save(proposed) !== false;
1007
+ }
1008
+ }
1009
+ catch (error) {
1010
+ const message = `Cannot save config "${getPaths().configFile}": ${error instanceof Error ? error.message : String(error)}.`;
1011
+ writeLine(stderr, message);
1012
+ return errorResult(read, proposed, catalogValidation(catalog, catalogMessage), bindingValidations, changes, 15, message);
1013
+ }
1014
+ if (!saved) {
1015
+ const message = "Configuration unchanged.";
1016
+ if (!options.json)
1017
+ writeLine(stderr, message);
1018
+ return {
1019
+ code: 0,
1020
+ envelope: { proposta: proposed, validacoes: validations, mudancas: [], resultado: { status: "unchanged", code: 0, saved: false, message } },
1021
+ };
1022
+ }
1023
+ const message = "Configuration saved.";
1024
+ if (!options.json)
1025
+ writeLine(stderr, `saved: ${configSummary(proposed)}`);
1026
+ return {
1027
+ code: 0,
1028
+ envelope: { proposta: proposed, validacoes: validations, mudancas: changes, resultado: { status: "applied", code: 0, saved: true, message } },
1029
+ };
1030
+ }
1031
+ function commandTokens(opts, command) {
1032
+ const tokens = [];
1033
+ if (opts.refresh)
1034
+ tokens.push("--refresh");
1035
+ if (opts.nonInteractive)
1036
+ tokens.push("--non-interactive");
1037
+ if (opts.json)
1038
+ tokens.push("--json");
1039
+ if (opts.dryRun)
1040
+ tokens.push("--dry-run");
1041
+ const binds = Array.isArray(opts.bind) ? opts.bind : opts.bind === undefined ? [] : [opts.bind];
1042
+ for (const bind of binds) {
1043
+ tokens.push("--bind");
1044
+ if (bind !== true)
1045
+ tokens.push(String(bind));
1046
+ }
1047
+ return tokens.concat(command.args.map(String));
1048
+ }
1049
+ export async function executeSetupAction(args, dependencies = {}) {
1050
+ const parsed = parseSetupArgs(args);
1051
+ const stdout = dependencies.stdout ?? process.stdout;
1052
+ const input = dependencies.input ?? process.stdin;
1053
+ const writeError = (message) => writeLine(dependencies.stderr, message);
1054
+ if (!parsed.ok) {
1055
+ if (parsed.json)
1056
+ stdout.write(`${JSON.stringify(emptyEnvelope(parsed.message, 2, getPaths().configFile))}\n`);
1057
+ writeError(parsed.message);
1058
+ return { code: 2, envelope: parsed.json ? emptyEnvelope(parsed.message, 2, getPaths().configFile) : undefined };
1059
+ }
1060
+ const tty = dependencies.isTTY ?? Boolean(input.isTTY && stdout.isTTY);
1061
+ if (!parsed.options.batch) {
1062
+ if (!tty) {
1063
+ const message = `${getCliName()} setup needs a terminal on both stdin and stdout.`;
1064
+ writeError(message);
1065
+ return { code: 1 };
1066
+ }
1067
+ await runModelSetupWizard({
1068
+ registry: dependencies.registry,
1069
+ input: dependencies.input,
1070
+ output: dependencies.stdout,
1071
+ refresh: parsed.options.refresh,
1072
+ discoverModels: dependencies.wizardDiscoverModels,
1073
+ save: dependencies.saveConfig,
1074
+ isTTY: tty,
1075
+ });
1076
+ return { code: 0 };
1077
+ }
1078
+ const result = await runSetupBatch(parsed.options, dependencies);
1079
+ if (parsed.options.json)
1080
+ stdout.write(`${JSON.stringify(result.envelope)}\n`);
1081
+ return result;
1082
+ }
1083
+ export function registerSetupCommand(program, dependencies = {}) {
167
1084
  program
168
1085
  .command("setup")
169
- .description("Choose the model each installed agent should use")
1086
+ .description("Choose the harness and model each agent should run on")
1087
+ .allowUnknownOption(true)
1088
+ .allowExcessArguments(true)
170
1089
  .option("--refresh", "ignore the cached catalog and rediscover")
171
- .action(async (opts) => {
172
- // The message lives here rather than in the wizard, because `open` calls
173
- // the same function and has to stay quiet when it cannot prompt.
174
- if (!isInteractiveTerminal()) {
175
- console.error("codedeck setup needs a terminal on both stdin and stdout.");
176
- process.exitCode = 1;
177
- return;
178
- }
179
- await runModelSetupWizard({ refresh: opts.refresh });
1090
+ .option("--non-interactive", "run setup without the picker")
1091
+ .option("--json", "output one machine-readable envelope")
1092
+ .option("--dry-run", "show the proposed config without writing it")
1093
+ .option("--bind [binding]", "bind a role to a harness and model", (value, previous = []) => [...previous, value], [])
1094
+ .action(async (opts, command) => {
1095
+ const result = await executeSetupAction(commandTokens(opts, command), dependencies);
1096
+ process.exitCode = result.code;
180
1097
  });
181
1098
  }
182
1099
  //# sourceMappingURL=setup.js.map