@hizliemre/horse-code 0.1.1 → 0.2.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.
- package/README.md +1 -1
- package/dist/{app-UGFQKMLX.js → app-2GPGCDX6.js} +136 -329
- package/dist/{chunk-2DGO2BUB.js → chunk-23CLQ2KO.js} +366 -600
- package/dist/{chunk-JWAEW7AJ.js → chunk-5ZV42XGJ.js} +2 -2
- package/dist/{chunk-2SVAHH5N.js → chunk-63E73TGI.js} +2 -4
- package/dist/{chunk-DTWKSZXY.js → chunk-6OSEQOYY.js} +2 -2
- package/dist/{chunk-SSDLHWSF.js → chunk-6W4UH2BQ.js} +33 -1
- package/dist/{chunk-3XVZXTB6.js → chunk-7JMWPTJ5.js} +1818 -384
- package/dist/{chunk-YILDXPSI.js → chunk-AE36LLL2.js} +49 -25
- package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
- package/dist/{chunk-4EWK7HWQ.js → chunk-KKWZBZYK.js} +10 -0
- package/dist/{chunk-TOPZL5SU.js → chunk-LLL7QWXB.js} +41 -9
- package/dist/{chunk-7TBYMFMG.js → chunk-LNW557IO.js} +2 -2
- package/dist/{chunk-IW2KBAVZ.js → chunk-LPQU436C.js} +12 -1
- package/dist/chunk-M2RKCIGV.js +11 -0
- package/dist/{chunk-NNTIACT4.js → chunk-MRZVA5JB.js} +4 -4
- package/dist/{chunk-HBSC2HT2.js → chunk-UGESK765.js} +1 -1
- package/dist/{chunk-PGOYDOI4.js → chunk-XEGQT5EN.js} +3 -5
- package/dist/{chunk-FGVJFMK5.js → chunk-ZSQ24YDJ.js} +1 -1
- package/dist/cli.js +414 -100
- package/dist/{discover-5URG7C4J.js → discover-G2Z6XC3O.js} +9 -3
- package/dist/fix-JOIXQFVP.js +33 -0
- package/dist/git-QZTDZSJY.js +8 -0
- package/dist/{ongoing-OV5XROTU.js → ongoing-WHYXPW24.js} +4 -4
- package/dist/{project-graph-IOPCSZUA.js → project-graph-5HNPRFQG.js} +2 -3
- package/dist/{run-LQOZ5I7Z.js → run-P6ZYL5JL.js} +2 -3
- package/dist/{save-skills-NSLBU33X.js → save-skills-X7U3KCPU.js} +1 -1
- package/dist/{trace-ZMB7LT7W.js → trace-X6TU3AG6.js} +2 -3
- package/dist/{trace-adopt-C6TUWFJL.js → trace-adopt-URECQWJV.js} +2 -3
- package/dist/{trace-run-F23MFTY4.js → trace-run-7U4WJZ3V.js} +6 -7
- package/dist/{triage-2J3T5PVQ.js → triage-FCYHD2AQ.js} +8 -9
- package/dist/{verify-WQ3GHION.js → verify-6SC4I77M.js} +18 -19
- package/package.json +1 -1
- package/dist/chunk-B67BK5GQ.js +0 -34
- package/dist/chunk-F2IALVBU.js +0 -212
- package/dist/chunk-O74BDQKS.js +0 -28
- package/dist/fix-HBBOTUWM.js +0 -34
- package/dist/git-VTSZALSR.js +0 -6
package/dist/cli.js
CHANGED
|
@@ -2,25 +2,24 @@
|
|
|
2
2
|
import {
|
|
3
3
|
MAX_PARALLEL_TASKS,
|
|
4
4
|
MemoryStore,
|
|
5
|
-
OmniRouteProvider,
|
|
6
5
|
Timings,
|
|
7
6
|
autonomousAskHuman,
|
|
8
7
|
defaultCmdRunner,
|
|
8
|
+
describeTally,
|
|
9
9
|
detectPlatform,
|
|
10
10
|
makeApprove,
|
|
11
11
|
makeAskUser,
|
|
12
12
|
makePRAdapter,
|
|
13
13
|
nodeLineReader,
|
|
14
|
+
proposeSplit,
|
|
14
15
|
restoreTerminal,
|
|
15
16
|
runJob,
|
|
16
17
|
sttySane
|
|
17
|
-
} from "./chunk-
|
|
18
|
-
import
|
|
19
|
-
listOmniRouteModels
|
|
20
|
-
} from "./chunk-O74BDQKS.js";
|
|
21
|
-
import "./chunk-F2IALVBU.js";
|
|
18
|
+
} from "./chunk-23CLQ2KO.js";
|
|
19
|
+
import "./chunk-M2RKCIGV.js";
|
|
22
20
|
import {
|
|
23
21
|
CODE_TEAM,
|
|
22
|
+
CliProvider,
|
|
24
23
|
DEFAULT_COUNCIL,
|
|
25
24
|
DEFAULT_PROMPTS,
|
|
26
25
|
DEFAULT_ROLE_SKILLS,
|
|
@@ -28,16 +27,20 @@ import {
|
|
|
28
27
|
REQUIRED_ROLES,
|
|
29
28
|
RoleFitness,
|
|
30
29
|
RoleRegistry,
|
|
30
|
+
SHORT_CALL_MS,
|
|
31
31
|
SPEC_TEAM,
|
|
32
32
|
WorktreeManager,
|
|
33
33
|
buildCouncilRegistry,
|
|
34
34
|
buildTeamRegistry,
|
|
35
|
+
checkProfile,
|
|
36
|
+
cliCatalog,
|
|
35
37
|
mainWorktreeRoot,
|
|
38
|
+
runLogin,
|
|
36
39
|
toSlug
|
|
37
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-7JMWPTJ5.js";
|
|
38
41
|
import "./chunk-QF4MP6BS.js";
|
|
39
|
-
import "./chunk-
|
|
40
|
-
import "./chunk-
|
|
42
|
+
import "./chunk-UGESK765.js";
|
|
43
|
+
import "./chunk-ZSQ24YDJ.js";
|
|
41
44
|
import {
|
|
42
45
|
parseFrontmatter
|
|
43
46
|
} from "./chunk-BY4DP7IE.js";
|
|
@@ -45,7 +48,7 @@ import {
|
|
|
45
48
|
UNSET_MODEL,
|
|
46
49
|
loadConfig,
|
|
47
50
|
saveSkillSource
|
|
48
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-KKWZBZYK.js";
|
|
49
52
|
import "./chunk-H2FDGPVW.js";
|
|
50
53
|
import {
|
|
51
54
|
buildBrief,
|
|
@@ -53,50 +56,234 @@ import {
|
|
|
53
56
|
planFor,
|
|
54
57
|
runTraces,
|
|
55
58
|
traceableFiles
|
|
56
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-MRZVA5JB.js";
|
|
57
60
|
import {
|
|
58
61
|
defaultGitRunner
|
|
59
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-LPQU436C.js";
|
|
60
63
|
import {
|
|
61
64
|
InjectionLog,
|
|
62
65
|
memoryNote
|
|
63
|
-
} from "./chunk-
|
|
64
|
-
import "./chunk-
|
|
66
|
+
} from "./chunk-LLL7QWXB.js";
|
|
67
|
+
import "./chunk-63E73TGI.js";
|
|
65
68
|
import {
|
|
66
69
|
Telemetry,
|
|
70
|
+
ToolRegistry,
|
|
67
71
|
clearPerfMarks,
|
|
68
72
|
sampleMemory,
|
|
69
73
|
setTelemetry,
|
|
70
74
|
stripThinking,
|
|
71
75
|
telemetry,
|
|
72
76
|
writeHeapSnapshot
|
|
73
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-AE36LLL2.js";
|
|
74
78
|
import {
|
|
75
79
|
briefStatus
|
|
76
|
-
} from "./chunk-
|
|
80
|
+
} from "./chunk-6OSEQOYY.js";
|
|
77
81
|
import {
|
|
78
82
|
discoverTraceRoot,
|
|
79
83
|
setTraceRoot
|
|
80
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-KAGKX2YT.js";
|
|
81
85
|
import {
|
|
82
86
|
buildProjectGraph,
|
|
83
87
|
graphStatus,
|
|
84
88
|
graphifyPython
|
|
85
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-XEGQT5EN.js";
|
|
86
90
|
import {
|
|
87
91
|
sessionBase
|
|
88
|
-
} from "./chunk-
|
|
89
|
-
import "./chunk-B67BK5GQ.js";
|
|
92
|
+
} from "./chunk-6W4UH2BQ.js";
|
|
90
93
|
|
|
91
94
|
// src/cli.ts
|
|
92
95
|
import { execFileSync } from "child_process";
|
|
93
|
-
import { readFileSync as
|
|
94
|
-
import { join as
|
|
96
|
+
import { readFileSync as readFileSync4, existsSync as existsSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, realpathSync } from "fs";
|
|
97
|
+
import { join as join10, dirname as dirname5 } from "path";
|
|
95
98
|
import { pathToFileURL } from "url";
|
|
96
99
|
|
|
100
|
+
// src/agents/cli-accounts.ts
|
|
101
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
102
|
+
import { dirname, join } from "path";
|
|
103
|
+
var SPENT = 0.95;
|
|
104
|
+
function readingKey(kind, name) {
|
|
105
|
+
return `${kind}:${name}`;
|
|
106
|
+
}
|
|
107
|
+
function fileUsageStore(home) {
|
|
108
|
+
const path = join(home, ".horsecode", "usage.json");
|
|
109
|
+
return {
|
|
110
|
+
load() {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
113
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
116
|
+
const r = v;
|
|
117
|
+
if (typeof r?.spent === "number" && typeof r?.at === "number") out[k] = { spent: r.spent, at: r.at };
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
} catch {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
save(readings) {
|
|
125
|
+
try {
|
|
126
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
127
|
+
writeFileSync(path, JSON.stringify(readings, null, 2) + "\n");
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
var AccountPool = class {
|
|
134
|
+
accounts;
|
|
135
|
+
store;
|
|
136
|
+
readings;
|
|
137
|
+
constructor(accounts = [], store) {
|
|
138
|
+
this.accounts = accounts;
|
|
139
|
+
this.store = store;
|
|
140
|
+
this.readings = store?.load() ?? {};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The profile a call of this kind should run under, or undefined to run under the ambient login.
|
|
144
|
+
*
|
|
145
|
+
* Round-robin over the accounts that still have room, so two subscriptions drain together rather than one
|
|
146
|
+
* after the other. A spent one drops out of the rotation and rejoins the moment a reading says it can
|
|
147
|
+
* serve again.
|
|
148
|
+
*/
|
|
149
|
+
pick(kind) {
|
|
150
|
+
const mine = this.accounts.filter((a) => a.kind === kind);
|
|
151
|
+
if (!mine.length) return void 0;
|
|
152
|
+
const live = mine.filter((a) => (this.readings[readingKey(kind, a.name)]?.spent ?? 0) < SPENT);
|
|
153
|
+
const pool = live.length ? live : mine;
|
|
154
|
+
const n = this.turn.get(kind) ?? 0;
|
|
155
|
+
this.turn.set(kind, n + 1);
|
|
156
|
+
return pool[n % pool.length];
|
|
157
|
+
}
|
|
158
|
+
/** Whose turn it is, per kind. Turns are per SOURCE: Claude's rotation says nothing about Codex's. */
|
|
159
|
+
turn = /* @__PURE__ */ new Map();
|
|
160
|
+
/**
|
|
161
|
+
* File a quota reading against a profile.
|
|
162
|
+
*
|
|
163
|
+
* The windows are taken at their worst: a seven-day limit that is nearly gone stops the profile just as
|
|
164
|
+
* surely as a five-hour one, and reading only the shorter window would keep sending calls to a
|
|
165
|
+
* subscription whose longer window ran out days ago.
|
|
166
|
+
*/
|
|
167
|
+
record(kind, name, windows, now = Date.now()) {
|
|
168
|
+
const spent = Math.max(0, ...Object.values(windows));
|
|
169
|
+
this.readings = { ...this.readings, [readingKey(kind, name)]: { spent, at: now } };
|
|
170
|
+
this.store?.save(this.readings);
|
|
171
|
+
}
|
|
172
|
+
/** Every configured profile with its last reading, for a startup summary or a status line. */
|
|
173
|
+
usage() {
|
|
174
|
+
return this.accounts.map((a) => {
|
|
175
|
+
const r = this.readings[readingKey(a.kind, a.name)];
|
|
176
|
+
return { account: a, ...r ? { reading: r } : {} };
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** How many profiles are configured, in total or for one CLI. */
|
|
180
|
+
count(kind) {
|
|
181
|
+
return kind ? this.accounts.filter((a) => a.kind === kind).length : this.accounts.length;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
function ageOf(at, now) {
|
|
185
|
+
const mins = Math.max(0, Math.round((now - at) / 6e4));
|
|
186
|
+
if (mins < 60) return `${mins}m`;
|
|
187
|
+
const hours = Math.round(mins / 60);
|
|
188
|
+
return hours < 48 ? `${hours}h` : `${Math.round(hours / 24)}d`;
|
|
189
|
+
}
|
|
190
|
+
function accountsLine(usage2, notSignedIn = [], now = Date.now()) {
|
|
191
|
+
const parts = usage2.map(({ account, reading }) => {
|
|
192
|
+
const who = `${account.email ?? account.plan ?? account.name}${account.email && account.plan ? ` (${account.plan})` : ""}`;
|
|
193
|
+
const left = reading ? ` ${Math.round(reading.spent * 100)}% used ${ageOf(reading.at, now)} ago` : "";
|
|
194
|
+
return `${account.kind} ${who}${left}`;
|
|
195
|
+
});
|
|
196
|
+
for (const kind of notSignedIn) parts.push(`${kind} NOT signed in \u2014 every ${kind} call will fail`);
|
|
197
|
+
return parts.length ? parts.join(" \xB7 ") : void 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/agents/add-provider.ts
|
|
201
|
+
import { join as join2 } from "path";
|
|
202
|
+
function freshDir(kind, home, taken) {
|
|
203
|
+
const used = new Set(taken.map((a) => a.configDir));
|
|
204
|
+
for (let n = 2; ; n++) {
|
|
205
|
+
const dir = join2(home, ".horsecode", "profiles", `${kind}-${n}`);
|
|
206
|
+
if (!used.has(dir)) return dir;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function nameFor(kind, status, dir) {
|
|
210
|
+
return status.email ?? `${kind}-${dir.split("/").pop() ?? "profile"}`;
|
|
211
|
+
}
|
|
212
|
+
function withAmbient(kind, existing, ambient) {
|
|
213
|
+
if (existing.some((a) => a.kind === kind)) return [...existing];
|
|
214
|
+
if (!ambient.loggedIn) return [...existing];
|
|
215
|
+
return [
|
|
216
|
+
...existing,
|
|
217
|
+
{
|
|
218
|
+
kind,
|
|
219
|
+
name: ambient.email ?? `${kind}-default`,
|
|
220
|
+
...ambient.email ? { email: ambient.email } : {},
|
|
221
|
+
...ambient.plan ? { plan: ambient.plan } : {}
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
function accountsIn(config) {
|
|
226
|
+
const raw = config.accounts;
|
|
227
|
+
if (!Array.isArray(raw)) return [];
|
|
228
|
+
return raw.filter((a) => {
|
|
229
|
+
const e = a;
|
|
230
|
+
if (typeof a !== "object" || a === null) return false;
|
|
231
|
+
if (e.kind !== "claude" && e.kind !== "codex") return false;
|
|
232
|
+
return e.configDir === void 0 || typeof e.configDir === "string";
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function addProvider(kind, io) {
|
|
236
|
+
const config = io.readConfig();
|
|
237
|
+
const existing = accountsIn(config);
|
|
238
|
+
const accounts = withAmbient(kind, existing, io.check(kind));
|
|
239
|
+
if (accounts.length > existing.length) {
|
|
240
|
+
const a = accounts.at(-1);
|
|
241
|
+
io.log(`Recorded the ${kind} account you are already signed in as: ${a.email ?? a.name}${a.plan ? ` (${a.plan})` : ""}`);
|
|
242
|
+
io.log(`It stays first in line, so runs keep spending it before anything added now.`);
|
|
243
|
+
}
|
|
244
|
+
const dir = freshDir(kind, io.home, accounts);
|
|
245
|
+
io.log(`
|
|
246
|
+
Opening ${kind}'s own sign-in, for a NEW profile at ${dir}.`);
|
|
247
|
+
io.log(`Sign in with the account you want to add \u2014 not the one already connected.
|
|
248
|
+
`);
|
|
249
|
+
const r = io.login(kind, dir);
|
|
250
|
+
if (!r.ok) {
|
|
251
|
+
io.log(`
|
|
252
|
+
${kind} sign-in did not complete${r.error ? `: ${r.error}` : ""}. Nothing was changed.`);
|
|
253
|
+
io.log(`If a browser page opened, its address holds the reason \u2014 an \`error=\` there is the account being refused, not this command failing.`);
|
|
254
|
+
io.log(`Nothing needs cleaning up: ${dir} holds no session, and running this again reuses it.`);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
const status = io.check(kind, dir);
|
|
258
|
+
if (!status.loggedIn) {
|
|
259
|
+
io.log(`
|
|
260
|
+
${kind} still reports no session in ${dir}. Nothing was changed \u2014 run this again to retry.`);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
const already = status.email ? accounts.find((a) => a.kind === kind && a.email === status.email) : void 0;
|
|
264
|
+
if (already) {
|
|
265
|
+
io.log(`
|
|
266
|
+
That is the account already connected as "${already.name}". Nothing was changed.`);
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
const entry = {
|
|
270
|
+
kind,
|
|
271
|
+
name: nameFor(kind, status, dir),
|
|
272
|
+
configDir: dir,
|
|
273
|
+
...status.email ? { email: status.email } : {},
|
|
274
|
+
...status.plan ? { plan: status.plan } : {}
|
|
275
|
+
};
|
|
276
|
+
io.writeConfig({ ...config, accounts: [...accounts, entry] });
|
|
277
|
+
const mine = [...accounts, entry].filter((a) => a.kind === kind);
|
|
278
|
+
io.log(`
|
|
279
|
+
Connected ${entry.email ?? entry.name}${entry.plan ? ` (${entry.plan})` : ""}.`);
|
|
280
|
+
io.log(`${mine.length} ${kind} account${mine.length === 1 ? "" : "s"} now connected. Runs use them in order, spilling to the next when one is nearly out.`);
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
97
284
|
// src/skills/registry.ts
|
|
98
285
|
import { readdir, readFile } from "fs/promises";
|
|
99
|
-
import { join } from "path";
|
|
286
|
+
import { join as join3 } from "path";
|
|
100
287
|
var SkillRegistry = class {
|
|
101
288
|
skills = /* @__PURE__ */ new Map();
|
|
102
289
|
register(skill) {
|
|
@@ -119,7 +306,7 @@ var SkillRegistry = class {
|
|
|
119
306
|
if (!e.isDirectory()) continue;
|
|
120
307
|
let raw;
|
|
121
308
|
try {
|
|
122
|
-
raw = await readFile(
|
|
309
|
+
raw = await readFile(join3(dir, e.name, "SKILL.md"), "utf8");
|
|
123
310
|
} catch {
|
|
124
311
|
continue;
|
|
125
312
|
}
|
|
@@ -127,17 +314,17 @@ var SkillRegistry = class {
|
|
|
127
314
|
if (!name || !description) {
|
|
128
315
|
throw new Error(`skill ${e.name}: missing frontmatter (name/description)`);
|
|
129
316
|
}
|
|
130
|
-
this.register({ name, description, content: body, dir:
|
|
317
|
+
this.register({ name, description, content: body, dir: join3(dir, e.name) });
|
|
131
318
|
}
|
|
132
319
|
}
|
|
133
320
|
};
|
|
134
321
|
|
|
135
322
|
// src/skills/builtin.ts
|
|
136
323
|
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
137
|
-
import { dirname, join as
|
|
324
|
+
import { dirname as dirname2, join as join4, resolve } from "path";
|
|
138
325
|
import { fileURLToPath } from "url";
|
|
139
326
|
function candidates() {
|
|
140
|
-
const here =
|
|
327
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
141
328
|
return [
|
|
142
329
|
resolve(here, "..", "skills"),
|
|
143
330
|
// bundled: dist/… → <repo>/skills
|
|
@@ -155,14 +342,14 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
|
|
|
155
342
|
if (!e.isDirectory()) continue;
|
|
156
343
|
let raw;
|
|
157
344
|
try {
|
|
158
|
-
raw = await readFile2(
|
|
345
|
+
raw = await readFile2(join4(dir, e.name, "SKILL.md"), "utf8");
|
|
159
346
|
} catch {
|
|
160
347
|
continue;
|
|
161
348
|
}
|
|
162
349
|
const { parseFrontmatter: parseFrontmatter2 } = await import("./frontmatter-UNIPNLLO.js");
|
|
163
350
|
const { name, description, body } = parseFrontmatter2(raw);
|
|
164
351
|
if (!name || !description) continue;
|
|
165
|
-
found.push({ name, description, content: body, dir:
|
|
352
|
+
found.push({ name, description, content: body, dir: join4(dir, e.name) });
|
|
166
353
|
}
|
|
167
354
|
if (!found.length) continue;
|
|
168
355
|
for (const s of found) registry.register(s);
|
|
@@ -177,12 +364,12 @@ async function registerBuiltinSkills(registry, dirs = candidates()) {
|
|
|
177
364
|
// src/skills/external.ts
|
|
178
365
|
import { mkdir, rm, readFile as readFile3, writeFile, readdir as readdir3, cp } from "fs/promises";
|
|
179
366
|
import { existsSync } from "fs";
|
|
180
|
-
import { join as
|
|
367
|
+
import { join as join5 } from "path";
|
|
181
368
|
import { spawn } from "child_process";
|
|
182
369
|
import { tmpdir } from "os";
|
|
183
370
|
var PROVENANCE = ".horsecode-source.json";
|
|
184
371
|
function externalSkillsDir(home) {
|
|
185
|
-
return
|
|
372
|
+
return join5(home, ".horsecode", "skills");
|
|
186
373
|
}
|
|
187
374
|
function validName(name) {
|
|
188
375
|
return /^[A-Za-z0-9._-]+$/.test(name) && !name.includes("..");
|
|
@@ -226,7 +413,7 @@ async function resolveSha(src, deps) {
|
|
|
226
413
|
}
|
|
227
414
|
async function installedSource(home, name) {
|
|
228
415
|
try {
|
|
229
|
-
return JSON.parse(await readFile3(
|
|
416
|
+
return JSON.parse(await readFile3(join5(externalSkillsDir(home), name, PROVENANCE), "utf8"));
|
|
230
417
|
} catch {
|
|
231
418
|
return void 0;
|
|
232
419
|
}
|
|
@@ -236,27 +423,27 @@ async function installSkillSource(home, src, deps = {}) {
|
|
|
236
423
|
if (!validRepo(src.repo)) throw new Error(`skill source repo must be "owner/repo": ${src.repo}`);
|
|
237
424
|
if (src.path?.includes("..")) throw new Error(`skill source path may not contain "..": ${src.path}`);
|
|
238
425
|
const sha = await resolveSha(src, deps);
|
|
239
|
-
const dest =
|
|
426
|
+
const dest = join5(externalSkillsDir(home), src.name);
|
|
240
427
|
const current = await installedSource(home, src.name);
|
|
241
|
-
if (current?.sha === sha && existsSync(
|
|
428
|
+
if (current?.sha === sha && existsSync(join5(dest, "SKILL.md"))) {
|
|
242
429
|
return { name: src.name, sha, changed: false };
|
|
243
430
|
}
|
|
244
|
-
const staging =
|
|
431
|
+
const staging = join5(tmpdir(), `hc-skill-${src.name}-${sha.slice(0, 8)}`);
|
|
245
432
|
await rm(staging, { recursive: true, force: true });
|
|
246
433
|
await mkdir(staging, { recursive: true });
|
|
247
434
|
try {
|
|
248
435
|
const url = `https://codeload.github.com/${src.repo}/tar.gz/${sha}`;
|
|
249
436
|
const dl = await run("sh", ["-c", `curl -fsSL --max-time 120 ${JSON.stringify(url)} | tar xz -C ${JSON.stringify(staging)} --strip-components=1`]);
|
|
250
437
|
if (dl.code !== 0) throw new Error(`download failed for ${src.repo}@${sha.slice(0, 8)}: ${dl.err.trim().slice(0, 200)}`);
|
|
251
|
-
const from = src.path ?
|
|
252
|
-
if (!existsSync(
|
|
438
|
+
const from = src.path ? join5(staging, src.path) : staging;
|
|
439
|
+
if (!existsSync(join5(from, "SKILL.md"))) {
|
|
253
440
|
throw new Error(`${src.repo}${src.path ? `/${src.path}` : ""} has no SKILL.md`);
|
|
254
441
|
}
|
|
255
442
|
await rm(dest, { recursive: true, force: true });
|
|
256
443
|
await mkdir(dest, { recursive: true });
|
|
257
444
|
await cp(from, dest, { recursive: true });
|
|
258
445
|
const record = { ...src, sha, installedAt: (deps.now ?? Date.now)() };
|
|
259
|
-
await writeFile(
|
|
446
|
+
await writeFile(join5(dest, PROVENANCE), `${JSON.stringify(record, null, 2)}
|
|
260
447
|
`, "utf8");
|
|
261
448
|
return { name: src.name, sha, changed: true };
|
|
262
449
|
} finally {
|
|
@@ -546,8 +733,8 @@ var ProposalQueue = class {
|
|
|
546
733
|
};
|
|
547
734
|
|
|
548
735
|
// src/speckit/templates.ts
|
|
549
|
-
import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
550
|
-
import { join as
|
|
736
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
737
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
551
738
|
var TEMPLATE_FILES = {
|
|
552
739
|
spec: "spec-template.md",
|
|
553
740
|
plan: "plan-template.md",
|
|
@@ -570,10 +757,10 @@ async function loadSpecKit(opts) {
|
|
|
570
757
|
);
|
|
571
758
|
}
|
|
572
759
|
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
573
|
-
const cacheDir =
|
|
760
|
+
const cacheDir = join6(opts.home, ".horsecode", "spec-kit", opts.version, "templates");
|
|
574
761
|
const get = async (relPath) => {
|
|
575
|
-
const cachePath =
|
|
576
|
-
if (existsSync2(cachePath)) return
|
|
762
|
+
const cachePath = join6(cacheDir, relPath);
|
|
763
|
+
if (existsSync2(cachePath)) return readFileSync2(cachePath, "utf8");
|
|
577
764
|
const url = `${RAW_BASE}/${opts.version}/templates/${relPath}`;
|
|
578
765
|
const res = await fetchFn(url);
|
|
579
766
|
if (!res.ok) {
|
|
@@ -583,8 +770,8 @@ Check your network or set specKit.version to a valid tag.`
|
|
|
583
770
|
);
|
|
584
771
|
}
|
|
585
772
|
const text = await res.text();
|
|
586
|
-
|
|
587
|
-
|
|
773
|
+
mkdirSync2(dirname3(cachePath), { recursive: true });
|
|
774
|
+
writeFileSync2(cachePath, text, "utf8");
|
|
588
775
|
return text;
|
|
589
776
|
};
|
|
590
777
|
const templates = {};
|
|
@@ -600,7 +787,7 @@ Check your network or set specKit.version to a valid tag.`
|
|
|
600
787
|
|
|
601
788
|
// src/wiring.ts
|
|
602
789
|
import { homedir } from "os";
|
|
603
|
-
import { join as
|
|
790
|
+
import { join as join7 } from "path";
|
|
604
791
|
async function buildJobDeps(opts) {
|
|
605
792
|
const { config } = opts;
|
|
606
793
|
const roles = {};
|
|
@@ -612,6 +799,10 @@ async function buildJobDeps(opts) {
|
|
|
612
799
|
roles[name] = { ...cfg ?? { models: sessionChain }, ...skills.length ? { skills } : { skills: [] } };
|
|
613
800
|
}
|
|
614
801
|
const roleRegistry = new RoleRegistry(roles, DEFAULT_PROMPTS, opts.skillRegistry);
|
|
802
|
+
if (opts.accounts) {
|
|
803
|
+
const pool = opts.accounts;
|
|
804
|
+
roleRegistry.setSourceWeights(() => ({ claude: pool.count("claude"), codex: pool.count("codex") }));
|
|
805
|
+
}
|
|
615
806
|
const fillModels = (r) => ({
|
|
616
807
|
...r,
|
|
617
808
|
models: r.models.length > 0 ? r.models : config.roles[r.name]?.models ?? sessionChain
|
|
@@ -637,10 +828,27 @@ async function buildJobDeps(opts) {
|
|
|
637
828
|
const queue = new ProposalQueue();
|
|
638
829
|
let kitPromise;
|
|
639
830
|
const specKit = () => kitPromise ??= loadSpecKit({ version: config.specKit.version, home: opts.home, fetch: opts.fetch });
|
|
640
|
-
const fitness = new RoleFitness(
|
|
831
|
+
const fitness = new RoleFitness(join7(opts.home ?? homedir(), ".horsecode", "model-fitness.json"));
|
|
641
832
|
roleRegistry.setFitness(fitness);
|
|
642
833
|
return {
|
|
643
834
|
provider: opts.provider,
|
|
835
|
+
/**
|
|
836
|
+
* Implementers run as the CLI, because with a CLI provider nothing else can write.
|
|
837
|
+
*
|
|
838
|
+
* A CLI answers with TEXT. It never emits a tool call — it ran its tools itself, in its own process — so
|
|
839
|
+
* an implementer driven through this process's loop would receive prose, execute nothing, and report
|
|
840
|
+
* `no_changes` for every task on the board. This is not a choice between two working paths.
|
|
841
|
+
*
|
|
842
|
+
* It belongs HERE and not inside `runJob`, because it is a property of the provider that was chosen. Set
|
|
843
|
+
* in the pipeline it would override every caller that injected one: the job tests hand in a mock and
|
|
844
|
+
* would have spawned the real binary instead, which is exactly how it was first written and exactly how
|
|
845
|
+
* the tests caught it.
|
|
846
|
+
*
|
|
847
|
+
* `claude` is the fallback, not the decision — `cliFor` reads each role's own chain head, so a role on a
|
|
848
|
+
* `gpt-*` model runs Codex.
|
|
849
|
+
*/
|
|
850
|
+
...opts.delegateTo === null ? {} : { delegateTo: opts.delegateTo ?? "claude" },
|
|
851
|
+
...opts.accounts ? { accounts: opts.accounts } : {},
|
|
644
852
|
roleRegistry,
|
|
645
853
|
fitness,
|
|
646
854
|
skillRegistry: opts.skillRegistry,
|
|
@@ -665,7 +873,30 @@ async function buildJobDeps(opts) {
|
|
|
665
873
|
timings: new Timings(),
|
|
666
874
|
rounds: 3,
|
|
667
875
|
maxParallel: opts.config.maxParallel ?? MAX_PARALLEL_TASKS,
|
|
668
|
-
askHuman: opts.askHuman
|
|
876
|
+
askHuman: opts.askHuman,
|
|
877
|
+
/**
|
|
878
|
+
* Cutting a stuck card up, asked of the role that owns breakdowns in the first place.
|
|
879
|
+
*
|
|
880
|
+
* Read-only tools: this is a planning question about a card that already exists, and a splitter with a
|
|
881
|
+
* writer's toolset would be tempted to start doing the work it is describing.
|
|
882
|
+
*/
|
|
883
|
+
splitCard: async (card) => {
|
|
884
|
+
const resolved = roleRegistry.resolve("project-manager");
|
|
885
|
+
return proposeSplit({
|
|
886
|
+
provider: opts.provider,
|
|
887
|
+
...resolved,
|
|
888
|
+
tools: new ToolRegistry(),
|
|
889
|
+
// a planning question about a card that exists; it needs no tools
|
|
890
|
+
messages: [],
|
|
891
|
+
permission,
|
|
892
|
+
approve: opts.approve,
|
|
893
|
+
cwd: process.cwd(),
|
|
894
|
+
signal: opts.signal,
|
|
895
|
+
// It reads a card and answers; anything longer is exploring, which is not what was asked.
|
|
896
|
+
maxTurns: 3,
|
|
897
|
+
perAttemptMs: SHORT_CALL_MS
|
|
898
|
+
}, card);
|
|
899
|
+
}
|
|
669
900
|
};
|
|
670
901
|
}
|
|
671
902
|
|
|
@@ -717,6 +948,7 @@ async function* stream(inner, tel, req, signal) {
|
|
|
717
948
|
let promptTokens;
|
|
718
949
|
let completionTokens;
|
|
719
950
|
let cachedTokens;
|
|
951
|
+
let cacheWriteTokens;
|
|
720
952
|
let finish;
|
|
721
953
|
let failure;
|
|
722
954
|
let toolCalls = 0;
|
|
@@ -727,6 +959,7 @@ async function* stream(inner, tel, req, signal) {
|
|
|
727
959
|
promptTokens = ev.promptTokens;
|
|
728
960
|
completionTokens = ev.completionTokens;
|
|
729
961
|
cachedTokens = ev.cachedTokens;
|
|
962
|
+
cacheWriteTokens = ev.cacheWriteTokens;
|
|
730
963
|
} else if (ev.type === "tool-call") toolCalls++;
|
|
731
964
|
else if (ev.type === "text-delta") textChars += ev.text.length;
|
|
732
965
|
else if (ev.type === "done") finish = ev.finishReason;
|
|
@@ -745,6 +978,14 @@ async function* stream(inner, tel, req, signal) {
|
|
|
745
978
|
"gen_ai.usage.input_tokens": promptTokens,
|
|
746
979
|
"gen_ai.usage.output_tokens": completionTokens,
|
|
747
980
|
"gen_ai.usage.cached_tokens": cachedTokens,
|
|
981
|
+
/**
|
|
982
|
+
* What the cache COST, beside what it saved.
|
|
983
|
+
*
|
|
984
|
+
* A read is billed at a fraction and a write at a premium, so a run that reports only reads reads as
|
|
985
|
+
* pure profit — the first turn of every agent, which fills the cache, would cost nothing on paper.
|
|
986
|
+
* The true prompt spend is fresh + read + write, and this is the third.
|
|
987
|
+
*/
|
|
988
|
+
"gen_ai.usage.cache_write_tokens": cacheWriteTokens,
|
|
748
989
|
"gen_ai.response.finish_reason": finish,
|
|
749
990
|
"hc.duration_ms": Date.now() - started,
|
|
750
991
|
"hc.tools_requested": toolCalls,
|
|
@@ -779,10 +1020,10 @@ function redactRecord(record) {
|
|
|
779
1020
|
}
|
|
780
1021
|
|
|
781
1022
|
// src/obs/sink.ts
|
|
782
|
-
import { createWriteStream, mkdirSync as
|
|
783
|
-
import { join as
|
|
1023
|
+
import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
|
|
1024
|
+
import { join as join8 } from "path";
|
|
784
1025
|
function telemetryDir(home) {
|
|
785
|
-
return
|
|
1026
|
+
return join8(home, ".horsecode", "telemetry");
|
|
786
1027
|
}
|
|
787
1028
|
var MAX_QUEUE = 1e4;
|
|
788
1029
|
var FileSink = class {
|
|
@@ -791,9 +1032,9 @@ var FileSink = class {
|
|
|
791
1032
|
dropped = 0;
|
|
792
1033
|
path;
|
|
793
1034
|
constructor(home, runId) {
|
|
794
|
-
this.path =
|
|
1035
|
+
this.path = join8(telemetryDir(home), `${runId}.jsonl`);
|
|
795
1036
|
try {
|
|
796
|
-
|
|
1037
|
+
mkdirSync3(telemetryDir(home), { recursive: true });
|
|
797
1038
|
this.stream = createWriteStream(this.path, { flags: "a" });
|
|
798
1039
|
this.stream.on("error", () => {
|
|
799
1040
|
this.stream = void 0;
|
|
@@ -825,16 +1066,18 @@ var FileSink = class {
|
|
|
825
1066
|
return this.dropped;
|
|
826
1067
|
}
|
|
827
1068
|
/**
|
|
828
|
-
*
|
|
1069
|
+
* `flush()` is awaited as a run finishes, so whatever happens here decides whether the run can end.
|
|
829
1070
|
*
|
|
830
|
-
* `createWriteStream` does not throw when the path cannot be opened — it emits `error` on a later tick
|
|
1071
|
+
* `createWriteStream` does not throw when the path cannot be opened — it emits `error` on a later tick, so
|
|
831
1072
|
* the constructor's `catch` does not run, `stream` is set, and whether the error handler has cleared it by
|
|
832
|
-
* the time anything flushes is a race with the event loop.
|
|
833
|
-
*
|
|
1073
|
+
* the time anything flushes is a race with the event loop. Losing that race means `end(cb)` is called on a
|
|
1074
|
+
* stream that never opened.
|
|
834
1075
|
*
|
|
835
|
-
* The
|
|
836
|
-
*
|
|
837
|
-
*
|
|
1076
|
+
* The `error` listener is a second exit for that case. It was added believing the callback would otherwise
|
|
1077
|
+
* never fire and hang the run — an honest guess that has since been TESTED and did not hold: with the
|
|
1078
|
+
* listener removed, an EISDIR stream still resolves (test/obs/telemetry.test.ts). It stays because it
|
|
1079
|
+
* costs one line and closes a path nothing else covers, not because a hang was ever reproduced through it.
|
|
1080
|
+
* The 3h42m CI hang this was written during had a different cause, in the tests themselves.
|
|
838
1081
|
*/
|
|
839
1082
|
async flush() {
|
|
840
1083
|
const s = this.stream;
|
|
@@ -848,13 +1091,13 @@ var FileSink = class {
|
|
|
848
1091
|
};
|
|
849
1092
|
|
|
850
1093
|
// src/cli-info.ts
|
|
851
|
-
import { readFileSync as
|
|
852
|
-
import { dirname as
|
|
1094
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1095
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
853
1096
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
854
1097
|
function hcodeVersion() {
|
|
855
1098
|
try {
|
|
856
|
-
const here =
|
|
857
|
-
const pkg = JSON.parse(
|
|
1099
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
1100
|
+
const pkg = JSON.parse(readFileSync3(join9(here, "..", "package.json"), "utf8"));
|
|
858
1101
|
return pkg.version ?? "unknown";
|
|
859
1102
|
} catch {
|
|
860
1103
|
return "unknown";
|
|
@@ -881,6 +1124,7 @@ function usage() {
|
|
|
881
1124
|
" hcode start the REPL",
|
|
882
1125
|
' hcode "<request>" run one request to completion, then report where the work landed',
|
|
883
1126
|
" hcode init write .horsecode/ for this project and ask for the essentials",
|
|
1127
|
+
" hcode add-provider <cli> connect one more claude or codex subscription, through that CLI's own sign-in",
|
|
884
1128
|
"",
|
|
885
1129
|
"OPTIONS",
|
|
886
1130
|
" -b, --branch <name> branch the session's worktree from this ref (default: current branch)",
|
|
@@ -943,7 +1187,7 @@ function parseArgs(argv) {
|
|
|
943
1187
|
};
|
|
944
1188
|
}
|
|
945
1189
|
function whereItLanded(path) {
|
|
946
|
-
const base = sessionBase(
|
|
1190
|
+
const base = sessionBase(dirname5(path));
|
|
947
1191
|
if (base === void 0) {
|
|
948
1192
|
return "Written directly in your working tree: no branch, no worktree, nothing to merge. Review it and commit it when you are happy with it.";
|
|
949
1193
|
}
|
|
@@ -959,12 +1203,8 @@ function whereItLanded(path) {
|
|
|
959
1203
|
return `Written on ${branch ? `branch \`${branch}\`` : "its own branch"}, in the worktree at \`${base}\`. Review it there, then merge it in.`;
|
|
960
1204
|
}
|
|
961
1205
|
function describeDelivery(d) {
|
|
962
|
-
if (d.mergedInto) {
|
|
963
|
-
return `Merged into \`${d.mergedInto}\` \u2014 the files are in your working copy.`;
|
|
964
|
-
}
|
|
965
1206
|
return [
|
|
966
1207
|
`**The work is on branch \`${d.branch}\`** \u2014 not in your working copy yet.`,
|
|
967
|
-
d.notMerged ? `Not merged: ${d.notMerged}.` : "",
|
|
968
1208
|
"",
|
|
969
1209
|
"To bring it in:",
|
|
970
1210
|
"```",
|
|
@@ -973,6 +1213,37 @@ function describeDelivery(d) {
|
|
|
973
1213
|
`Or inspect it first: \`git diff HEAD...${d.branch}\` \xB7 worktree: \`${d.worktree}\``
|
|
974
1214
|
].filter((l) => l !== void 0).join("\n");
|
|
975
1215
|
}
|
|
1216
|
+
function describeOutcome(w) {
|
|
1217
|
+
if (w.status === "completed") return w.pr ? `PR: ${w.pr.url}` : "all tasks merged";
|
|
1218
|
+
const failed = w.failed.length;
|
|
1219
|
+
const blocked = w.skipped.length;
|
|
1220
|
+
const total = w.waves.flat().length || failed + blocked;
|
|
1221
|
+
return describeTally({ merged: Math.max(0, total - failed - blocked), failed, blocked, unfinished: 0 });
|
|
1222
|
+
}
|
|
1223
|
+
function describeTraceFailures(failed) {
|
|
1224
|
+
if (!failed.length) return "";
|
|
1225
|
+
const byError = /* @__PURE__ */ new Map();
|
|
1226
|
+
for (const f of failed) byError.set(f.error, [...byError.get(f.error) ?? [], f.file]);
|
|
1227
|
+
if (byError.size === 1) {
|
|
1228
|
+
const [error, files] = [...byError][0];
|
|
1229
|
+
const shown = files.slice(0, 3).map((f) => `- \`${f}\``);
|
|
1230
|
+
if (files.length > shown.length) shown.push(`- \u2026and ${files.length - shown.length} more`);
|
|
1231
|
+
return `\u26A0\uFE0F ${failed.length} failed, all for the same reason \u2014 ${error}
|
|
1232
|
+
${shown.join("\n")}`;
|
|
1233
|
+
}
|
|
1234
|
+
const rows = [...byError].sort((a, b) => b[1].length - a[1].length).slice(0, 5).map(([error, files]) => `- ${files.length}\xD7 ${error} (e.g. \`${files[0]}\`)`);
|
|
1235
|
+
const rest = byError.size - rows.length;
|
|
1236
|
+
if (rest > 0) rows.push(`- \u2026and ${rest} further cause(s)`);
|
|
1237
|
+
return `\u26A0\uFE0F ${failed.length} failed:
|
|
1238
|
+
${rows.join("\n")}`;
|
|
1239
|
+
}
|
|
1240
|
+
async function poolWithConfigured(catalog, configured, probe) {
|
|
1241
|
+
const known = new Set(catalog);
|
|
1242
|
+
const orphaned = configured.filter((m) => !known.has(m));
|
|
1243
|
+
if (!orphaned.length) return catalog;
|
|
1244
|
+
const alive = await Promise.all(orphaned.map(async (m) => await probe(m) ? m : void 0));
|
|
1245
|
+
return [...catalog, ...alive.filter((m) => m !== void 0)];
|
|
1246
|
+
}
|
|
976
1247
|
function renderResult(res) {
|
|
977
1248
|
if (res.kind === "chat") return stripThinking(res.response);
|
|
978
1249
|
if (res.kind === "rejected") return `Not approved (stopped at the ${res.stage} stage).`;
|
|
@@ -984,7 +1255,7 @@ function renderResult(res) {
|
|
|
984
1255
|
|
|
985
1256
|
_${whereItLanded(res.path)}_` : `The constitution phase finished without writing \`${res.path}\` \u2014 nothing was changed.`;
|
|
986
1257
|
}
|
|
987
|
-
const outcome = res.wave
|
|
1258
|
+
const outcome = describeOutcome(res.wave);
|
|
988
1259
|
const rev = res.revision ? `
|
|
989
1260
|
revision: ${res.revision.status}` : "";
|
|
990
1261
|
return `${stripThinking(res.report)}
|
|
@@ -1039,14 +1310,14 @@ async function main(argv) {
|
|
|
1039
1310
|
read: read2,
|
|
1040
1311
|
readFile: (p) => {
|
|
1041
1312
|
try {
|
|
1042
|
-
return
|
|
1313
|
+
return readFileSync4(p, "utf8");
|
|
1043
1314
|
} catch {
|
|
1044
1315
|
return void 0;
|
|
1045
1316
|
}
|
|
1046
1317
|
},
|
|
1047
1318
|
writeFile: (p, c) => {
|
|
1048
|
-
|
|
1049
|
-
|
|
1319
|
+
mkdirSync4(dirname5(p), { recursive: true });
|
|
1320
|
+
writeFileSync3(p, c);
|
|
1050
1321
|
},
|
|
1051
1322
|
home: process.env.HOME ?? "",
|
|
1052
1323
|
log: (s) => console.log(s)
|
|
@@ -1056,6 +1327,34 @@ async function main(argv) {
|
|
|
1056
1327
|
}
|
|
1057
1328
|
return;
|
|
1058
1329
|
}
|
|
1330
|
+
if (argv[0] === "add-provider") {
|
|
1331
|
+
const kind = argv[1];
|
|
1332
|
+
if (kind !== "claude" && kind !== "codex") {
|
|
1333
|
+
console.error(`add-provider needs a CLI to connect: \`hcode add-provider claude\` or \`hcode add-provider codex\`${kind ? ` (got "${kind}")` : ""}`);
|
|
1334
|
+
process.exitCode = 1;
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const path = `${process.env.HOME ?? ""}/.horsecode/config.json`;
|
|
1338
|
+
process.exitCode = addProvider(kind, {
|
|
1339
|
+
home: process.env.HOME ?? "",
|
|
1340
|
+
readConfig: () => {
|
|
1341
|
+
try {
|
|
1342
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
1343
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
1344
|
+
} catch {
|
|
1345
|
+
return {};
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
writeConfig: (c) => {
|
|
1349
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
1350
|
+
writeFileSync3(path, JSON.stringify(c, null, 2) + "\n");
|
|
1351
|
+
},
|
|
1352
|
+
login: runLogin,
|
|
1353
|
+
check: checkProfile,
|
|
1354
|
+
log: (line) => console.log(line)
|
|
1355
|
+
});
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1059
1358
|
const cwd = process.cwd();
|
|
1060
1359
|
const config = loadConfig({
|
|
1061
1360
|
cwd,
|
|
@@ -1063,7 +1362,7 @@ async function main(argv) {
|
|
|
1063
1362
|
env: process.env,
|
|
1064
1363
|
readFile: (p) => {
|
|
1065
1364
|
try {
|
|
1066
|
-
return
|
|
1365
|
+
return readFileSync4(p, "utf8");
|
|
1067
1366
|
} catch {
|
|
1068
1367
|
return void 0;
|
|
1069
1368
|
}
|
|
@@ -1113,12 +1412,27 @@ async function main(argv) {
|
|
|
1113
1412
|
process.once("SIGTERM", flush);
|
|
1114
1413
|
}
|
|
1115
1414
|
const telemetryNote = sink ? `\u{1F4C8} Telemetry \u2192 \`${sink.path}\` (one JSON object per line)` : void 0;
|
|
1116
|
-
const
|
|
1415
|
+
const ambientLogins = ["claude", "codex"].filter((k) => !config.accounts.some((a) => a.kind === k)).map((kind) => ({ kind, status: checkProfile(kind) }));
|
|
1416
|
+
const accounts = new AccountPool(
|
|
1417
|
+
[
|
|
1418
|
+
...config.accounts,
|
|
1419
|
+
...ambientLogins.filter((a) => a.status.loggedIn).map(({ kind, status }) => ({
|
|
1420
|
+
kind,
|
|
1421
|
+
name: status.email ?? `${kind}-default`,
|
|
1422
|
+
...status.email ? { email: status.email } : {},
|
|
1423
|
+
...status.plan ? { plan: status.plan } : {}
|
|
1424
|
+
}))
|
|
1425
|
+
],
|
|
1426
|
+
fileUsageStore(home)
|
|
1427
|
+
);
|
|
1428
|
+
const notSignedIn = ambientLogins.filter((a) => !a.status.loggedIn).map((a) => a.kind);
|
|
1429
|
+
const accountsNote = () => accountsLine(accounts.usage(), notSignedIn);
|
|
1430
|
+
const raw = new CliProvider({ readOnly: false, accounts });
|
|
1117
1431
|
const provider = config.telemetry ? telemetryProvider(raw, telemetry()) : raw;
|
|
1118
1432
|
const skillRegistry = new SkillRegistry();
|
|
1119
1433
|
await registerBuiltinSkills(skillRegistry);
|
|
1120
1434
|
await skillRegistry.loadFromDir(externalSkillsDir(home));
|
|
1121
|
-
const skillsDir =
|
|
1435
|
+
const skillsDir = join10(cwd, ".horsecode", "skills");
|
|
1122
1436
|
if (existsSync3(skillsDir)) await skillRegistry.loadFromDir(skillsDir);
|
|
1123
1437
|
const worktreeHome = await mainWorktreeRoot(defaultGitRunner, cwd);
|
|
1124
1438
|
if (worktreeHome !== cwd) {
|
|
@@ -1140,6 +1454,7 @@ async function main(argv) {
|
|
|
1140
1454
|
manager,
|
|
1141
1455
|
prAdapter,
|
|
1142
1456
|
rules,
|
|
1457
|
+
accounts,
|
|
1143
1458
|
// Autonomous by default: a task that exhausts the escalation ladder is auto-retried (then abandoned) so
|
|
1144
1459
|
// an unattended run finishes on its own instead of blocking on an interactive per-failure human prompt.
|
|
1145
1460
|
askHuman: autonomousAskHuman(),
|
|
@@ -1150,25 +1465,12 @@ async function main(argv) {
|
|
|
1150
1465
|
const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
|
|
1151
1466
|
if (!args.prompt) {
|
|
1152
1467
|
if (useTui) {
|
|
1153
|
-
const { runTuiRepl } = await import("./app-
|
|
1154
|
-
const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-
|
|
1468
|
+
const { runTuiRepl } = await import("./app-2GPGCDX6.js");
|
|
1469
|
+
const { fetchCatalog, makeProbe, discoverSources } = await import("./discover-G2Z6XC3O.js");
|
|
1155
1470
|
const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
|
|
1156
1471
|
const manualSources = config.modelSources.length > 0;
|
|
1157
1472
|
const sourcesRef = { current: manualSources ? config.modelSources : loadSourceCache(home, config.baseUrl) ?? [] };
|
|
1158
1473
|
const configuredModels = () => [...new Set(Object.values(config.roles).flatMap((r) => r.models ?? []))].filter((m) => m && m !== "default");
|
|
1159
|
-
const listModels = async () => {
|
|
1160
|
-
const catalog = await listOmniRouteModels({ baseUrl: config.baseUrl, apiKey: config.apiKey, sources: sourcesRef.current });
|
|
1161
|
-
const known = new Set(catalog);
|
|
1162
|
-
return [...catalog, ...configuredModels().filter((m) => !known.has(m))];
|
|
1163
|
-
};
|
|
1164
|
-
const refreshSources = async () => {
|
|
1165
|
-
const catalog = await fetchCatalog({ baseUrl: config.baseUrl, apiKey: config.apiKey });
|
|
1166
|
-
const found = await discoverSources({ catalog, probe: makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey }) });
|
|
1167
|
-
sourcesRef.current = found;
|
|
1168
|
-
saveSourceCache(home, config.baseUrl, found);
|
|
1169
|
-
return found;
|
|
1170
|
-
};
|
|
1171
|
-
const sourcesInfo = () => ({ sources: sourcesRef.current, manual: manualSources, needsDiscovery: !manualSources && sourcesRef.current.length === 0 });
|
|
1172
1474
|
const probeModel = async (model) => {
|
|
1173
1475
|
try {
|
|
1174
1476
|
const res = await fetch(`${config.baseUrl.replace(/\/$/, "")}/api/v1/chat/completions`, {
|
|
@@ -1182,6 +1484,16 @@ async function main(argv) {
|
|
|
1182
1484
|
return false;
|
|
1183
1485
|
}
|
|
1184
1486
|
};
|
|
1487
|
+
const routableModel = makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey });
|
|
1488
|
+
const listModels = async () => cliCatalog();
|
|
1489
|
+
const refreshSources = async () => {
|
|
1490
|
+
const catalog = await fetchCatalog({ baseUrl: config.baseUrl, apiKey: config.apiKey });
|
|
1491
|
+
const found = await discoverSources({ catalog, probe: makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey }) });
|
|
1492
|
+
sourcesRef.current = found;
|
|
1493
|
+
saveSourceCache(home, config.baseUrl, found);
|
|
1494
|
+
return found;
|
|
1495
|
+
};
|
|
1496
|
+
const sourcesInfo = () => ({ sources: sourcesRef.current, manual: manualSources, needsDiscovery: !manualSources && sourcesRef.current.length === 0 });
|
|
1185
1497
|
const effectiveRoleSkills = (skill) => Object.keys(DEFAULT_ROLE_SKILLS).concat(Object.keys(config.roles)).filter((r, i, a) => a.indexOf(r) === i).filter((r) => (config.roles[r]?.skills ?? DEFAULT_ROLE_SKILLS[r] ?? []).includes(skill));
|
|
1186
1498
|
const listSkills = () => skillRegistry.list().map((s) => ({
|
|
1187
1499
|
...s,
|
|
@@ -1204,7 +1516,7 @@ _Expected something like \`https://github.com/owner/repo\` or a link to the dire
|
|
|
1204
1516
|
_Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits; \`/skills update\` re-installs it from upstream._`;
|
|
1205
1517
|
};
|
|
1206
1518
|
const reloadProjectSkills = async () => {
|
|
1207
|
-
const dir =
|
|
1519
|
+
const dir = join10(cwd, ".horsecode", "skills");
|
|
1208
1520
|
if (existsSync3(dir)) await skillRegistry.loadFromDir(dir);
|
|
1209
1521
|
};
|
|
1210
1522
|
const updateSkills = async () => {
|
|
@@ -1284,19 +1596,17 @@ _Every agent can query it: \`graph_impact\` (blast radius), \`graph_trace\`, \`g
|
|
|
1284
1596
|
if (res.upToDate) bits.push(`${res.upToDate} already current`);
|
|
1285
1597
|
if (res.pruned.length) bits.push(`${res.pruned.length} removed for deleted files`);
|
|
1286
1598
|
if (res.wroteGitignore) bits.push("\n\n_Added .gitignore rules: traces are committed, the AST cache is not._");
|
|
1287
|
-
|
|
1288
|
-
|
|
1599
|
+
const failures = describeTraceFailures(res.failed);
|
|
1600
|
+
return `${bits.join(" \xB7 ")}${failures ? `
|
|
1289
1601
|
|
|
1290
|
-
|
|
1291
|
-
${res.failed.slice(0, 5).map((f) => `- \`${f.file}\` \u2014 ${f.error}`).join("\n")}`);
|
|
1292
|
-
}
|
|
1293
|
-
return `${bits.join(" \xB7 ")}
|
|
1602
|
+
${failures}` : ""}
|
|
1294
1603
|
|
|
1295
1604
|
_Committed with the repo, so every clone starts with them. Agents read one with \`graph_trace\`._`;
|
|
1296
1605
|
};
|
|
1297
1606
|
await runTuiRepl({
|
|
1298
1607
|
buildDeps,
|
|
1299
1608
|
memStore,
|
|
1609
|
+
accountsNote,
|
|
1300
1610
|
listSkills,
|
|
1301
1611
|
updateSkills,
|
|
1302
1612
|
addSkill,
|
|
@@ -1316,7 +1626,8 @@ _Committed with the repo, so every clone starts with them. Agents read one with
|
|
|
1316
1626
|
...sink ? { telemetryPath: sink.path } : {},
|
|
1317
1627
|
refreshSources,
|
|
1318
1628
|
sourcesInfo,
|
|
1319
|
-
probeModel
|
|
1629
|
+
probeModel,
|
|
1630
|
+
routableModel
|
|
1320
1631
|
});
|
|
1321
1632
|
return;
|
|
1322
1633
|
}
|
|
@@ -1333,7 +1644,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
|
|
|
1333
1644
|
...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
|
|
1334
1645
|
};
|
|
1335
1646
|
if (useTui) {
|
|
1336
|
-
const { runTui } = await import("./app-
|
|
1647
|
+
const { runTui } = await import("./app-2GPGCDX6.js");
|
|
1337
1648
|
const res = await runTui({ buildDeps, job });
|
|
1338
1649
|
console.log(renderResult(res));
|
|
1339
1650
|
return;
|
|
@@ -1381,8 +1692,11 @@ if (isMainModule()) {
|
|
|
1381
1692
|
}
|
|
1382
1693
|
export {
|
|
1383
1694
|
describeDelivery,
|
|
1695
|
+
describeOutcome,
|
|
1696
|
+
describeTraceFailures,
|
|
1384
1697
|
main,
|
|
1385
1698
|
parseArgs,
|
|
1699
|
+
poolWithConfigured,
|
|
1386
1700
|
renderResult,
|
|
1387
1701
|
shouldUseTui,
|
|
1388
1702
|
whereItLanded
|