@hizliemre/horse-code 0.2.0 → 0.3.1

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 (29) hide show
  1. package/dist/{app-2GPGCDX6.js → app-4WN37LZ3.js} +282 -57
  2. package/dist/{chunk-7JMWPTJ5.js → chunk-27F44PBD.js} +221 -1363
  3. package/dist/{chunk-23CLQ2KO.js → chunk-2WXG35EM.js} +19 -15
  4. package/dist/{chunk-LNW557IO.js → chunk-372X5HHU.js} +2 -2
  5. package/dist/{chunk-5ZV42XGJ.js → chunk-3ACDNDCG.js} +1 -1
  6. package/dist/{chunk-XEGQT5EN.js → chunk-4M6LXNG2.js} +1 -1
  7. package/dist/{chunk-6OSEQOYY.js → chunk-6S4WWQMN.js} +2 -2
  8. package/dist/{chunk-LLL7QWXB.js → chunk-BFIZMM4G.js} +6 -6
  9. package/dist/chunk-CYLPQWIF.js +214 -0
  10. package/dist/chunk-G45RWL7S.js +289 -0
  11. package/dist/{chunk-AE36LLL2.js → chunk-JLWQCA7B.js} +2 -209
  12. package/dist/{chunk-KKWZBZYK.js → chunk-JR2JLRE3.js} +26 -4
  13. package/dist/{run-P6ZYL5JL.js → chunk-QJYVZPLG.js} +133 -389
  14. package/dist/{chunk-UGESK765.js → chunk-UTHLEW5V.js} +1 -1
  15. package/dist/chunk-YULQ4URQ.js +1220 -0
  16. package/dist/{chunk-KAGKX2YT.js → chunk-ZPJP2VH5.js} +10 -1
  17. package/dist/cli.js +586 -104
  18. package/dist/{fix-JOIXQFVP.js → fix-QCL5AITT.js} +10 -8
  19. package/dist/{ongoing-WHYXPW24.js → ongoing-6NUSPSCV.js} +3 -2
  20. package/dist/{project-graph-5HNPRFQG.js → project-graph-OGIM2B33.js} +1 -1
  21. package/dist/run-V5ZLZ3LS.js +274 -0
  22. package/dist/{save-skills-X7U3KCPU.js → save-skills-NPKTYNAF.js} +2 -1
  23. package/dist/{trace-X6TU3AG6.js → trace-UVMZZRA5.js} +1 -1
  24. package/dist/{trace-adopt-URECQWJV.js → trace-adopt-7HWELJFE.js} +1 -1
  25. package/dist/{trace-run-7U4WJZ3V.js → trace-run-CZWEZ4R6.js} +8 -4
  26. package/dist/{triage-FCYHD2AQ.js → triage-IFCVL5MA.js} +7 -6
  27. package/dist/{verify-6SC4I77M.js → verify-HWZBTK5X.js} +16 -12
  28. package/package.json +1 -1
  29. package/dist/chunk-MRZVA5JB.js +0 -163
