@hizliemre/horse-code 0.1.2 → 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/dist/{app-LAJN3TWC.js → app-2GPGCDX6.js} +136 -329
- package/dist/{chunk-K2VERI5Q.js → chunk-23CLQ2KO.js} +365 -666
- package/dist/{chunk-KOWMHL23.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-RPVAIS3P.js → chunk-7JMWPTJ5.js} +1724 -306
- package/dist/{chunk-VPAWRRHL.js → chunk-AE36LLL2.js} +31 -21
- package/dist/{chunk-FFYBY2NA.js → chunk-KAGKX2YT.js} +2 -4
- package/dist/{chunk-4EWK7HWQ.js → chunk-KKWZBZYK.js} +10 -0
- package/dist/{chunk-DRZSUQ7Q.js → chunk-LLL7QWXB.js} +18 -7
- package/dist/{chunk-WYQBRCKY.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-EQX7BQYN.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 +379 -93
- 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-XP6WXNI7.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-ES5OHZOS.js → triage-FCYHD2AQ.js} +8 -9
- package/dist/{verify-3R7DGUSI.js → verify-6SC4I77M.js} +18 -19
- package/package.json +1 -1
- package/dist/chunk-3UYA3KUG.js +0 -212
- package/dist/chunk-B67BK5GQ.js +0 -34
- package/dist/chunk-O74BDQKS.js +0 -28
- package/dist/fix-CMARU6JR.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-3UYA3KUG.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;
|
|
@@ -850,13 +1091,13 @@ var FileSink = class {
|
|
|
850
1091
|
};
|
|
851
1092
|
|
|
852
1093
|
// src/cli-info.ts
|
|
853
|
-
import { readFileSync as
|
|
854
|
-
import { dirname as
|
|
1094
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1095
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
855
1096
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
856
1097
|
function hcodeVersion() {
|
|
857
1098
|
try {
|
|
858
|
-
const here =
|
|
859
|
-
const pkg = JSON.parse(
|
|
1099
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
1100
|
+
const pkg = JSON.parse(readFileSync3(join9(here, "..", "package.json"), "utf8"));
|
|
860
1101
|
return pkg.version ?? "unknown";
|
|
861
1102
|
} catch {
|
|
862
1103
|
return "unknown";
|
|
@@ -883,6 +1124,7 @@ function usage() {
|
|
|
883
1124
|
" hcode start the REPL",
|
|
884
1125
|
' hcode "<request>" run one request to completion, then report where the work landed',
|
|
885
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",
|
|
886
1128
|
"",
|
|
887
1129
|
"OPTIONS",
|
|
888
1130
|
" -b, --branch <name> branch the session's worktree from this ref (default: current branch)",
|
|
@@ -945,7 +1187,7 @@ function parseArgs(argv) {
|
|
|
945
1187
|
};
|
|
946
1188
|
}
|
|
947
1189
|
function whereItLanded(path) {
|
|
948
|
-
const base = sessionBase(
|
|
1190
|
+
const base = sessionBase(dirname5(path));
|
|
949
1191
|
if (base === void 0) {
|
|
950
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.";
|
|
951
1193
|
}
|
|
@@ -971,18 +1213,12 @@ function describeDelivery(d) {
|
|
|
971
1213
|
`Or inspect it first: \`git diff HEAD...${d.branch}\` \xB7 worktree: \`${d.worktree}\``
|
|
972
1214
|
].filter((l) => l !== void 0).join("\n");
|
|
973
1215
|
}
|
|
974
|
-
var DELIVERED_SHARE = 0.5;
|
|
975
1216
|
function describeOutcome(w) {
|
|
976
1217
|
if (w.status === "completed") return w.pr ? `PR: ${w.pr.url}` : "all tasks merged";
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
w.failed.length ? `${w.failed.length} failed` : "",
|
|
982
|
-
w.skipped.length ? `${w.skipped.length} blocked behind them` : ""
|
|
983
|
-
].filter(Boolean).join(", ");
|
|
984
|
-
const head = `${merged} of ${total} tasks merged`;
|
|
985
|
-
return merged / total < DELIVERED_SHARE ? `\u26A0\uFE0F ${head} \u2014 ${parts}. Most of the plan did not land; the feature is not built.` : `${head} \u2014 ${parts}.`;
|
|
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 });
|
|
986
1222
|
}
|
|
987
1223
|
function describeTraceFailures(failed) {
|
|
988
1224
|
if (!failed.length) return "";
|
|
@@ -1001,6 +1237,13 @@ ${shown.join("\n")}`;
|
|
|
1001
1237
|
return `\u26A0\uFE0F ${failed.length} failed:
|
|
1002
1238
|
${rows.join("\n")}`;
|
|
1003
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
|
+
}
|
|
1004
1247
|
function renderResult(res) {
|
|
1005
1248
|
if (res.kind === "chat") return stripThinking(res.response);
|
|
1006
1249
|
if (res.kind === "rejected") return `Not approved (stopped at the ${res.stage} stage).`;
|
|
@@ -1067,14 +1310,14 @@ async function main(argv) {
|
|
|
1067
1310
|
read: read2,
|
|
1068
1311
|
readFile: (p) => {
|
|
1069
1312
|
try {
|
|
1070
|
-
return
|
|
1313
|
+
return readFileSync4(p, "utf8");
|
|
1071
1314
|
} catch {
|
|
1072
1315
|
return void 0;
|
|
1073
1316
|
}
|
|
1074
1317
|
},
|
|
1075
1318
|
writeFile: (p, c) => {
|
|
1076
|
-
|
|
1077
|
-
|
|
1319
|
+
mkdirSync4(dirname5(p), { recursive: true });
|
|
1320
|
+
writeFileSync3(p, c);
|
|
1078
1321
|
},
|
|
1079
1322
|
home: process.env.HOME ?? "",
|
|
1080
1323
|
log: (s) => console.log(s)
|
|
@@ -1084,6 +1327,34 @@ async function main(argv) {
|
|
|
1084
1327
|
}
|
|
1085
1328
|
return;
|
|
1086
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
|
+
}
|
|
1087
1358
|
const cwd = process.cwd();
|
|
1088
1359
|
const config = loadConfig({
|
|
1089
1360
|
cwd,
|
|
@@ -1091,7 +1362,7 @@ async function main(argv) {
|
|
|
1091
1362
|
env: process.env,
|
|
1092
1363
|
readFile: (p) => {
|
|
1093
1364
|
try {
|
|
1094
|
-
return
|
|
1365
|
+
return readFileSync4(p, "utf8");
|
|
1095
1366
|
} catch {
|
|
1096
1367
|
return void 0;
|
|
1097
1368
|
}
|
|
@@ -1141,12 +1412,27 @@ async function main(argv) {
|
|
|
1141
1412
|
process.once("SIGTERM", flush);
|
|
1142
1413
|
}
|
|
1143
1414
|
const telemetryNote = sink ? `\u{1F4C8} Telemetry \u2192 \`${sink.path}\` (one JSON object per line)` : void 0;
|
|
1144
|
-
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 });
|
|
1145
1431
|
const provider = config.telemetry ? telemetryProvider(raw, telemetry()) : raw;
|
|
1146
1432
|
const skillRegistry = new SkillRegistry();
|
|
1147
1433
|
await registerBuiltinSkills(skillRegistry);
|
|
1148
1434
|
await skillRegistry.loadFromDir(externalSkillsDir(home));
|
|
1149
|
-
const skillsDir =
|
|
1435
|
+
const skillsDir = join10(cwd, ".horsecode", "skills");
|
|
1150
1436
|
if (existsSync3(skillsDir)) await skillRegistry.loadFromDir(skillsDir);
|
|
1151
1437
|
const worktreeHome = await mainWorktreeRoot(defaultGitRunner, cwd);
|
|
1152
1438
|
if (worktreeHome !== cwd) {
|
|
@@ -1168,6 +1454,7 @@ async function main(argv) {
|
|
|
1168
1454
|
manager,
|
|
1169
1455
|
prAdapter,
|
|
1170
1456
|
rules,
|
|
1457
|
+
accounts,
|
|
1171
1458
|
// Autonomous by default: a task that exhausts the escalation ladder is auto-retried (then abandoned) so
|
|
1172
1459
|
// an unattended run finishes on its own instead of blocking on an interactive per-failure human prompt.
|
|
1173
1460
|
askHuman: autonomousAskHuman(),
|
|
@@ -1178,25 +1465,12 @@ async function main(argv) {
|
|
|
1178
1465
|
const useTui = shouldUseTui(!!process.stdin.isTTY, !!process.stdout.isTTY, !!args.noTui);
|
|
1179
1466
|
if (!args.prompt) {
|
|
1180
1467
|
if (useTui) {
|
|
1181
|
-
const { runTuiRepl } = await import("./app-
|
|
1182
|
-
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");
|
|
1183
1470
|
const { loadSourceCache, saveSourceCache } = await import("./source-cache-XEK5WN7I.js");
|
|
1184
1471
|
const manualSources = config.modelSources.length > 0;
|
|
1185
1472
|
const sourcesRef = { current: manualSources ? config.modelSources : loadSourceCache(home, config.baseUrl) ?? [] };
|
|
1186
1473
|
const configuredModels = () => [...new Set(Object.values(config.roles).flatMap((r) => r.models ?? []))].filter((m) => m && m !== "default");
|
|
1187
|
-
const listModels = async () => {
|
|
1188
|
-
const catalog = await listOmniRouteModels({ baseUrl: config.baseUrl, apiKey: config.apiKey, sources: sourcesRef.current });
|
|
1189
|
-
const known = new Set(catalog);
|
|
1190
|
-
return [...catalog, ...configuredModels().filter((m) => !known.has(m))];
|
|
1191
|
-
};
|
|
1192
|
-
const refreshSources = async () => {
|
|
1193
|
-
const catalog = await fetchCatalog({ baseUrl: config.baseUrl, apiKey: config.apiKey });
|
|
1194
|
-
const found = await discoverSources({ catalog, probe: makeProbe({ baseUrl: config.baseUrl, apiKey: config.apiKey }) });
|
|
1195
|
-
sourcesRef.current = found;
|
|
1196
|
-
saveSourceCache(home, config.baseUrl, found);
|
|
1197
|
-
return found;
|
|
1198
|
-
};
|
|
1199
|
-
const sourcesInfo = () => ({ sources: sourcesRef.current, manual: manualSources, needsDiscovery: !manualSources && sourcesRef.current.length === 0 });
|
|
1200
1474
|
const probeModel = async (model) => {
|
|
1201
1475
|
try {
|
|
1202
1476
|
const res = await fetch(`${config.baseUrl.replace(/\/$/, "")}/api/v1/chat/completions`, {
|
|
@@ -1210,6 +1484,16 @@ async function main(argv) {
|
|
|
1210
1484
|
return false;
|
|
1211
1485
|
}
|
|
1212
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 });
|
|
1213
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));
|
|
1214
1498
|
const listSkills = () => skillRegistry.list().map((s) => ({
|
|
1215
1499
|
...s,
|
|
@@ -1232,7 +1516,7 @@ _Expected something like \`https://github.com/owner/repo\` or a link to the dire
|
|
|
1232
1516
|
_Discoverable by every agent. \`/roles adjust\` assigns it to the roles it fits; \`/skills update\` re-installs it from upstream._`;
|
|
1233
1517
|
};
|
|
1234
1518
|
const reloadProjectSkills = async () => {
|
|
1235
|
-
const dir =
|
|
1519
|
+
const dir = join10(cwd, ".horsecode", "skills");
|
|
1236
1520
|
if (existsSync3(dir)) await skillRegistry.loadFromDir(dir);
|
|
1237
1521
|
};
|
|
1238
1522
|
const updateSkills = async () => {
|
|
@@ -1322,6 +1606,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
|
|
|
1322
1606
|
await runTuiRepl({
|
|
1323
1607
|
buildDeps,
|
|
1324
1608
|
memStore,
|
|
1609
|
+
accountsNote,
|
|
1325
1610
|
listSkills,
|
|
1326
1611
|
updateSkills,
|
|
1327
1612
|
addSkill,
|
|
@@ -1341,7 +1626,8 @@ _Committed with the repo, so every clone starts with them. Agents read one with
|
|
|
1341
1626
|
...sink ? { telemetryPath: sink.path } : {},
|
|
1342
1627
|
refreshSources,
|
|
1343
1628
|
sourcesInfo,
|
|
1344
|
-
probeModel
|
|
1629
|
+
probeModel,
|
|
1630
|
+
routableModel
|
|
1345
1631
|
});
|
|
1346
1632
|
return;
|
|
1347
1633
|
}
|
|
@@ -1358,7 +1644,7 @@ _Committed with the repo, so every clone starts with them. Agents read one with
|
|
|
1358
1644
|
...args.revisionRounds !== void 0 && { revisionRounds: args.revisionRounds }
|
|
1359
1645
|
};
|
|
1360
1646
|
if (useTui) {
|
|
1361
|
-
const { runTui } = await import("./app-
|
|
1647
|
+
const { runTui } = await import("./app-2GPGCDX6.js");
|
|
1362
1648
|
const res = await runTui({ buildDeps, job });
|
|
1363
1649
|
console.log(renderResult(res));
|
|
1364
1650
|
return;
|
|
@@ -1405,12 +1691,12 @@ if (isMainModule()) {
|
|
|
1405
1691
|
});
|
|
1406
1692
|
}
|
|
1407
1693
|
export {
|
|
1408
|
-
DELIVERED_SHARE,
|
|
1409
1694
|
describeDelivery,
|
|
1410
1695
|
describeOutcome,
|
|
1411
1696
|
describeTraceFailures,
|
|
1412
1697
|
main,
|
|
1413
1698
|
parseArgs,
|
|
1699
|
+
poolWithConfigured,
|
|
1414
1700
|
renderResult,
|
|
1415
1701
|
shouldUseTui,
|
|
1416
1702
|
whereItLanded
|