@@ -0,0 +1,289 @@
1
+ // src/agents/cli-auth.ts
2
+ import { spawnSync } from "child_process";
3
+
4
+ // src/agents/cli-agent.ts
5
+ import { spawn } from "child_process";
6
+ var CLI_KINDS = ["claude", "codex", "grok", "zai"];
7
+ function cliBinary(kind) {
8
+ return kind === "zai" ? "claude" : kind;
9
+ }
10
+ var CLI_ERROR_UNSPOKEN = "the CLI reported an error";
11
+ function cliArgs(kind, prompt, extra = []) {
12
+ if (kind === "claude" || kind === "zai") {
13
+ return ["--output-format", "stream-json", "--verbose", ...extra, "-p", "--", prompt];
14
+ }
15
+ if (kind === "codex") {
16
+ return ["exec", "--json", "--skip-git-repo-check", ...extra, "--", prompt];
17
+ }
18
+ return ["--output-format", "streaming-messages-json", ...extra, `--single=${prompt}`];
19
+ }
20
+ function decodeClaudeEvent(line) {
21
+ let e;
22
+ try {
23
+ e = JSON.parse(line);
24
+ } catch {
25
+ return void 0;
26
+ }
27
+ const type = e.type;
28
+ if (type === "rate_limit_event") {
29
+ const info = e.rate_limit_info ?? {};
30
+ const status = String(info.status ?? "unknown");
31
+ const raw = info.unifiedWindows ?? {};
32
+ const windows = {};
33
+ for (const [name, w] of Object.entries(raw)) windows[name] = w?.utilization ?? 0;
34
+ const quota = {
35
+ status,
36
+ windows,
37
+ ...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt } : {}
38
+ };
39
+ return status.startsWith("allowed") ? { quota } : {
40
+ quota,
41
+ /**
42
+ * The reset time rides along, as an ISO instant rather than prose.
43
+ *
44
+ * A spent five-hour window reopens; without saying when, the only safe bench is "the rest of the
45
+ * run", which on a ten-hour board writes off a subscription for hours after it recovered. The
46
+ * gateway's wordings said "reset after 4h" and nothing ever parsed them — see `quotaResetAt`.
47
+ */
48
+ rateLimited: `${status} \u2014 ${describeWindows(windows)}` + (quota.resetsAt ? ` (resets ${new Date(quota.resetsAt * 1e3).toISOString()})` : "")
49
+ };
50
+ }
51
+ if (type === "assistant") {
52
+ const msg = e.message;
53
+ const parts = Array.isArray(msg?.content) ? msg.content : [];
54
+ const text = parts.filter((b) => typeof b === "object" && b !== null && b.type === "text").map((b) => b.text).join("");
55
+ const tool = parts.find((b) => typeof b === "object" && b !== null && b.type === "tool_use");
56
+ return {
57
+ ...text ? { text } : {},
58
+ ...msg?.model ? { served: msg.model } : {},
59
+ ...tool ? { tool: { name: tool.name, ...targetOf(tool.input) ? { target: targetOf(tool.input) } : {} } } : {}
60
+ };
61
+ }
62
+ if (type === "user") {
63
+ const parts = e.message?.content;
64
+ const failed = (Array.isArray(parts) ? parts : []).find(
65
+ (b) => typeof b === "object" && b !== null && b.type === "tool_result" && b.is_error === true
66
+ );
67
+ return failed ? { tool: { name: "tool", ok: false } } : void 0;
68
+ }
69
+ if (type === "result") {
70
+ const u = e.usage ?? {};
71
+ const cost = e.total_cost_usd;
72
+ return {
73
+ usage: {
74
+ freshTokens: u.input_tokens ?? 0,
75
+ cachedTokens: u.cache_read_input_tokens ?? 0,
76
+ cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
77
+ outputTokens: u.output_tokens ?? 0,
78
+ ...cost !== void 0 ? { costUsd: cost } : {}
79
+ },
80
+ ...e.subtype === "error_during_execution" ? { error: String(e.result ?? CLI_ERROR_UNSPOKEN) } : {}
81
+ };
82
+ }
83
+ return void 0;
84
+ }
85
+ function decodeCodexEvent(line) {
86
+ let e;
87
+ try {
88
+ e = JSON.parse(line);
89
+ } catch {
90
+ return void 0;
91
+ }
92
+ const type = String(e.type ?? "");
93
+ if (/rate.?limit/i.test(type)) return { rateLimited: String(e.message ?? "rate limited by the CLI") };
94
+ if (type === "item.completed") {
95
+ const item = e.item;
96
+ if (item?.type === "agent_message" && item.text) return { text: item.text };
97
+ if (item?.type && item.type !== "agent_message") {
98
+ const changes = item.changes;
99
+ const first = Array.isArray(changes) ? changes.find((c) => typeof c?.path === "string")?.path : void 0;
100
+ const more = Array.isArray(changes) && changes.length > 1 ? ` +${changes.length - 1}` : "";
101
+ return {
102
+ tool: {
103
+ name: item.name ?? item.type,
104
+ ...first ? { target: `${first}${more}` } : {}
105
+ }
106
+ };
107
+ }
108
+ return void 0;
109
+ }
110
+ if (type === "turn.completed") {
111
+ const u = e.usage ?? {};
112
+ return {
113
+ usage: {
114
+ freshTokens: u.input_tokens ?? 0,
115
+ cachedTokens: u.cached_input_tokens ?? 0,
116
+ cacheWriteTokens: u.cache_write_input_tokens ?? 0,
117
+ outputTokens: u.output_tokens ?? 0
118
+ }
119
+ };
120
+ }
121
+ if (type === "turn.failed" || type === "error") {
122
+ return { error: String(e.message ?? "codex reported an error") };
123
+ }
124
+ return void 0;
125
+ }
126
+ function describeWindows(windows) {
127
+ const parts = Object.entries(windows).map(([k, v]) => `${k} ${Math.round(v * 100)}%`);
128
+ return parts.length ? parts.join(", ") : "no window reported";
129
+ }
130
+ var SYNTHETIC = "<synthetic>";
131
+ function targetOf(input) {
132
+ for (const k of ["file_path", "path", "filePath", "notebook_path"]) {
133
+ const v = input?.[k];
134
+ if (typeof v === "string" && v) return v;
135
+ }
136
+ return void 0;
137
+ }
138
+ function makeStreamReader(decode, onEvent) {
139
+ let pending = "";
140
+ const drain = (upToNewline) => {
141
+ const lines = pending.split("\n");
142
+ pending = upToNewline ? lines.pop() ?? "" : "";
143
+ for (const line of lines) {
144
+ if (!line.trim()) continue;
145
+ const ev = decode(line);
146
+ if (ev) onEvent(ev);
147
+ }
148
+ };
149
+ return {
150
+ push(chunk) {
151
+ pending += chunk;
152
+ drain(true);
153
+ },
154
+ end() {
155
+ if (pending.trim()) drain(false);
156
+ }
157
+ };
158
+ }
159
+ function reportedError(decoded, stderr, exitCode) {
160
+ if (decoded && decoded !== CLI_ERROR_UNSPOKEN) return decoded;
161
+ const spoken = exitCode !== 0 ? stderr.trim().slice(0, 500) : "";
162
+ return spoken || decoded || void 0;
163
+ }
164
+ async function runCliAgent(run) {
165
+ const decode = run.kind === "codex" ? decodeCodexEvent : decodeClaudeEvent;
166
+ const args = cliArgs(run.kind, run.prompt, run.args ?? []);
167
+ return new Promise((resolve) => {
168
+ let child;
169
+ try {
170
+ child = spawn(cliBinary(run.kind), args, {
171
+ cwd: run.cwd,
172
+ signal: run.signal,
173
+ stdio: ["ignore", "pipe", "pipe"],
174
+ ...run.configDir ? { env: { ...process.env, ...profileEnv(run.kind, run.configDir) } } : {}
175
+ });
176
+ } catch (e) {
177
+ resolve({ text: "", error: e instanceof Error ? e.message : String(e), exitCode: -1 });
178
+ return;
179
+ }
180
+ let text = "";
181
+ let usage;
182
+ let rateLimited;
183
+ let served;
184
+ let quota;
185
+ let error;
186
+ let stderr = "";
187
+ const reader = makeStreamReader(decode, (ev) => {
188
+ if (ev.text) text += ev.text;
189
+ if (ev.usage) usage = ev.usage;
190
+ if (ev.rateLimited) rateLimited = ev.rateLimited;
191
+ if (ev.served) served = ev.served;
192
+ if (ev.quota) quota = ev.quota;
193
+ if (ev.error) error = ev.error;
194
+ run.onEvent?.(ev);
195
+ });
196
+ child.stdout?.on("data", (d) => reader.push(d.toString()));
197
+ child.stderr?.on("data", (d) => {
198
+ stderr += d.toString();
199
+ });
200
+ child.on("error", (e) => resolve({ text, ...usage ? { usage } : {}, error: e.message, exitCode: -1 }));
201
+ child.on("close", (code) => {
202
+ reader.end();
203
+ const reported = reportedError(error, stderr, code ?? -1);
204
+ resolve({
205
+ text,
206
+ ...usage ? { usage } : {},
207
+ ...rateLimited ? { rateLimited } : {},
208
+ ...quota ? { quota } : {},
209
+ ...served ? { served } : {},
210
+ ...reported ? { error: reported } : {},
211
+ exitCode: code ?? -1
212
+ });
213
+ });
214
+ });
215
+ }
216
+
217
+ // src/agents/cli-auth.ts
218
+ function profileEnv(kind, configDir) {
219
+ if (!configDir) return {};
220
+ if (kind === "claude" || kind === "zai") return { CLAUDE_CONFIG_DIR: configDir };
221
+ if (kind === "codex") return { CODEX_HOME: configDir };
222
+ return { GROK_HOME: configDir };
223
+ }
224
+ function readAuthStatus(kind, out) {
225
+ if (kind === "grok") {
226
+ const m = /you are logged in with\s+(.+)/i.exec(out);
227
+ if (!m) return { loggedIn: false };
228
+ const plan = m[1].trim().replace(/\.$/, "");
229
+ return { loggedIn: true, ...plan ? { plan } : {} };
230
+ }
231
+ if (kind === "codex") {
232
+ const m = /logged in(?: using (.+))?/i.exec(out);
233
+ if (!m || /not logged in/i.test(out)) return { loggedIn: false };
234
+ const plan = m[1]?.trim();
235
+ return { loggedIn: true, ...plan ? { plan } : {} };
236
+ }
237
+ try {
238
+ const j = JSON.parse(out);
239
+ if (!j.loggedIn) return { loggedIn: false };
240
+ return {
241
+ loggedIn: true,
242
+ ...j.email ? { email: j.email } : {},
243
+ ...j.subscriptionType ? { plan: j.subscriptionType } : {}
244
+ };
245
+ } catch {
246
+ return { loggedIn: false };
247
+ }
248
+ }
249
+ function statusArgs(kind) {
250
+ if (kind === "claude" || kind === "zai") return ["auth", "status"];
251
+ if (kind === "codex") return ["login", "status"];
252
+ return ["models"];
253
+ }
254
+ function loginArgs(kind) {
255
+ return kind === "claude" ? ["auth", "login"] : ["login"];
256
+ }
257
+ function checkProfile(kind, configDir) {
258
+ if (kind === "zai" && !configDir) return { loggedIn: false };
259
+ const r = spawnSync(cliBinary(kind), statusArgs(kind), {
260
+ env: { ...process.env, ...profileEnv(kind, configDir) },
261
+ encoding: "utf8",
262
+ // A status check that hangs must not hang the startup summary with it.
263
+ timeout: 2e4
264
+ });
265
+ if (r.error) return { loggedIn: false };
266
+ return readAuthStatus(kind, `${r.stdout ?? ""}${r.stderr ?? ""}`);
267
+ }
268
+ function runLogin(kind, configDir) {
269
+ if (kind === "zai") {
270
+ return { ok: false, error: "z.ai has no sign-in \u2014 a profile is connected by writing its settings file" };
271
+ }
272
+ const r = spawnSync(cliBinary(kind), loginArgs(kind), {
273
+ env: { ...process.env, ...profileEnv(kind, configDir) },
274
+ stdio: "inherit"
275
+ });
276
+ if (r.error) {
277
+ const e = r.error;
278
+ return e.code === "ENOENT" ? { ok: false, error: `\`${kind}\` is not installed, or not on PATH` } : { ok: false, error: e.message };
279
+ }
280
+ return { ok: r.status === 0 };
281
+ }
282
+
283
+ export {
284
+ checkProfile,
285
+ runLogin,
286
+ CLI_KINDS,
287
+ SYNTHETIC,
288
+ runCliAgent
289
+ };
@@ -1160,212 +1160,6 @@ async function runToCompletion(opts) {
1160
1160
  return last;
1161
1161
  }
1162
1162
 
1163
- // src/tools/registry.ts
1164
- import { z } from "zod";
1165
- var ToolRegistry = class {
1166
- tools = /* @__PURE__ */ new Map();
1167
- /**
1168
- * Registered and callable, but whose SCHEMA is withheld until something asks for it.
1169
- *
1170
- * A schema is paid for on every turn, whether or not the tool is ever used. Measured across twelve runs:
1171
- * 49 MCP tool schemas came to 86,620 characters (~21,655 tokens), 242 calls carried them, and that is
1172
- * ~5.2M of the 21.7M input tokens billed — 24% of everything — for FIVE tool calls, of two distinct tools.
1173
- * The catalogue that names them costs 900 characters (see MAX_TOOL_NOTE_CHARS); it is the schemas that are
1174
- * expensive, and a schema nobody is about to use buys nothing.
1175
- */
1176
- deferred = /* @__PURE__ */ new Set();
1177
- /** Bumped by anything that changes what `schemas()` would return, so the derivation can be cached. */
1178
- version = 0;
1179
- cached;
1180
- register(tool) {
1181
- this.tools.set(tool.name, tool);
1182
- this.deferred.delete(tool.name);
1183
- this.version++;
1184
- }
1185
- /** Callable by name from the moment it is registered; sent to the model only once {@link surface}d. */
1186
- registerDeferred(tool) {
1187
- this.tools.set(tool.name, tool);
1188
- this.deferred.add(tool.name);
1189
- this.version++;
1190
- }
1191
- /**
1192
- * Hands over the schemas for these names, from the next turn onward.
1193
- *
1194
- * Returns the ones that were actually withheld, so a caller can say what it just made available and stay
1195
- * quiet about what was already there.
1196
- */
1197
- surface(names) {
1198
- const opened = names.filter((n) => this.deferred.has(n));
1199
- for (const n of opened) this.deferred.delete(n);
1200
- if (opened.length) this.version++;
1201
- return opened;
1202
- }
1203
- /** Everything still withheld — what a search tool searches. */
1204
- deferredTools() {
1205
- return [...this.deferred].map((n) => this.tools.get(n)).filter((t) => t !== void 0);
1206
- }
1207
- /**
1208
- * A withheld tool is still CALLABLE.
1209
- *
1210
- * A model that reads the catalogue and calls the name straight off is right, and refusing it to enforce a
1211
- * search step would spend a turn teaching it a rule that exists for our benefit, not its.
1212
- */
1213
- get(name) {
1214
- return this.tools.get(name);
1215
- }
1216
- list() {
1217
- return [...this.tools.values()];
1218
- }
1219
- /**
1220
- * Tool schemas to send to the LLM: zod parameters → JSON Schema (zod 4 native). Withheld ones are omitted.
1221
- *
1222
- * …and so is a tool that has withdrawn itself. `Tool.broken` was documented as being read "where tools are
1223
- * OFFERED, so a broken one stops being handed to fresh agents" — and only `find_tool` ever read it, which
1224
- * covers the deferred tools and not the ones already on the list.
1225
- *
1226
- * Measured on one run: `mcp__angular-cli__list_projects` answered its first caller with a reply that failed
1227
- * its own declared output schema and withdrew itself. It was then offered to seventeen more agents, who
1228
- * called it twenty-eight more times. Every one of those was answered instantly, without touching the
1229
- * server — and still cost a whole model turn to learn what the run already knew.
1230
- */
1231
- schemas() {
1232
- const withdrawn = this.list().reduce((n, t) => n + (t.broken === void 0 ? 0 : 1), 0);
1233
- if (this.cached?.version === this.version && this.cached.withdrawn === withdrawn) return this.cached.schemas;
1234
- const schemas = this.list().filter((t) => !this.deferred.has(t.name) && t.broken === void 0).map((t) => ({
1235
- name: t.name,
1236
- description: t.description,
1237
- // MCP tools already carry a JSON Schema; everyone else derives it from their zod parameters.
1238
- parameters: t.rawSchema ?? z.toJSONSchema(t.parameters, { target: "draft-7" })
1239
- }));
1240
- this.cached = { version: this.version, withdrawn, schemas };
1241
- return schemas;
1242
- }
1243
- };
1244
-
1245
- // src/core/types.ts
1246
- var DEADLINE_MESSAGE = "the model did not answer within its deadline";
1247
- var CHAIN_BUDGET_MESSAGE = "the chain's total budget ran out before this model was given a fair turn";
1248
-
1249
- // src/agent/structured.ts
1250
- function valueAt(args, path) {
1251
- let cur = args;
1252
- for (const key of path) {
1253
- if (typeof cur !== "object" || cur === null) return void 0;
1254
- cur = cur[key];
1255
- }
1256
- return cur;
1257
- }
1258
- function whatWasWrong(issues, args) {
1259
- return issues.map((i) => {
1260
- const where = i.path.length ? i.path.join(".") : void 0;
1261
- const got = valueAt(args, i.path);
1262
- const shown = got === void 0 ? "nothing" : JSON.stringify(got);
1263
- const head = where ? `${where}: ${i.message}` : i.message;
1264
- if (got === void 0 && /received\s+(undefined|null|nothing)/i.test(i.message)) {
1265
- const parent = i.path.length > 1 ? valueAt(args, i.path.slice(0, -1)) : args;
1266
- const sent = parent && typeof parent === "object" ? Object.keys(parent) : [];
1267
- return sent.length ? `${head} \u2014 you sent only ${sent.map((k) => `\`${k}\``).join(", ")}` : head;
1268
- }
1269
- return `${head} \u2014 got ${shown.length > 120 ? `${shown.slice(0, 120)}\u2026` : shown}`;
1270
- }).join("; ");
1271
- }
1272
- function buildSubmitTool(schema) {
1273
- let box;
1274
- const tool = {
1275
- name: "submit",
1276
- description: "When you are done, submit your result in structured form with this tool.",
1277
- permissionLevel: "safe",
1278
- parameters: schema,
1279
- run: async (rawArgs) => {
1280
- const parsed = schema.safeParse(rawArgs);
1281
- if (!parsed.success) {
1282
- return { content: `submit: invalid output: ${whatWasWrong(parsed.error.issues, rawArgs)}`, isError: true };
1283
- }
1284
- box = { value: parsed.data };
1285
- return { content: "received", isError: false };
1286
- }
1287
- };
1288
- return { tool, result: () => box };
1289
- }
1290
- function extractStructured(text, schema) {
1291
- const trimmed = text.trim();
1292
- if (!trimmed) return void 0;
1293
- const candidates = [trimmed];
1294
- const block = trimmed.match(/\{[\s\S]*\}/);
1295
- if (block) candidates.push(block[0]);
1296
- for (const c of candidates) {
1297
- try {
1298
- const parsed = schema.safeParse(JSON.parse(c));
1299
- if (parsed.success) return parsed.data;
1300
- } catch {
1301
- }
1302
- }
1303
- return void 0;
1304
- }
1305
- var TURN_LIMIT_RE = /maximum turn count exceeded/i;
1306
- async function runStructuredRole(opts, schema, maxAttempts = 2) {
1307
- const handle = buildSubmitTool(schema);
1308
- const registry = new ToolRegistry();
1309
- for (const t of opts.tools.list()) registry.register(t);
1310
- registry.register(handle.tool);
1311
- const chain = [opts.model, ...opts.fallbacks ?? []];
1312
- const total = opts.totalMs ? AbortSignal.timeout(opts.totalMs) : void 0;
1313
- const outOfTime = () => total?.aborted === true;
1314
- const signalFor = () => {
1315
- const parts = [opts.signal];
1316
- if (total) parts.push(total);
1317
- if (opts.perAttemptMs) parts.push(AbortSignal.timeout(opts.perAttemptMs));
1318
- return parts.length === 1 ? opts.signal : AbortSignal.any(parts);
1319
- };
1320
- let lastError;
1321
- for (let ci = 0; ci < chain.length; ci++) {
1322
- const model = chain[ci];
1323
- if (ci > 0) opts.onFallback?.(chain[ci - 1], model, "structured: previous model returned no valid result");
1324
- const messages = [...opts.messages];
1325
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
1326
- if (opts.signal.aborted) throw new Error("cancelled");
1327
- if (outOfTime()) break;
1328
- let lastText = "";
1329
- let errored;
1330
- for await (const ev of runRoleAgent({ ...opts, model, fallbacks: [], messages, tools: registry, signal: signalFor() })) {
1331
- if (ev.type === "error") {
1332
- errored = ev.message;
1333
- break;
1334
- }
1335
- if (ev.type === "abort") {
1336
- if (opts.signal.aborted) throw new Error("cancelled");
1337
- errored = total?.aborted ? CHAIN_BUDGET_MESSAGE : DEADLINE_MESSAGE;
1338
- break;
1339
- }
1340
- if (ev.type === "message.done") lastText = ev.message.content ?? lastText;
1341
- if (handle.result() !== void 0) break;
1342
- }
1343
- const r = handle.result();
1344
- if (r !== void 0) return r.value;
1345
- const salvaged = extractStructured(lastText, schema);
1346
- if (salvaged !== void 0) return salvaged;
1347
- if (errored !== void 0) {
1348
- if (TURN_LIMIT_RE.test(errored) && attempt < maxAttempts - 1) {
1349
- messages.push({ role: "assistant", content: lastText });
1350
- messages.push({ role: "user", content: "You have used your entire tool-call budget. Call `submit` NOW with the findings you already have. Do not read, grep or inspect anything else." });
1351
- continue;
1352
- }
1353
- lastError = errored;
1354
- break;
1355
- }
1356
- messages.push({ role: "assistant", content: lastText });
1357
- messages.push({
1358
- role: "user",
1359
- content: "You did not call the `submit` tool. Call `submit` now with your result as structured arguments \u2014 do not answer in prose."
1360
- });
1361
- }
1362
- if (opts.signal.aborted) throw new Error("cancelled");
1363
- if (outOfTime()) throw new Error("the model chain did not produce a result within its total budget");
1364
- opts.onStructuralFailure?.(model, "answered in prose instead of calling submit");
1365
- }
1366
- throw new Error(lastError ?? "structured role: submit was not called (whole model chain tried)");
1367
- }
1368
-
1369
1163
  export {
1370
1164
  fmtTokens,
1371
1165
  fmtDuration,
@@ -1381,7 +1175,6 @@ export {
1381
1175
  setTelemetry,
1382
1176
  telemetry,
1383
1177
  redactSecrets,
1384
- runToCompletion,
1385
- ToolRegistry,
1386
- runStructuredRole
1178
+ runRoleAgent,
1179
+ runToCompletion
1387
1180
  };
@@ -3,6 +3,9 @@ import {
3
3
  objectField,
4
4
  patchConfig
5
5
  } from "./chunk-H2FDGPVW.js";
6
+ import {
7
+ CLI_KINDS
8
+ } from "./chunk-G45RWL7S.js";
6
9
 
7
10
  // src/config/config.ts
8
11
  import { z } from "zod";
@@ -61,14 +64,33 @@ var fileSchema = z.object({
61
64
  council: z.object({ members: z.array(reviewerSchema) }).optional(),
62
65
  specKit: z.object({ version: z.string() }).optional(),
63
66
  modelSources: z.array(z.string()).optional(),
64
- // Logged-in profile directories, in spill order. A path each, never a credential.
67
+ /**
68
+ * Logged-in profile directories, in spill order. A path each, never a credential.
69
+ *
70
+ * Two things here were wrong in a way that could not be seen from this file, and both were found by
71
+ * connecting a real account.
72
+ *
73
+ * The kinds were spelled out — `["claude", "codex"]` — and every entry of a kind added since was
74
+ * rejected. `CLI_KINDS` is where they are declared, so this reads them rather than repeating them, and a
75
+ * fifth subscription cannot be half-added again.
76
+ *
77
+ * `configDir` was REQUIRED, and the signed-in default is precisely the entry that has none: `withAmbient`
78
+ * records it without a directory, deliberately, because Claude Code keeps that session in the Keychain
79
+ * and naming a directory switches it to file credentials. So the one entry written to stop a second
80
+ * account quietly retiring the first was itself unloadable.
81
+ *
82
+ * `.catch([])` bounds what a bad row can cost. The loader reads `parsed.success ? parsed.data : {}`, so
83
+ * a single rejected entry did not merely drop that account — it discarded the WHOLE global config.
84
+ * Measured on a live one: the file held 64 role chains and an API key, and with one z.ai entry present
85
+ * `loadConfig` returned zero roles and no key, silently, for every session since it was connected.
86
+ */
65
87
  accounts: z.array(z.object({
66
- kind: z.enum(["claude", "codex"]),
88
+ kind: z.enum(CLI_KINDS),
67
89
  name: z.string(),
68
- configDir: z.string(),
90
+ configDir: z.string().optional(),
69
91
  email: z.string().optional(),
70
92
  plan: z.string().optional()
71
- })).optional(),
93
+ })).catch([]).optional(),
72
94
  traceDir: z.string().optional(),
73
95
  // where /graph trace writes; empty = .horsecode/traces
74
96
  mainBranch: z.string().optional(),