heyspex 0.2.4 → 0.2.6
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/.claude-plugin/marketplace.json +1 -1
- package/bin/hs.mjs +281 -30
- package/dev-factory/lib/dispatch-guard.mjs +75 -0
- package/dev-factory/lib/runner-path.mjs +51 -0
- package/dev-factory/spexcrawl/spexcrawl-cli.mjs +734 -0
- package/dist/cli/hs-connect.mjs +20 -1
- package/dist/cli/hs-defer.mjs +3802 -3744
- package/dist/cli/hs-events.mjs +1296 -0
- package/dist/cli/hs-export-events.mjs +1252 -0
- package/dist/cli/hs-handoff.mjs +2576 -0
- package/dist/cli/hs-index-rebuild.mjs +209 -36
- package/dist/cli/hs-index-reconcile.mjs +212 -39
- package/dist/cli/hs-init.mjs +20 -1
- package/dist/cli/hs-install.mjs +20 -1
- package/dist/cli/hs-join.mjs +20 -1
- package/dist/cli/hs-next-launch.mjs +2698 -0
- package/dist/cli/hs-pin.mjs +1336 -0
- package/dist/cli/hs-prereq.mjs +1380 -0
- package/dist/cli/hs-revalidate.mjs +3257 -0
- package/dist/cli/hs-spawn.mjs +12377 -0
- package/dist/cli/hs-test.mjs +1046 -0
- package/dist/cli/hs-trash.mjs +362 -0
- package/dist/cli/hs-trust.mjs +197 -0
- package/dist/cli/hs-vault-pull.mjs +214 -41
- package/dist/cli/hs-vault-push.mjs +212 -39
- package/dist/cli-templates/harness-commands/claude/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/claude/hs-res.md +71 -63
- package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
- package/dist/cli-templates/harness-commands/cursor/hs-log.md +1 -1
- package/dist/cli-templates/harness-commands/cursor/hs-res.md +66 -58
- package/dist/cli-templates/harness-commands/cursor/hs-review.md +18 -18
- package/docs/copilot-plugin-install.md +2 -2
- package/package.json +9 -5
- package/plugins/heyspex/.claude-plugin/plugin.json +1 -1
- package/plugins/heyspex/.cursor-plugin/plugin.json +1 -1
- package/scripts/claude-hooks/wip-checkpoint.mjs +17 -2
- package/scripts/claude-statusline/statusline-command.mjs +287 -27
- package/scripts/hs-companion.mjs +8 -0
- package/scripts/install-claude-code-hooks.mjs +104 -3
- package/scripts/install-copilot-hooks.mjs +21 -1
- package/scripts/install-cursor-hooks.mjs +4 -3
- package/scripts/lib/capture-execution-guard.mjs +221 -0
- package/scripts/lib/customer-kit.mjs +48 -0
- package/scripts/lib/hs-companion-settings.mjs +25 -0
- package/scripts/record-grounding.mjs +38 -0
- package/vendor/heyspex-mcp/http-transport.mjs +1 -1
- package/vendor/heyspex-mcp/test-helpers.mjs +37 -23
- package/bin/postinstall.mjs +0 -88
|
@@ -0,0 +1,2698 @@
|
|
|
1
|
+
import { createRequire as __hsCreateRequire } from 'node:module';
|
|
2
|
+
const require = __hsCreateRequire(import.meta.url);
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// scripts/lib/heyspex-home.mjs
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
function heyspexHome(env = process.env) {
|
|
11
|
+
const override = env?.HEYSPEX_HOME;
|
|
12
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
13
|
+
return override.trim();
|
|
14
|
+
}
|
|
15
|
+
return homedir();
|
|
16
|
+
}
|
|
17
|
+
var init_heyspex_home = __esm({
|
|
18
|
+
"scripts/lib/heyspex-home.mjs"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// vendor/heyspex-mcp/credentials.mjs
|
|
24
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
25
|
+
import os, { homedir as homedir2 } from "node:os";
|
|
26
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
27
|
+
function credentialsFilePath(homeDir = heyspexHome()) {
|
|
28
|
+
return join2(homeDir, ".heyspex", "credentials.yaml");
|
|
29
|
+
}
|
|
30
|
+
function parseMcpToken(raw) {
|
|
31
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
32
|
+
const token = raw.match(MCP_TOKEN_LINE_RE)?.[1]?.trim();
|
|
33
|
+
return token ? token : null;
|
|
34
|
+
}
|
|
35
|
+
function isValidAccountId(id) {
|
|
36
|
+
return typeof id === "string" && ACCOUNT_ID_RE.test(id);
|
|
37
|
+
}
|
|
38
|
+
function topLevelScalar(raw, key) {
|
|
39
|
+
const re = new RegExp(`^${key}:[ \\t]*["']?([^"'\\r\\n#]+)`, "m");
|
|
40
|
+
const value = raw.match(re)?.[1]?.trim();
|
|
41
|
+
return value ? value : null;
|
|
42
|
+
}
|
|
43
|
+
function decodeScalar(text) {
|
|
44
|
+
const t = text.trim();
|
|
45
|
+
if (t === "" || t === "null" || t === "~") return null;
|
|
46
|
+
if (t.startsWith('"')) {
|
|
47
|
+
try {
|
|
48
|
+
const v = JSON.parse(t);
|
|
49
|
+
return typeof v === "string" && v ? v : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return t.replace(/^"|"$/g, "") || null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (t.startsWith("'")) return t.replace(/^'|'$/g, "") || null;
|
|
55
|
+
const bare = t.replace(/\s+#.*$/, "").trim();
|
|
56
|
+
return bare || null;
|
|
57
|
+
}
|
|
58
|
+
function emptyAccount() {
|
|
59
|
+
return { mcp_token: null, console_url: null, email: null, paired_at: null };
|
|
60
|
+
}
|
|
61
|
+
function findAccountsBlock(lines) {
|
|
62
|
+
const start = lines.findIndex((l) => /^accounts:[ \t]*(#.*)?$/.test(l));
|
|
63
|
+
if (start < 0) return null;
|
|
64
|
+
let end = start + 1;
|
|
65
|
+
while (end < lines.length && (lines[end].trim() === "" || /^[ \t]/.test(lines[end]))) end += 1;
|
|
66
|
+
while (end > start + 1 && lines[end - 1].trim() === "") end -= 1;
|
|
67
|
+
return { start, end };
|
|
68
|
+
}
|
|
69
|
+
function parseAccountsBlock(lines) {
|
|
70
|
+
const accounts = {};
|
|
71
|
+
const block = findAccountsBlock(lines);
|
|
72
|
+
if (!block) return accounts;
|
|
73
|
+
let current = null;
|
|
74
|
+
for (const line of lines.slice(block.start + 1, block.end)) {
|
|
75
|
+
const header = line.match(/^ {2}([^\s:#][^:]*):[ \t]*(#.*)?$/);
|
|
76
|
+
if (header) {
|
|
77
|
+
const id = decodeScalar(header[1]);
|
|
78
|
+
current = isValidAccountId(id) ? id : null;
|
|
79
|
+
if (current) accounts[current] = emptyAccount();
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const field = line.match(/^ {4}([a-z_]+):(.*)$/);
|
|
83
|
+
if (field && current && ACCOUNT_FIELDS.includes(field[1])) {
|
|
84
|
+
accounts[current][field[1]] = decodeScalar(field[2]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return accounts;
|
|
88
|
+
}
|
|
89
|
+
function parseCredentialsStore(raw) {
|
|
90
|
+
const text = typeof raw === "string" ? raw : "";
|
|
91
|
+
if (topLevelScalar(text, "schema_version") !== String(CREDENTIALS_SCHEMA_VERSION)) {
|
|
92
|
+
const token = parseMcpToken(text);
|
|
93
|
+
if (!token) return { schemaVersion: 1, currentAccountId: null, accounts: {} };
|
|
94
|
+
return {
|
|
95
|
+
schemaVersion: 1,
|
|
96
|
+
currentAccountId: LEGACY_ACCOUNT_ID,
|
|
97
|
+
accounts: {
|
|
98
|
+
[LEGACY_ACCOUNT_ID]: { ...emptyAccount(), mcp_token: token, console_url: topLevelScalar(text, "console_url") }
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const accounts = parseAccountsBlock(text.split(/\r?\n/));
|
|
103
|
+
const current = topLevelScalar(text, "current_account_id");
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 2,
|
|
106
|
+
currentAccountId: current && accounts[current] ? current : null,
|
|
107
|
+
accounts
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function resolveBearer({ envToken, credentialsText, accountId, legacyAccountId } = {}) {
|
|
111
|
+
const fromEnv = typeof envToken === "string" ? envToken.trim() : "";
|
|
112
|
+
if (fromEnv) return { token: fromEnv, source: "env" };
|
|
113
|
+
const store = parseCredentialsStore(credentialsText);
|
|
114
|
+
let entry;
|
|
115
|
+
if (typeof accountId === "string" && accountId.trim()) {
|
|
116
|
+
const wanted = accountId.trim();
|
|
117
|
+
entry = store.accounts[wanted];
|
|
118
|
+
if (!entry && store.schemaVersion === 1 && legacyAccountId && wanted === legacyAccountId) {
|
|
119
|
+
entry = store.accounts[LEGACY_ACCOUNT_ID];
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
entry = store.currentAccountId ? store.accounts[store.currentAccountId] : void 0;
|
|
123
|
+
}
|
|
124
|
+
if (entry?.mcp_token) return { token: entry.mcp_token, source: "credentials-file" };
|
|
125
|
+
return { token: null, source: null };
|
|
126
|
+
}
|
|
127
|
+
function readTextOrNull(read, path3) {
|
|
128
|
+
try {
|
|
129
|
+
const raw = read(path3);
|
|
130
|
+
return typeof raw === "string" ? raw : null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function readLegacyConfigAccountId(read, homeDir) {
|
|
136
|
+
const raw = readTextOrNull(read, join2(homeDir, ".heyspex", "config.yaml"));
|
|
137
|
+
const value = raw?.match(CONFIG_ACCOUNT_ID_RE)?.[1]?.trim() ?? "";
|
|
138
|
+
return value || null;
|
|
139
|
+
}
|
|
140
|
+
function readBearer({ env = process.env, homeDir = heyspexHome(), readFile, accountId } = {}) {
|
|
141
|
+
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
142
|
+
const credentialsText = readTextOrNull(read, credentialsFilePath(homeDir));
|
|
143
|
+
const named = typeof accountId === "string" && accountId.trim() ? accountId.trim() : null;
|
|
144
|
+
const legacyAccountId = named && parseCredentialsStore(credentialsText).schemaVersion === 1 ? readLegacyConfigAccountId(read, homeDir) : null;
|
|
145
|
+
return resolveBearer({ envToken: env?.[MCP_TOKEN_ENV], credentialsText, accountId: named, legacyAccountId });
|
|
146
|
+
}
|
|
147
|
+
function readCurrentAccountId({ homeDir = heyspexHome(), readFile } = {}) {
|
|
148
|
+
const read = readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
149
|
+
const store = parseCredentialsStore(readTextOrNull(read, credentialsFilePath(homeDir)));
|
|
150
|
+
if (store.schemaVersion === 2 && store.currentAccountId && store.currentAccountId !== LEGACY_ACCOUNT_ID) {
|
|
151
|
+
return store.currentAccountId;
|
|
152
|
+
}
|
|
153
|
+
if (store.schemaVersion === 2 && store.currentAccountId === null) return null;
|
|
154
|
+
return readLegacyConfigAccountId(read, homeDir);
|
|
155
|
+
}
|
|
156
|
+
var MCP_TOKEN_ENV, MCP_TOKEN_LINE_RE, CREDENTIALS_SCHEMA_VERSION, LEGACY_ACCOUNT_ID, ACCOUNT_FIELDS, ACCOUNT_ID_RE, CONFIG_ACCOUNT_ID_RE;
|
|
157
|
+
var init_credentials = __esm({
|
|
158
|
+
"vendor/heyspex-mcp/credentials.mjs"() {
|
|
159
|
+
"use strict";
|
|
160
|
+
init_heyspex_home();
|
|
161
|
+
MCP_TOKEN_ENV = "HEYSPEX_MCP_TOKEN";
|
|
162
|
+
MCP_TOKEN_LINE_RE = /^mcp_token:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
163
|
+
CREDENTIALS_SCHEMA_VERSION = 2;
|
|
164
|
+
LEGACY_ACCOUNT_ID = "legacy";
|
|
165
|
+
ACCOUNT_FIELDS = ["mcp_token", "console_url", "email", "paired_at"];
|
|
166
|
+
ACCOUNT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
|
|
167
|
+
CONFIG_ACCOUNT_ID_RE = /^account_id:\s*["']?([^"'\r\n#]+)/m;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// scripts/lib/dev-mode.mjs
|
|
172
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
173
|
+
import { homedir as homedir8 } from "node:os";
|
|
174
|
+
import { join as join8 } from "node:path";
|
|
175
|
+
function devModeFilePath(homeDir = heyspexHome()) {
|
|
176
|
+
return join8(homeDir, ".heyspex", DEV_MODE_FILENAME);
|
|
177
|
+
}
|
|
178
|
+
function normalizeMode(raw) {
|
|
179
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
180
|
+
if (v === "prod" || v === "production") return { mode: "prod", reliance: "cloud" };
|
|
181
|
+
if (v === "dev") return { mode: "dev", reliance: null };
|
|
182
|
+
if (v === "local") return { mode: "dev", reliance: "local" };
|
|
183
|
+
if (v === "cloud") return { mode: "dev", reliance: "cloud" };
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function normalizeReliance(raw) {
|
|
187
|
+
const v = String(raw ?? "").trim().toLowerCase();
|
|
188
|
+
return v === "local" || v === "cloud" ? v : null;
|
|
189
|
+
}
|
|
190
|
+
function deriveSemantics(mode, reliance) {
|
|
191
|
+
const effectiveReliance = mode === "prod" ? "cloud" : reliance ?? "local";
|
|
192
|
+
const local = mode === "dev" && effectiveReliance === "local";
|
|
193
|
+
return {
|
|
194
|
+
reliance: effectiveReliance,
|
|
195
|
+
consoleUrl: local ? LOCAL_CONSOLE_URL : CLOUD_CONSOLE_URL,
|
|
196
|
+
readTier: local ? "disk" : "neon",
|
|
197
|
+
vaultWriteMode: local ? "local" : "cloud"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function readLocalDevModeRecord({ homeDir = heyspexHome(), readFile = defaultReadFile } = {}) {
|
|
201
|
+
let raw;
|
|
202
|
+
try {
|
|
203
|
+
raw = readFile(devModeFilePath(homeDir));
|
|
204
|
+
} catch {
|
|
205
|
+
return { record: null, parseError: null };
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(raw);
|
|
209
|
+
const mode = parsed?.mode === "prod" || parsed?.mode === "dev" ? parsed.mode : null;
|
|
210
|
+
const reliance = normalizeReliance(parsed?.reliance);
|
|
211
|
+
if (!mode || mode === "dev" && !reliance) {
|
|
212
|
+
return { record: null, parseError: `invalid dev-mode record: ${JSON.stringify(parsed)}` };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
record: {
|
|
216
|
+
mode,
|
|
217
|
+
reliance: mode === "prod" ? "cloud" : reliance,
|
|
218
|
+
console_url: typeof parsed.console_url === "string" ? parsed.console_url : null,
|
|
219
|
+
updated_at: typeof parsed.updated_at === "string" ? parsed.updated_at : null,
|
|
220
|
+
source: typeof parsed.source === "string" ? parsed.source : null
|
|
221
|
+
},
|
|
222
|
+
parseError: null
|
|
223
|
+
};
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return { record: null, parseError: `unparseable dev-mode.json: ${String(err?.message ?? err)}` };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function overrideCloudCheck(source, cloudKnown, staleAgainstCloud) {
|
|
229
|
+
if (source !== "local-override") return "not-applicable";
|
|
230
|
+
if (!cloudKnown) return "unverified";
|
|
231
|
+
return staleAgainstCloud ? "stale" : "in-sync";
|
|
232
|
+
}
|
|
233
|
+
function resolveDevMode({
|
|
234
|
+
env = process.env,
|
|
235
|
+
homeDir = heyspexHome(),
|
|
236
|
+
readFile = defaultReadFile,
|
|
237
|
+
cloudSetting = void 0
|
|
238
|
+
} = {}) {
|
|
239
|
+
const local = readLocalDevModeRecord({ homeDir, readFile });
|
|
240
|
+
const cloudKnown = cloudSetting !== void 0;
|
|
241
|
+
const cloudValue = cloudKnown && cloudSetting ? {
|
|
242
|
+
mode: cloudSetting.mode === "dev" ? "dev" : "prod",
|
|
243
|
+
reliance: normalizeReliance(cloudSetting.reliance) ?? "cloud"
|
|
244
|
+
} : null;
|
|
245
|
+
let mode;
|
|
246
|
+
let reliance;
|
|
247
|
+
let source;
|
|
248
|
+
let relianceSource;
|
|
249
|
+
const envParsed = normalizeMode(env.HEYSPEX_DEV_MODE);
|
|
250
|
+
if (envParsed) {
|
|
251
|
+
mode = envParsed.mode;
|
|
252
|
+
const envReliance = envParsed.reliance ?? normalizeReliance(env.HEYSPEX_DEV_RELIANCE);
|
|
253
|
+
reliance = envReliance ?? "local";
|
|
254
|
+
source = "env";
|
|
255
|
+
relianceSource = envReliance ? "env" : "assumed-local";
|
|
256
|
+
} else if (local.record) {
|
|
257
|
+
mode = local.record.mode;
|
|
258
|
+
reliance = local.record.reliance;
|
|
259
|
+
source = "local-override";
|
|
260
|
+
} else if (cloudValue) {
|
|
261
|
+
mode = cloudValue.mode;
|
|
262
|
+
reliance = cloudValue.reliance;
|
|
263
|
+
source = "cloud-setting";
|
|
264
|
+
} else {
|
|
265
|
+
mode = "prod";
|
|
266
|
+
reliance = "cloud";
|
|
267
|
+
source = "default";
|
|
268
|
+
}
|
|
269
|
+
const semantics = deriveSemantics(mode, reliance);
|
|
270
|
+
const consoleUrl = source === "local-override" && local.record?.console_url ? local.record.console_url.replace(/\/+$/, "") : semantics.consoleUrl;
|
|
271
|
+
const staleAgainstCloud = source === "local-override" && cloudValue !== null && (cloudValue.mode !== mode || mode === "dev" && cloudValue.reliance !== semantics.reliance);
|
|
272
|
+
const cloudCheck = overrideCloudCheck(source, cloudKnown, staleAgainstCloud);
|
|
273
|
+
return {
|
|
274
|
+
mode,
|
|
275
|
+
reliance: semantics.reliance,
|
|
276
|
+
consoleUrl,
|
|
277
|
+
readTier: semantics.readTier,
|
|
278
|
+
vaultWriteMode: semantics.vaultWriteMode,
|
|
279
|
+
sources: {
|
|
280
|
+
mode: source,
|
|
281
|
+
reliance: relianceSource ?? source,
|
|
282
|
+
consoleUrl: source === "local-override" && local.record?.console_url ? "local-override" : "derived",
|
|
283
|
+
readTier: "derived",
|
|
284
|
+
vaultWriteMode: "derived"
|
|
285
|
+
},
|
|
286
|
+
localOverride: {
|
|
287
|
+
present: local.record !== null,
|
|
288
|
+
record: local.record,
|
|
289
|
+
parseError: local.parseError,
|
|
290
|
+
staleAgainstCloud,
|
|
291
|
+
cloudCheck
|
|
292
|
+
},
|
|
293
|
+
cloudSetting: { known: cloudKnown, value: cloudValue }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
var CLOUD_CONSOLE_URL, LOCAL_CONSOLE_URL, DEV_MODE_FILENAME, defaultReadFile;
|
|
297
|
+
var init_dev_mode = __esm({
|
|
298
|
+
"scripts/lib/dev-mode.mjs"() {
|
|
299
|
+
"use strict";
|
|
300
|
+
init_heyspex_home();
|
|
301
|
+
init_credentials();
|
|
302
|
+
CLOUD_CONSOLE_URL = "https://app.heyspex.com";
|
|
303
|
+
LOCAL_CONSOLE_URL = "http://localhost:3000";
|
|
304
|
+
DEV_MODE_FILENAME = "dev-mode.json";
|
|
305
|
+
defaultReadFile = (p) => readFileSync8(p, "utf8");
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// spex-console/bin/hs-next-launch.ts
|
|
310
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
311
|
+
import process3 from "node:process";
|
|
312
|
+
|
|
313
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
314
|
+
import path2 from "node:path";
|
|
315
|
+
|
|
316
|
+
// scripts/lib/hs-command.mjs
|
|
317
|
+
import { existsSync } from "node:fs";
|
|
318
|
+
import { delimiter, dirname, join } from "node:path";
|
|
319
|
+
import { fileURLToPath } from "node:url";
|
|
320
|
+
var HS_BIN_NAME = "hs";
|
|
321
|
+
var HS_DISPATCHER_PATH = (() => {
|
|
322
|
+
try {
|
|
323
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
324
|
+
return join(dirname(dirname(here)), "bin", "hs.mjs");
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
})();
|
|
329
|
+
function shellQuote(p) {
|
|
330
|
+
return /[\\\s]/.test(p) ? `"${p}"` : p;
|
|
331
|
+
}
|
|
332
|
+
var HS_DISPATCHER_PREFIX = HS_DISPATCHER_PATH ? `node ${shellQuote(HS_DISPATCHER_PATH)}` : HS_BIN_NAME;
|
|
333
|
+
function candidateNames(name, env) {
|
|
334
|
+
if (process.platform !== "win32") return [name];
|
|
335
|
+
const pathext = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean);
|
|
336
|
+
const extensions = [.../* @__PURE__ */ new Set([...pathext, ".ps1"])];
|
|
337
|
+
return [name, ...extensions.map((ext) => name + ext)];
|
|
338
|
+
}
|
|
339
|
+
function findOnPath(name, { env = process.env, exists = existsSync } = {}) {
|
|
340
|
+
try {
|
|
341
|
+
const raw = env.PATH ?? env.Path ?? env.path ?? "";
|
|
342
|
+
for (const dir of String(raw).split(delimiter)) {
|
|
343
|
+
const trimmed = dir.trim().replace(/^"(.*)"$/, "$1");
|
|
344
|
+
if (!trimmed) continue;
|
|
345
|
+
for (const candidate of candidateNames(name, env)) {
|
|
346
|
+
const full = join(trimmed, candidate);
|
|
347
|
+
try {
|
|
348
|
+
if (exists(full)) return full;
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
var cached = null;
|
|
358
|
+
var HS_ON_PATH_OVERRIDE_ENV = "HEYSPEX_HS_ON_PATH";
|
|
359
|
+
function onPathOverride(env) {
|
|
360
|
+
const raw = String(env?.[HS_ON_PATH_OVERRIDE_ENV] ?? "");
|
|
361
|
+
if (raw === "1") return true;
|
|
362
|
+
if (raw === "0") return false;
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
function describeHsCommand({ env, exists, useCache = true } = {}) {
|
|
366
|
+
const injected = env !== void 0 || exists !== void 0;
|
|
367
|
+
const effectiveEnv = env ?? process.env;
|
|
368
|
+
const override = onPathOverride(effectiveEnv);
|
|
369
|
+
if (override !== void 0) {
|
|
370
|
+
return {
|
|
371
|
+
onPath: override,
|
|
372
|
+
prefix: override ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
|
|
373
|
+
resolvedPath: null,
|
|
374
|
+
dispatcherPath: HS_DISPATCHER_PATH
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (useCache && !injected && cached) return cached;
|
|
378
|
+
const resolvedPath = findOnPath(HS_BIN_NAME, {
|
|
379
|
+
env: env ?? process.env,
|
|
380
|
+
exists: exists ?? existsSync
|
|
381
|
+
});
|
|
382
|
+
const answer = {
|
|
383
|
+
onPath: resolvedPath !== null,
|
|
384
|
+
prefix: resolvedPath !== null ? HS_BIN_NAME : HS_DISPATCHER_PREFIX,
|
|
385
|
+
resolvedPath,
|
|
386
|
+
dispatcherPath: HS_DISPATCHER_PATH
|
|
387
|
+
};
|
|
388
|
+
if (useCache && !injected) cached = answer;
|
|
389
|
+
return answer;
|
|
390
|
+
}
|
|
391
|
+
function hsCommand(args = "", options = {}) {
|
|
392
|
+
let prefix = HS_DISPATCHER_PREFIX;
|
|
393
|
+
try {
|
|
394
|
+
prefix = describeHsCommand(options).prefix;
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
const tail = String(args ?? "").trim();
|
|
398
|
+
return tail ? `${prefix} ${tail}` : prefix;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
402
|
+
init_heyspex_home();
|
|
403
|
+
init_credentials();
|
|
404
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
405
|
+
import { homedir as homedir6 } from "node:os";
|
|
406
|
+
import { join as join6 } from "node:path";
|
|
407
|
+
|
|
408
|
+
// vendor/heyspex-mcp/session-owner.mjs
|
|
409
|
+
init_heyspex_home();
|
|
410
|
+
init_credentials();
|
|
411
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
412
|
+
import { homedir as homedir3 } from "node:os";
|
|
413
|
+
import { join as join3 } from "node:path";
|
|
414
|
+
function resolveSessionOwnerUncached({ env = process.env, homeDir = heyspexHome(), readFile } = {}) {
|
|
415
|
+
const fromEnv = typeof env?.HS_ACCOUNT_ID === "string" ? env.HS_ACCOUNT_ID.trim() : "";
|
|
416
|
+
if (fromEnv) return fromEnv;
|
|
417
|
+
return readCurrentAccountId({ homeDir, readFile });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// vendor/heyspex-mcp/vault-less-endpoints.mjs
|
|
421
|
+
var VAULT_LESS_ENDPOINTS = Object.freeze([
|
|
422
|
+
"* /api/mcp/session",
|
|
423
|
+
"* /api/mcp/pairing-codes",
|
|
424
|
+
"POST /api/vaults",
|
|
425
|
+
"GET /api/vaults",
|
|
426
|
+
"POST /api/workspaces",
|
|
427
|
+
"GET /api/workspaces",
|
|
428
|
+
"* /api/admin/dev-mode"
|
|
429
|
+
]);
|
|
430
|
+
function pathnameOf(path3) {
|
|
431
|
+
const raw = String(path3 ?? "").split("?")[0].split("#")[0];
|
|
432
|
+
const noTrailing = raw.length > 1 ? raw.replace(/\/+$/, "") : raw;
|
|
433
|
+
return noTrailing || "/";
|
|
434
|
+
}
|
|
435
|
+
function isVaultLessEndpoint(method, path3) {
|
|
436
|
+
const m = String(method || "GET").toUpperCase();
|
|
437
|
+
const pathname = pathnameOf(path3);
|
|
438
|
+
return VAULT_LESS_ENDPOINTS.some((row) => {
|
|
439
|
+
const [rowMethod, rowPath] = row.split(" ");
|
|
440
|
+
return rowPath === pathname && (rowMethod === "*" || rowMethod === m);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// vendor/heyspex-mcp/write-outbox.mjs
|
|
445
|
+
import {
|
|
446
|
+
readdirSync,
|
|
447
|
+
readFileSync as readFileSync3,
|
|
448
|
+
writeFileSync as writeFileSync2,
|
|
449
|
+
mkdirSync as mkdirSync2,
|
|
450
|
+
existsSync as existsSync2,
|
|
451
|
+
renameSync as renameSync2,
|
|
452
|
+
rmSync
|
|
453
|
+
} from "node:fs";
|
|
454
|
+
import { basename, join as join4, isAbsolute as isAbsolute2 } from "node:path";
|
|
455
|
+
|
|
456
|
+
// scripts/lib/shared-path-helpers.mjs
|
|
457
|
+
import { realpathSync } from "node:fs";
|
|
458
|
+
import { isAbsolute, resolve as resolve2, sep } from "node:path";
|
|
459
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
460
|
+
|
|
461
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
462
|
+
init_heyspex_home();
|
|
463
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync as realpathSync3, statSync as statSync3 } from "node:fs";
|
|
464
|
+
import { homedir as homedir5, tmpdir } from "node:os";
|
|
465
|
+
|
|
466
|
+
// vendor/heyspex-mcp/usable-dir-env.mjs
|
|
467
|
+
import { statSync } from "node:fs";
|
|
468
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
469
|
+
|
|
470
|
+
// vendor/heyspex-mcp/vault-resolve.mjs
|
|
471
|
+
import { basename as basename2, dirname as dirname3, join as join5, resolve as resolve3 } from "node:path";
|
|
472
|
+
|
|
473
|
+
// vendor/heyspex-mcp/vault-binding.mjs
|
|
474
|
+
import {
|
|
475
|
+
copyFileSync,
|
|
476
|
+
existsSync as existsSync3,
|
|
477
|
+
mkdirSync as mkdirSync3,
|
|
478
|
+
readFileSync as readFileSync4,
|
|
479
|
+
realpathSync as realpathSync2,
|
|
480
|
+
renameSync as renameSync3,
|
|
481
|
+
statSync as statSync2,
|
|
482
|
+
unlinkSync,
|
|
483
|
+
writeFileSync as writeFileSync3
|
|
484
|
+
} from "node:fs";
|
|
485
|
+
import { homedir as homedir4 } from "node:os";
|
|
486
|
+
import path from "node:path";
|
|
487
|
+
|
|
488
|
+
// vendor/heyspex-mcp/console-fetch.mjs
|
|
489
|
+
var VAULT_NOT_REGISTERED = "VAULT_NOT_REGISTERED";
|
|
490
|
+
var VaultNotRegisteredError = class extends Error {
|
|
491
|
+
/**
|
|
492
|
+
* @param {string | null | undefined} repoRootAbs the root the resolver settled on
|
|
493
|
+
* @param {string | null | undefined} source the resolver tier that produced it
|
|
494
|
+
* @param {{ reason?: 'unregistered' | 'legacy-id', legacyId?: string | null }} [detail]
|
|
495
|
+
*/
|
|
496
|
+
constructor(repoRootAbs, source, { reason = "unregistered", legacyId = null } = {}) {
|
|
497
|
+
const root = typeof repoRootAbs === "string" && repoRootAbs.trim() ? repoRootAbs.trim() : null;
|
|
498
|
+
const where = root ? ` (${root})` : "";
|
|
499
|
+
const why = reason === "legacy-id" ? `This repo${where} is registered on this machine under a legacy id (\`${legacyId}\`), not the vault id the console knows, and its heyspex.yaml carries no \`vault_id\` to stand in for it, ` : `This repo${where} is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), `;
|
|
500
|
+
super(
|
|
501
|
+
why + `so the console call was refused before it was sent. Run \`${hsCommand("connect")}\`${root ? ` in ${root}` : ""} to ${reason === "legacy-id" ? "re-register" : "connect"} it, then try again. Nothing was sent.`
|
|
502
|
+
);
|
|
503
|
+
this.name = "VaultNotRegisteredError";
|
|
504
|
+
this.code = VAULT_NOT_REGISTERED;
|
|
505
|
+
this.reason = reason;
|
|
506
|
+
this.repoRootAbs = root;
|
|
507
|
+
this.source = typeof source === "string" ? source : null;
|
|
508
|
+
this.legacyId = legacyId;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
var CLOUD_VAULT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
512
|
+
function isCloudVaultId(value) {
|
|
513
|
+
return typeof value === "string" && CLOUD_VAULT_ID_RE.test(value.trim());
|
|
514
|
+
}
|
|
515
|
+
var MARKER_VAULT_ID_RE = /^vault_id:[ \t]*["']?([^"'\r\n#]+)/m;
|
|
516
|
+
function readMarkerVaultId(repoRootAbs, { readFile } = {}) {
|
|
517
|
+
if (typeof repoRootAbs !== "string" || !repoRootAbs.trim()) return null;
|
|
518
|
+
const read = readFile ?? ((p) => readFileSync6(p, "utf8"));
|
|
519
|
+
try {
|
|
520
|
+
const raw = read(join6(repoRootAbs, "heyspex.yaml"));
|
|
521
|
+
const value = String(raw ?? "").match(MARKER_VAULT_ID_RE)?.[1]?.trim() ?? "";
|
|
522
|
+
return value || null;
|
|
523
|
+
} catch {
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function cloudVaultIdFor(resolved, { readMarkerVaultId: readMarker = readMarkerVaultId } = {}) {
|
|
528
|
+
const rowId = resolved.vaultId.trim();
|
|
529
|
+
if (isCloudVaultId(rowId)) return rowId;
|
|
530
|
+
const marker = readMarker(resolved.repoRootAbs);
|
|
531
|
+
if (isCloudVaultId(marker)) return marker.trim();
|
|
532
|
+
throw new VaultNotRegisteredError(resolved.repoRootAbs, resolved.source, { reason: "legacy-id", legacyId: rowId });
|
|
533
|
+
}
|
|
534
|
+
var SCOPE_REFUSAL_CODES = Object.freeze(["VAULT_SCOPE_MISMATCH", "ACCOUNT_MISMATCH", "VAULT_NOT_REGISTERED"]);
|
|
535
|
+
|
|
536
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
537
|
+
init_credentials();
|
|
538
|
+
|
|
539
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
540
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
541
|
+
import { dirname as dirname4, join as join7, resolve as resolve4 } from "node:path";
|
|
542
|
+
|
|
543
|
+
// spex-console/src/lib/heyspex-home.ts
|
|
544
|
+
import { homedir as homedir7 } from "node:os";
|
|
545
|
+
function heyspexHome2(env = process.env) {
|
|
546
|
+
const override = env?.HEYSPEX_HOME;
|
|
547
|
+
if (typeof override === "string" && override.trim().length > 0) {
|
|
548
|
+
return override.trim();
|
|
549
|
+
}
|
|
550
|
+
return homedir7();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// spex-console/src/lib/vault-registry/types.ts
|
|
554
|
+
var LEGACY_REGISTRY_ACCOUNT_ID = "legacy";
|
|
555
|
+
var VAULT_REGISTRY_SCHEMA_VERSION = 2;
|
|
556
|
+
function emptyVaultRegistry() {
|
|
557
|
+
return {
|
|
558
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
559
|
+
current_account_id: null,
|
|
560
|
+
accounts: {},
|
|
561
|
+
workspaces: [],
|
|
562
|
+
vaults: []
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// spex-console/src/lib/vault-registry/registry-file.ts
|
|
567
|
+
function vaultRegistryDir(opts = {}) {
|
|
568
|
+
return opts.registryDir ?? join7(heyspexHome2(), ".heyspex");
|
|
569
|
+
}
|
|
570
|
+
function vaultRegistryFilePath(opts = {}) {
|
|
571
|
+
return join7(vaultRegistryDir(opts), "registry.json");
|
|
572
|
+
}
|
|
573
|
+
function isNonEmptyString(v) {
|
|
574
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
575
|
+
}
|
|
576
|
+
function accountOf(o) {
|
|
577
|
+
return isNonEmptyString(o.account_id) ? o.account_id : LEGACY_REGISTRY_ACCOUNT_ID;
|
|
578
|
+
}
|
|
579
|
+
function normalizeWorkspace(raw) {
|
|
580
|
+
if (!raw || typeof raw !== "object") return null;
|
|
581
|
+
const o = raw;
|
|
582
|
+
if (!isNonEmptyString(o.id)) return null;
|
|
583
|
+
if (!isNonEmptyString(o.display_name)) return null;
|
|
584
|
+
if (!isNonEmptyString(o.created_at)) return null;
|
|
585
|
+
const record = {
|
|
586
|
+
id: o.id,
|
|
587
|
+
display_name: o.display_name,
|
|
588
|
+
created_at: o.created_at,
|
|
589
|
+
archived: o.archived === true,
|
|
590
|
+
account_id: accountOf(o)
|
|
591
|
+
};
|
|
592
|
+
if (isNonEmptyString(o.github_org)) record.github_org = o.github_org;
|
|
593
|
+
return record;
|
|
594
|
+
}
|
|
595
|
+
function normalizeVault(raw) {
|
|
596
|
+
if (!raw || typeof raw !== "object") return null;
|
|
597
|
+
const o = raw;
|
|
598
|
+
if (!isNonEmptyString(o.id)) return null;
|
|
599
|
+
if (!isNonEmptyString(o.workspace_id)) return null;
|
|
600
|
+
if (!isNonEmptyString(o.display_name)) return null;
|
|
601
|
+
if (!isNonEmptyString(o.repo_root_abs)) return null;
|
|
602
|
+
if (typeof o.schema_version !== "number") return null;
|
|
603
|
+
if (!isNonEmptyString(o.registered_at)) return null;
|
|
604
|
+
const record = {
|
|
605
|
+
id: o.id,
|
|
606
|
+
workspace_id: o.workspace_id,
|
|
607
|
+
display_name: o.display_name,
|
|
608
|
+
repo_root_abs: o.repo_root_abs,
|
|
609
|
+
schema_version: o.schema_version,
|
|
610
|
+
registered_at: o.registered_at,
|
|
611
|
+
archived: o.archived === true,
|
|
612
|
+
account_id: accountOf(o)
|
|
613
|
+
};
|
|
614
|
+
if (isNonEmptyString(o.github_remote)) record.github_remote = o.github_remote;
|
|
615
|
+
return record;
|
|
616
|
+
}
|
|
617
|
+
function normalizeStoredPointers(raw) {
|
|
618
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
619
|
+
const out = {};
|
|
620
|
+
for (const field of ["active_vault_id", "active_workspace_id"]) {
|
|
621
|
+
const v = o[field];
|
|
622
|
+
if (v === null || isNonEmptyString(v)) out[field] = v;
|
|
623
|
+
}
|
|
624
|
+
return out;
|
|
625
|
+
}
|
|
626
|
+
function normalizePointers(raw) {
|
|
627
|
+
const o = raw && typeof raw === "object" ? raw : {};
|
|
628
|
+
return {
|
|
629
|
+
active_vault_id: isNonEmptyString(o.active_vault_id) ? o.active_vault_id : null,
|
|
630
|
+
active_workspace_id: isNonEmptyString(o.active_workspace_id) ? o.active_workspace_id : null
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function registryFileSchemaOf(json) {
|
|
634
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) return null;
|
|
635
|
+
return json.schema_version === 2 ? 2 : 1;
|
|
636
|
+
}
|
|
637
|
+
function parseVaultRegistryFile(json) {
|
|
638
|
+
const schema = registryFileSchemaOf(json);
|
|
639
|
+
if (schema === null) return emptyVaultRegistry();
|
|
640
|
+
const o = json;
|
|
641
|
+
const workspacesRaw = Array.isArray(o.workspaces) ? o.workspaces : [];
|
|
642
|
+
const workspaces = workspacesRaw.map(normalizeWorkspace).filter((w) => w !== null);
|
|
643
|
+
const vaultsRaw = Array.isArray(o.vaults) ? o.vaults : [];
|
|
644
|
+
const vaults = vaultsRaw.map(normalizeVault).filter((v) => v !== null);
|
|
645
|
+
if (schema === 1) {
|
|
646
|
+
const legacy = normalizePointers(o);
|
|
647
|
+
const hasLegacyState = legacy.active_vault_id !== null || legacy.active_workspace_id !== null || workspaces.length > 0 || vaults.length > 0;
|
|
648
|
+
return {
|
|
649
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
650
|
+
current_account_id: hasLegacyState ? LEGACY_REGISTRY_ACCOUNT_ID : null,
|
|
651
|
+
accounts: hasLegacyState ? { [LEGACY_REGISTRY_ACCOUNT_ID]: legacy } : {},
|
|
652
|
+
workspaces,
|
|
653
|
+
vaults
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const accounts = {};
|
|
657
|
+
if (o.accounts && typeof o.accounts === "object" && !Array.isArray(o.accounts)) {
|
|
658
|
+
for (const [id, pointers] of Object.entries(o.accounts)) {
|
|
659
|
+
if (isNonEmptyString(id)) accounts[id] = normalizeStoredPointers(pointers);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
schema_version: VAULT_REGISTRY_SCHEMA_VERSION,
|
|
664
|
+
current_account_id: isNonEmptyString(o.current_account_id) ? o.current_account_id : null,
|
|
665
|
+
accounts,
|
|
666
|
+
workspaces,
|
|
667
|
+
vaults
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function readRegistryJson(opts) {
|
|
671
|
+
const filePath = vaultRegistryFilePath(opts);
|
|
672
|
+
if (!existsSync5(filePath)) return { kind: "missing" };
|
|
673
|
+
try {
|
|
674
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
675
|
+
const json = JSON.parse(raw);
|
|
676
|
+
if (registryFileSchemaOf(json) === null) return { kind: "unreadable" };
|
|
677
|
+
return { kind: "ok", raw, json };
|
|
678
|
+
} catch {
|
|
679
|
+
return { kind: "unreadable" };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function inspectVaultRegistry(opts = {}) {
|
|
683
|
+
const read = readRegistryJson(opts);
|
|
684
|
+
if (read.kind !== "ok") {
|
|
685
|
+
return {
|
|
686
|
+
registry: emptyVaultRegistry(),
|
|
687
|
+
file_schema_version: null,
|
|
688
|
+
registry_unreadable: read.kind === "unreadable"
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
registry: parseVaultRegistryFile(read.json),
|
|
693
|
+
file_schema_version: registryFileSchemaOf(read.json),
|
|
694
|
+
registry_unreadable: false
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function loadVaultRegistry(opts = {}) {
|
|
698
|
+
return inspectVaultRegistry(opts).registry;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// spex-console/src/lib/cli/control-shared.ts
|
|
702
|
+
var EXIT = {
|
|
703
|
+
/** Success. */
|
|
704
|
+
OK: 0,
|
|
705
|
+
/**
|
|
706
|
+
* A read-only CHECK ran correctly and its answer is "no" — `hs connect
|
|
707
|
+
* --check` on an unconnected repo. Distinct from {@link EXIT.USAGE} so a
|
|
708
|
+
* script can tell "you typed the command wrong" from "the command worked,
|
|
709
|
+
* the repo is not connected". Both were `1` until 2026-09-18, which made
|
|
710
|
+
* `--check` unusable in a conditional: every caller had to parse stdout to
|
|
711
|
+
* find out which of the two had happened.
|
|
712
|
+
*/
|
|
713
|
+
CHECK_FAILED: 2,
|
|
714
|
+
/** Usage error — unknown subcommand/flag, or a missing required argument. */
|
|
715
|
+
USAGE: 1,
|
|
716
|
+
/** Claim race lost — the spec is already claimed by another agent (409). */
|
|
717
|
+
CONFLICT: 9,
|
|
718
|
+
/** Target not found — release of an unheld claim, etc. (404). */
|
|
719
|
+
NOT_FOUND: 10,
|
|
720
|
+
/** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
|
|
721
|
+
INVALID_ARG: 11,
|
|
722
|
+
/** SpexConsole unreachable (network error reaching the dev server). */
|
|
723
|
+
CONSOLE_UNREACHABLE: 12,
|
|
724
|
+
/** Server failure — a non-OK response that isn't 400/402/404/409. */
|
|
725
|
+
WRITE_FAILED: 13,
|
|
726
|
+
/** Neon control plane absent — the endpoint returned 402 (upgrade required). */
|
|
727
|
+
NEON_REQUIRED: 14,
|
|
728
|
+
/**
|
|
729
|
+
* A cross-account `--account <id>` override was refused — the caller is
|
|
730
|
+
* not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
|
|
731
|
+
* [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
|
|
732
|
+
* from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
|
|
733
|
+
* malformed command.
|
|
734
|
+
*/
|
|
735
|
+
FORBIDDEN: 15,
|
|
736
|
+
/**
|
|
737
|
+
* A CLOUD write refused because no account id could be resolved (no
|
|
738
|
+
* `HS_ACCOUNT_ID` env var, no `account_id:` in `~/.heyspex/config.yaml`) --
|
|
739
|
+
* `resolveCloudAccountIdOrRefuse` in `vault-workspace-shared.ts`
|
|
740
|
+
* ([[HS-YU1S-UAGY-TASK-cloud-writes-refuse-without-a]]). Distinct from
|
|
741
|
+
* `FORBIDDEN` -- this is "not signed in at all", not "signed in but not
|
|
742
|
+
* authorized for this override".
|
|
743
|
+
*/
|
|
744
|
+
SIGN_IN_REQUIRED: 16,
|
|
745
|
+
/**
|
|
746
|
+
* Signed in, but the account a repo would be attached to was not confirmed:
|
|
747
|
+
* a non-interactive install/connect with no matching `--account` / `--yes`,
|
|
748
|
+
* an `account_id` that disagrees with the bearer's user, or the human
|
|
749
|
+
* aborting the identity prompt (`install-identity.ts`,
|
|
750
|
+
* [[HS-HAQI-KODY-DIAG-hs-install-and-hs-connect-bind-a-new]]).
|
|
751
|
+
*/
|
|
752
|
+
IDENTITY_UNCONFIRMED: 17,
|
|
753
|
+
/**
|
|
754
|
+
* The repo this verb runs in is not registered on this machine (no
|
|
755
|
+
* `~/.heyspex/registry.json` row for its root, or a legacy slug row with no
|
|
756
|
+
* cloud `vault_id` in its heyspex.yaml), so the call was refused LOCALLY —
|
|
757
|
+
* nothing was sent. Names `hs connect`
|
|
758
|
+
* ([[HS-MVG4-WW9R-TASK-mcp-and-hs-name-account-vault-on-every]]).
|
|
759
|
+
*/
|
|
760
|
+
VAULT_NOT_REGISTERED: 18,
|
|
761
|
+
/**
|
|
762
|
+
* The local half of a setup verb (`hs connect`, `hs vault *`, `hs workspace
|
|
763
|
+
* *`) was written but the CLOUD half did not land -- the console refused
|
|
764
|
+
* it or was never reached. The verb's receipt names the failed half and the
|
|
765
|
+
* exact command to re-run
|
|
766
|
+
* ([[HS-STCY-WW9R-TASK-add-a-shared-receipt-that-reads-back]], invariant 6).
|
|
767
|
+
* Distinct from {@link EXIT.WRITE_FAILED} so a script can tell "re-run to
|
|
768
|
+
* sync the cloud" from a generic server failure.
|
|
769
|
+
*/
|
|
770
|
+
CLOUD_WRITE_FAILED: 19,
|
|
771
|
+
/**
|
|
772
|
+
* The repo's committed `heyspex.yaml` records `account: <A>` but the machine
|
|
773
|
+
* is signed in as another account, so `hs connect` refused and wrote
|
|
774
|
+
* nothing (marker, registry, cloud). Repair: `hs login --switch-account <A>`
|
|
775
|
+
* ([[HS-VWUD-WW9R-TASK-record-the-connecting-account-in]], invariant 4).
|
|
776
|
+
*/
|
|
777
|
+
ACCOUNT_MISMATCH: 20,
|
|
778
|
+
/**
|
|
779
|
+
* Both sides of a setup verb's receipt were read back and still DISAGREE
|
|
780
|
+
* after a short bounded re-read of the cloud side (a briefly stale read is
|
|
781
|
+
* absorbed first). The write may well have landed; what the verb cannot
|
|
782
|
+
* claim is that this machine and the console now say the same thing. The
|
|
783
|
+
* receipt prints the diffs and the command to re-run
|
|
784
|
+
* ([[HS-FKSR-WW9R-TASK-make-one-hs-connect-run-create-a-cloud]], invariant 6).
|
|
785
|
+
* Distinct from {@link EXIT.CLOUD_WRITE_FAILED} (a write that did not land).
|
|
786
|
+
*/
|
|
787
|
+
RECEIPT_DISAGREE: 21
|
|
788
|
+
};
|
|
789
|
+
var VAULT_ID_HEADER = "x-heyspex-vault-id";
|
|
790
|
+
var ACCOUNT_ID_HEADER = "x-heyspex-account-id";
|
|
791
|
+
function readPairedBearer(opts = {}) {
|
|
792
|
+
return readBearer(opts);
|
|
793
|
+
}
|
|
794
|
+
function readPairedToken(opts = {}) {
|
|
795
|
+
return readPairedBearer(opts).token ?? void 0;
|
|
796
|
+
}
|
|
797
|
+
function bearerHeaderFor(consoleUrl, opts = {}) {
|
|
798
|
+
try {
|
|
799
|
+
const host = new URL(consoleUrl).hostname;
|
|
800
|
+
if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1") {
|
|
801
|
+
return {};
|
|
802
|
+
}
|
|
803
|
+
const token = readPairedToken(opts);
|
|
804
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
805
|
+
} catch {
|
|
806
|
+
return {};
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function scopeHeadersFor(scope, method, path3) {
|
|
810
|
+
if (scope === "none") {
|
|
811
|
+
if (!isVaultLessEndpoint(method, path3)) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`controlRequest: scope "none" is only allowed for the vault-less endpoints (${VAULT_LESS_ENDPOINTS.join(", ")}); ${method.toUpperCase()} ${path3} is vault-scoped and must name { accountId, vaultId }.`
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
return {};
|
|
817
|
+
}
|
|
818
|
+
return { [VAULT_ID_HEADER]: scope.vaultId, [ACCOUNT_ID_HEADER]: scope.accountId };
|
|
819
|
+
}
|
|
820
|
+
async function controlRequest(deps, req) {
|
|
821
|
+
const url = `${deps.consoleUrl}${req.path}`;
|
|
822
|
+
const scopeHeaders = scopeHeadersFor(deps.scope, req.method, req.path);
|
|
823
|
+
let res;
|
|
824
|
+
try {
|
|
825
|
+
res = await deps.fetchImpl(url, {
|
|
826
|
+
method: req.method,
|
|
827
|
+
headers: {
|
|
828
|
+
"content-type": "application/json",
|
|
829
|
+
// A remote console (cloud vault write mode, [[HS-PH7O-ILNC-DIAG-hs-check-report-cannot-authenticate]])
|
|
830
|
+
// authenticates every control call; loopback never asked, so the header
|
|
831
|
+
// is attached only when the target is non-loopback and a paired token
|
|
832
|
+
// exists. Same `hsmcp_` bearer the MCP server presents.
|
|
833
|
+
...bearerHeaderFor(deps.consoleUrl),
|
|
834
|
+
// Invariant 2: name the vault and the account on every call, or the
|
|
835
|
+
// console serves whatever its leftover pointer says.
|
|
836
|
+
...scopeHeaders
|
|
837
|
+
},
|
|
838
|
+
...req.body !== void 0 ? { body: JSON.stringify(req.body) } : {}
|
|
839
|
+
});
|
|
840
|
+
} catch (err) {
|
|
841
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
842
|
+
return {
|
|
843
|
+
ok: false,
|
|
844
|
+
error: {
|
|
845
|
+
code: EXIT.CONSOLE_UNREACHABLE,
|
|
846
|
+
message: `could not reach console at ${deps.consoleUrl}: ${message}
|
|
847
|
+
Start the dev server or set HEYSPEX_CONSOLE_URL.`,
|
|
848
|
+
payload: {
|
|
849
|
+
ok: false,
|
|
850
|
+
error: "console_unreachable",
|
|
851
|
+
url: deps.consoleUrl,
|
|
852
|
+
message
|
|
853
|
+
},
|
|
854
|
+
isError: true
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
let parsed = null;
|
|
859
|
+
try {
|
|
860
|
+
parsed = await res.json();
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
if (res.status === 402) {
|
|
864
|
+
return {
|
|
865
|
+
ok: false,
|
|
866
|
+
error: {
|
|
867
|
+
code: EXIT.NEON_REQUIRED,
|
|
868
|
+
message: "this command requires the Neon control plane, which is not configured (console returned 402).",
|
|
869
|
+
payload: {
|
|
870
|
+
ok: false,
|
|
871
|
+
error: "neon_required",
|
|
872
|
+
status: 402,
|
|
873
|
+
console_response: parsed
|
|
874
|
+
},
|
|
875
|
+
isError: true
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return { ok: true, status: res.status, body: parsed };
|
|
880
|
+
}
|
|
881
|
+
function envelopeMessage(body) {
|
|
882
|
+
if (body && typeof body === "object" && "message" in body) {
|
|
883
|
+
const m = body.message;
|
|
884
|
+
if (typeof m === "string") return m;
|
|
885
|
+
}
|
|
886
|
+
return "";
|
|
887
|
+
}
|
|
888
|
+
function writeFailed(status, body) {
|
|
889
|
+
const detail = envelopeMessage(body);
|
|
890
|
+
return {
|
|
891
|
+
code: EXIT.WRITE_FAILED,
|
|
892
|
+
message: `console returned ${status}${detail ? `: ${detail}` : ""}`,
|
|
893
|
+
payload: { ok: false, error: "write_failed", status, console_response: body },
|
|
894
|
+
isError: true
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
function normalizeRoot(p) {
|
|
898
|
+
return path2.resolve(p).replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
899
|
+
}
|
|
900
|
+
function resolveControlScope(input) {
|
|
901
|
+
const target = normalizeRoot(input.vaultRoot);
|
|
902
|
+
const row = input.vaults.find((v) => normalizeRoot(v.repo_root_abs) === target) ?? null;
|
|
903
|
+
if (!row) {
|
|
904
|
+
return {
|
|
905
|
+
ok: false,
|
|
906
|
+
error: {
|
|
907
|
+
code: EXIT.VAULT_NOT_REGISTERED,
|
|
908
|
+
message: `this repo (${input.vaultRoot}) is not connected to a HeySpex vault on this machine (\`hs connect\` has not registered it), so the call was refused before it was sent. Run \`${hsCommand("connect")}\` in ${input.vaultRoot}, then try again.`,
|
|
909
|
+
payload: {
|
|
910
|
+
ok: false,
|
|
911
|
+
error: "vault_not_registered",
|
|
912
|
+
reason: "unregistered",
|
|
913
|
+
vault_root: input.vaultRoot
|
|
914
|
+
},
|
|
915
|
+
isError: true
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
let vaultId;
|
|
920
|
+
try {
|
|
921
|
+
vaultId = cloudVaultIdFor(
|
|
922
|
+
{ vaultId: row.id, repoRootAbs: row.repo_root_abs, source: "registry" },
|
|
923
|
+
input.readMarkerVaultId ? { readMarkerVaultId: input.readMarkerVaultId } : {}
|
|
924
|
+
);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
if (!(err instanceof VaultNotRegisteredError)) throw err;
|
|
927
|
+
const refused = err;
|
|
928
|
+
return {
|
|
929
|
+
ok: false,
|
|
930
|
+
error: {
|
|
931
|
+
code: EXIT.VAULT_NOT_REGISTERED,
|
|
932
|
+
message: refused.message,
|
|
933
|
+
payload: {
|
|
934
|
+
ok: false,
|
|
935
|
+
error: "vault_not_registered",
|
|
936
|
+
reason: refused.reason ?? "legacy-id",
|
|
937
|
+
vault_root: input.vaultRoot,
|
|
938
|
+
legacy_id: refused.legacyId ?? row.id
|
|
939
|
+
},
|
|
940
|
+
isError: true
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
const accountId = input.accountId?.trim() ?? "";
|
|
945
|
+
if (!accountId) {
|
|
946
|
+
return {
|
|
947
|
+
ok: false,
|
|
948
|
+
error: {
|
|
949
|
+
code: EXIT.SIGN_IN_REQUIRED,
|
|
950
|
+
message: `no HeySpex account is configured on this machine (\`hs login\` has not been run), so the call was refused before it was sent. Run \`${hsCommand("login")}\` on this machine, then try again.`,
|
|
951
|
+
payload: { ok: false, error: "sign_in_required", vault_root: input.vaultRoot },
|
|
952
|
+
isError: true
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
return { ok: true, scope: { accountId, vaultId } };
|
|
957
|
+
}
|
|
958
|
+
function resolveControlScopeFromMachine(vaultRoot, opts = {}) {
|
|
959
|
+
const registry = loadVaultRegistry(opts.registry);
|
|
960
|
+
const accountId = resolveSessionOwnerUncached({ env: opts.env, homeDir: opts.homeDir });
|
|
961
|
+
return resolveControlScope({ vaultRoot, accountId, vaults: registry.vaults });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// scripts/lib/console-url-resolver.mjs
|
|
965
|
+
init_heyspex_home();
|
|
966
|
+
init_dev_mode();
|
|
967
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
968
|
+
import { homedir as homedir9 } from "node:os";
|
|
969
|
+
import { join as join9 } from "node:path";
|
|
970
|
+
var DEFAULT_CONSOLE_URL = LOCAL_CONSOLE_URL;
|
|
971
|
+
function trimTrailingSlash(url) {
|
|
972
|
+
return url.replace(/\/+$/, "");
|
|
973
|
+
}
|
|
974
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]", "0.0.0.0"]);
|
|
975
|
+
function isLoopbackConsoleUrl(url) {
|
|
976
|
+
if (typeof url !== "string" || !url) return false;
|
|
977
|
+
try {
|
|
978
|
+
return LOOPBACK_HOSTS.has(new URL(url).hostname.toLowerCase());
|
|
979
|
+
} catch {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
var staleLoopbackCredentialsWarned = false;
|
|
984
|
+
function warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log }) {
|
|
985
|
+
if (staleLoopbackCredentialsWarned) return;
|
|
986
|
+
staleLoopbackCredentialsWarned = true;
|
|
987
|
+
try {
|
|
988
|
+
log(
|
|
989
|
+
`[heyspex] ignoring console_url ${credentialsUrl} from ~/.heyspex/credentials.yaml: the resolved reliance is cloud, and a loopback console cannot serve it \u2014 using ${derivedUrl} (set HEYSPEX_CONSOLE_URL to override explicitly, or run \`hs dev mode:local\` to go back to a local console).
|
|
990
|
+
`
|
|
991
|
+
);
|
|
992
|
+
} catch {
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function readCredentialsConsoleUrl({ homeDir = heyspexHome(), readFile = defaultReadFile2 } = {}) {
|
|
996
|
+
try {
|
|
997
|
+
const raw = readFile(join9(homeDir, ".heyspex", "credentials.yaml"));
|
|
998
|
+
const match = String(raw ?? "").match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
|
|
999
|
+
const value = match?.[1]?.trim();
|
|
1000
|
+
return value && value.length > 0 ? trimTrailingSlash(value) : null;
|
|
1001
|
+
} catch {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function defaultReadFile2(p) {
|
|
1006
|
+
return readFileSync9(p, "utf8");
|
|
1007
|
+
}
|
|
1008
|
+
function readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery: readDiscovery2 = defaultReadFile2 } = {}) {
|
|
1009
|
+
if (!repoRoot) return null;
|
|
1010
|
+
try {
|
|
1011
|
+
const discovery = String(readDiscovery2(join9(repoRoot, ".heyspex", "console-url")) ?? "").trim();
|
|
1012
|
+
if (discovery.length > 0) return trimTrailingSlash(discovery);
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
var ConsoleUrlUnresolvedError = class extends Error {
|
|
1018
|
+
constructor() {
|
|
1019
|
+
super(
|
|
1020
|
+
"resolveConsoleUrl: local reliance with no HEYSPEX_CONSOLE_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the localhost:3000 default in strict mode."
|
|
1021
|
+
);
|
|
1022
|
+
this.name = "ConsoleUrlUnresolvedError";
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
function resolveModeEnvelope({ env, homeDir }) {
|
|
1026
|
+
try {
|
|
1027
|
+
const resolved = resolveDevMode({ env, homeDir });
|
|
1028
|
+
return {
|
|
1029
|
+
reliance: resolved.reliance === "cloud" ? "cloud" : "local",
|
|
1030
|
+
consoleUrl: resolved.consoleUrl || DEFAULT_CONSOLE_URL
|
|
1031
|
+
};
|
|
1032
|
+
} catch {
|
|
1033
|
+
return { reliance: "local", consoleUrl: DEFAULT_CONSOLE_URL };
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function resolveConsoleUrl({
|
|
1037
|
+
repoRoot,
|
|
1038
|
+
env = process.env,
|
|
1039
|
+
homeDir = heyspexHome(),
|
|
1040
|
+
strict = false,
|
|
1041
|
+
devMode,
|
|
1042
|
+
log = (line) => process.stderr.write(line),
|
|
1043
|
+
readFile = defaultReadFile2,
|
|
1044
|
+
readDiscovery: readDiscovery2 = defaultReadFile2
|
|
1045
|
+
} = {}) {
|
|
1046
|
+
const envUrl = env.HEYSPEX_CONSOLE_URL?.trim();
|
|
1047
|
+
if (envUrl) return trimTrailingSlash(envUrl);
|
|
1048
|
+
const mode = devMode ?? resolveModeEnvelope({ env, homeDir });
|
|
1049
|
+
const cloud = mode.reliance === "cloud";
|
|
1050
|
+
const derivedUrl = trimTrailingSlash(
|
|
1051
|
+
cloud && isLoopbackConsoleUrl(mode.consoleUrl) ? CLOUD_CONSOLE_URL : mode.consoleUrl || DEFAULT_CONSOLE_URL
|
|
1052
|
+
);
|
|
1053
|
+
const credentialsUrl = readCredentialsConsoleUrl({ homeDir, readFile });
|
|
1054
|
+
if (credentialsUrl) {
|
|
1055
|
+
if (cloud && isLoopbackConsoleUrl(credentialsUrl)) {
|
|
1056
|
+
warnStaleLoopbackCredentialsOnce(credentialsUrl, derivedUrl, { log });
|
|
1057
|
+
} else {
|
|
1058
|
+
return credentialsUrl;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (!cloud) {
|
|
1062
|
+
const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot, { readDiscovery: readDiscovery2 });
|
|
1063
|
+
if (discoveryUrl) return discoveryUrl;
|
|
1064
|
+
if (strict) throw new ConsoleUrlUnresolvedError();
|
|
1065
|
+
}
|
|
1066
|
+
return derivedUrl;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// spex-console/src/lib/cli/defer.ts
|
|
1070
|
+
function resolveSpexConsoleUrl(vaultRoot, readDiscovery2, opts = {}) {
|
|
1071
|
+
return resolveConsoleUrl({
|
|
1072
|
+
repoRoot: vaultRoot,
|
|
1073
|
+
env: opts.env,
|
|
1074
|
+
homeDir: opts.homeDir,
|
|
1075
|
+
readFile: opts.readFile,
|
|
1076
|
+
readDiscovery: readDiscovery2,
|
|
1077
|
+
log: opts.log
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
var HELP = [
|
|
1081
|
+
"Usage:",
|
|
1082
|
+
" hs defer <spec-id> [--until <when>] [--json] [--no-daemon]",
|
|
1083
|
+
" hs undefer <spec-id> [--json] [--no-daemon]",
|
|
1084
|
+
"",
|
|
1085
|
+
"Time-shift a spec via its `defer_until` frontmatter field. A deferred spec is",
|
|
1086
|
+
"scheduler-invisible until the date passes, then auto-revives.",
|
|
1087
|
+
"",
|
|
1088
|
+
"Arguments:",
|
|
1089
|
+
" <spec-id> The spec to defer/undefer (e.g. HS-TASK-EWL5).",
|
|
1090
|
+
"",
|
|
1091
|
+
"Flags:",
|
|
1092
|
+
" --until <when> ISO date, or relative offset +Nd / +Nw / +Nm / +Ny.",
|
|
1093
|
+
" Defaults to +1y (matches the console Defer button).",
|
|
1094
|
+
" Ignored by `undefer` (which always clears the field).",
|
|
1095
|
+
" --json Emit a machine-readable JSON result.",
|
|
1096
|
+
" --no-daemon Write straight to the vault (no console needed; CI path).",
|
|
1097
|
+
" --help, -h Show this help.",
|
|
1098
|
+
"",
|
|
1099
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 10 not-found \xB7 11 bad-arg \xB7 12 console-unreachable \xB7 13 write-failed"
|
|
1100
|
+
].join("\n");
|
|
1101
|
+
|
|
1102
|
+
// spex-console/src/lib/launch-model-aliases.ts
|
|
1103
|
+
var FAMILIES = [
|
|
1104
|
+
{ canonical: "claude-opus", family: "opus", cliNativeBareForm: "opus" },
|
|
1105
|
+
{ canonical: "claude-sonnet", family: "sonnet", cliNativeBareForm: "sonnet" },
|
|
1106
|
+
{ canonical: "claude-fable", family: "fable", cliNativeBareForm: "fable" },
|
|
1107
|
+
{ canonical: "claude-haiku", family: "haiku", cliNativeBareForm: null }
|
|
1108
|
+
];
|
|
1109
|
+
var ALIAS_LOOKUP = new Map(
|
|
1110
|
+
FAMILIES.flatMap((entry) => [[entry.canonical, entry], [entry.family, entry]])
|
|
1111
|
+
);
|
|
1112
|
+
var MODEL_ALIAS_INPUT_FORMS = Array.from(ALIAS_LOOKUP.keys());
|
|
1113
|
+
function normalizeModelAlias(value) {
|
|
1114
|
+
const key = value.trim().toLowerCase();
|
|
1115
|
+
if (!key) return null;
|
|
1116
|
+
return ALIAS_LOOKUP.get(key) ?? null;
|
|
1117
|
+
}
|
|
1118
|
+
function isModelAliasValue(value) {
|
|
1119
|
+
return normalizeModelAlias(value) !== null;
|
|
1120
|
+
}
|
|
1121
|
+
function canonicalizeModelAlias(value) {
|
|
1122
|
+
return normalizeModelAlias(value)?.canonical ?? null;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// dev-factory/model-catalog.json
|
|
1126
|
+
var model_catalog_default = {
|
|
1127
|
+
_comment: "SINGLE SOURCE OF TRUTH for model ids, aliases, retirements, context windows, capabilities and list prices \u2014 every cloud provider the fleet can call (DeepSeek, Anthropic, OpenAI, Gemini, OpenRouter, GitHub Models) AND every local runtime (Ollama, vLLM). Hand-edit THIS file when a vendor ships or retires a model; never scatter a new literal id through the code. Readers: dev-factory/lib/model-catalog.mjs (typed accessors + self-test), dev-factory/cloud-clients.mjs (the whole PROVIDERS table is derived from `providerKeys` below), dev-factory/lib/fleet-manifest.mjs (context/VRAM/cost tables), spexharness/src/chat/compact.mjs (window sizes), spex-console/src/lib/model-catalog.ts (the console's ONLY import path \u2014 keep it so when dev-factory merges into SpexFleet one line moves), dev-factory/judge.mjs, dev-factory/router.mjs --self-test (refuses retired ids in routing-table.json), scripts/check-model-ids.mjs (refuses retired ids in live code). ROUTING (which seat runs which model, which machine hosts which local model) stays in routing-table.json \u2014 this file says what a model IS and how it is reached, not who uses it. UPGRADE RECIPE: (1) add the new id under `models` with its window/price; (2) move the old id into `aliases` (old \u2192 new) and `retired`; (3) repoint the `roles` / `providerKeys` you want moved; (4) run `node dev-factory/lib/model-catalog.mjs --self-test && node dev-factory/router.mjs --self-test && npm run check:model-ids`. `verifiedOn` per provider records when the prices were last checked against the vendor's sheet.",
|
|
1128
|
+
verified: "2026-09-18",
|
|
1129
|
+
cloud: {
|
|
1130
|
+
deepseek: {
|
|
1131
|
+
kind: "openai",
|
|
1132
|
+
company: "deepseek",
|
|
1133
|
+
baseUrl: "https://api.deepseek.com/v1",
|
|
1134
|
+
betaBaseUrl: "https://api.deepseek.com/beta",
|
|
1135
|
+
anthropicBaseUrl: "https://api.deepseek.com/anthropic",
|
|
1136
|
+
userIdField: "user_id",
|
|
1137
|
+
_userIdFieldNote: "Wire field for per-seat cache/scheduling isolation (DeepSeek rate-limit doc: user_id isolates KVCache and scheduling). cloud-clients.mjs sends opts.userId (defaulting to opts.seat) under this name only for providers that declare it; betaBaseUrl is used only when a caller opts into strict tool schemas (opts.strictTools) \u2014 DeepSeek's strict mode is served from /beta.",
|
|
1138
|
+
keyEnv: "HEYSPEX_DEEPSEEK_API_KEY",
|
|
1139
|
+
keyEnvFallback: "DEEPSEEK_API_KEY",
|
|
1140
|
+
source: "https://api-docs.deepseek.com/quick_start/pricing",
|
|
1141
|
+
verifiedOn: "2026-09-18",
|
|
1142
|
+
peakHoursUtc: { days: [1, 2, 3, 4, 5], ranges: [["01:00", "04:00"], ["06:00", "10:00"]], note: "Off-peak rates are half of peak; all other hours are off-peak (DeepSeek pricing note 3)." },
|
|
1143
|
+
roles: {
|
|
1144
|
+
cheap: "deepseek-flash",
|
|
1145
|
+
strong: "deepseek-v4-pro",
|
|
1146
|
+
chatDefault: "deepseek-flash",
|
|
1147
|
+
judge: "deepseek-flash",
|
|
1148
|
+
legacyJudge: "deepseek-chat"
|
|
1149
|
+
},
|
|
1150
|
+
models: {
|
|
1151
|
+
"deepseek-flash": {
|
|
1152
|
+
displayName: "DeepSeek-V4.1-Flash",
|
|
1153
|
+
released: "2026-09-10",
|
|
1154
|
+
contextWindow: 1048576,
|
|
1155
|
+
maxOutputTokens: 393216,
|
|
1156
|
+
tier: "cheap",
|
|
1157
|
+
capabilities: ["api", "metered", "tools", "thinking", "json", "vision", "strict-tools", "fim", "prefix"],
|
|
1158
|
+
thinkingDefault: true,
|
|
1159
|
+
effortLevels: ["low", "high", "max"],
|
|
1160
|
+
concurrencyLimit: 2500,
|
|
1161
|
+
pricePer1M: {
|
|
1162
|
+
peak: { cacheHit: 6e-3, cacheMiss: 0.3, output: 1.2 },
|
|
1163
|
+
offPeak: { cacheHit: 3e-3, cacheMiss: 0.15, output: 0.6 }
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
"deepseek-v4-pro": {
|
|
1167
|
+
displayName: "DeepSeek-V4-Pro-0813",
|
|
1168
|
+
released: "2026-08-13",
|
|
1169
|
+
contextWindow: 1048576,
|
|
1170
|
+
maxOutputTokens: 393216,
|
|
1171
|
+
tier: "strong",
|
|
1172
|
+
capabilities: ["api", "metered", "tools", "thinking", "json", "strict-tools", "fim", "prefix", "max_reasoning"],
|
|
1173
|
+
thinkingDefault: true,
|
|
1174
|
+
effortLevels: ["low", "high", "max"],
|
|
1175
|
+
concurrencyLimit: 500,
|
|
1176
|
+
note: "Retirement announced 2026-09-10 was reversed; service continues past 2026-09-14 at unchanged billing (pricing note 2).",
|
|
1177
|
+
pricePer1M: {
|
|
1178
|
+
peak: { cacheHit: 0.044, cacheMiss: 1.32, output: 3.96 },
|
|
1179
|
+
offPeak: { cacheHit: 0.022, cacheMiss: 0.66, output: 1.98 }
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
aliases: {
|
|
1184
|
+
"deepseek-v4-flash": "deepseek-flash",
|
|
1185
|
+
"deepseek-v4-flash-vision-exp": "deepseek-flash",
|
|
1186
|
+
"deepseek-chat": "deepseek-flash"
|
|
1187
|
+
},
|
|
1188
|
+
retired: [
|
|
1189
|
+
{ id: "deepseek-v4-flash", on: "2026-09-10", servedBy: "deepseek-flash", note: "Legacy name still accepted by the API and routed to V4.1 Flash at the Flash price 'temporarily'." },
|
|
1190
|
+
{ id: "deepseek-v4-flash-vision-exp", on: "2026-09-10", servedBy: "deepseek-flash" }
|
|
1191
|
+
],
|
|
1192
|
+
unlisted: {
|
|
1193
|
+
"deepseek-chat": "Not on the 2026-09-10 model sheet. Kept only as the held-constant LLM-judge alias (PROVIDERS key `deepseek`); priced as deepseek-flash. Verify with --liveness before relying on it."
|
|
1194
|
+
},
|
|
1195
|
+
emitAliasesAsProviderKeys: true,
|
|
1196
|
+
providerKeys: {
|
|
1197
|
+
"deepseek-flash": { model: "deepseek-flash" },
|
|
1198
|
+
"deepseek-v4-pro": { model: "deepseek-v4-pro" },
|
|
1199
|
+
deepseek: { model: "deepseek-chat", legacyAliasOf: "deepseek-flash", note: "Legacy alias, held-constant LLM judge. Sends `deepseek-chat` on the wire, priced as deepseek-flash." }
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
anthropic: {
|
|
1203
|
+
kind: "anthropic",
|
|
1204
|
+
company: "anthropic",
|
|
1205
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
1206
|
+
keyEnv: "HEYSPEX_ANTHROPIC_API_KEY",
|
|
1207
|
+
source: "claude-api skill model table (cached 2026-06-24) \u2014 first-party API rates",
|
|
1208
|
+
verifiedOn: "2026-06-24",
|
|
1209
|
+
_note: "Anthropic models are excluded from the dev-factory council by construction and are the ceiling/reference arm in benches; the PROVIDERS keys `claude` / `claude-opus` still point at the 4.x generation on purpose (flip `providerKeys` to the 5-family when the owner rules on it \u2014 the ids and prices are already here). Claude Code CLI launches use the bare alias words in `cliAliases` (resolved by spex-console/src/lib/model-registry/launch-resolve.ts), not these ids.",
|
|
1210
|
+
roles: {
|
|
1211
|
+
default: "claude-opus-5",
|
|
1212
|
+
strong: "claude-opus-5",
|
|
1213
|
+
mid: "claude-sonnet-5",
|
|
1214
|
+
cheap: "claude-haiku-4-5",
|
|
1215
|
+
max: "claude-fable-5-1",
|
|
1216
|
+
previousStrong: "claude-opus-4-8",
|
|
1217
|
+
previousMid: "claude-sonnet-4-6"
|
|
1218
|
+
},
|
|
1219
|
+
cliAliases: { opus: "claude-opus-5", sonnet: "claude-sonnet-5", haiku: "claude-haiku-4-5", fable: "claude-fable-5-1" },
|
|
1220
|
+
models: {
|
|
1221
|
+
"claude-fable-5-1": { displayName: "Claude Fable 5.1", contextWindow: 1048576, maxOutputTokens: 131072, tier: "max", capabilities: ["api", "metered", "tools", "thinking", "vision", "json"], noTemp: true, thinkingAlwaysOn: true, pricePer1M: { input: 10, output: 50 } },
|
|
1222
|
+
"claude-opus-5": { displayName: "Claude Opus 5", contextWindow: 1048576, maxOutputTokens: 131072, tier: "strong", capabilities: ["api", "metered", "tools", "thinking", "vision", "json"], noTemp: true, pricePer1M: { input: 5, output: 25 } },
|
|
1223
|
+
"claude-sonnet-5": { displayName: "Claude Sonnet 5", contextWindow: 1048576, maxOutputTokens: 131072, tier: "strong", capabilities: ["api", "metered", "tools", "thinking", "vision", "json"], noTemp: true, pricePer1M: { input: 2, output: 10 } },
|
|
1224
|
+
"claude-haiku-4-5": { displayName: "Claude Haiku 4.5", contextWindow: 2e5, maxOutputTokens: 65536, tier: "cheap", capabilities: ["api", "metered", "tools", "thinking", "vision"], pricePer1M: { input: 1, output: 5 } },
|
|
1225
|
+
"claude-opus-4-8": { displayName: "Claude Opus 4.8", contextWindow: 1048576, maxOutputTokens: 131072, tier: "strong", capabilities: ["api", "metered", "tools", "thinking", "vision", "json"], noTemp: true, generation: "previous", pricePer1M: { input: 5, output: 25 } },
|
|
1226
|
+
"claude-sonnet-4-6": { displayName: "Claude Sonnet 4.6", contextWindow: 1048576, maxOutputTokens: 131072, tier: "strong", capabilities: ["api", "metered", "tools", "thinking", "vision", "json"], generation: "previous", pricePer1M: { input: 3, output: 15 } }
|
|
1227
|
+
},
|
|
1228
|
+
aliases: {},
|
|
1229
|
+
retired: [],
|
|
1230
|
+
providerKeys: {
|
|
1231
|
+
claude: { model: "claude-sonnet-4-6", note: "Generic Claude key \u2014 previous generation kept on purpose; see _note." },
|
|
1232
|
+
"claude-opus": { model: "claude-opus-4-8", noTemp: true, note: "Opus key \u2014 previous generation kept on purpose. Old table priced this at $15/$75; the sheet says $5/$25." }
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
openai: {
|
|
1236
|
+
kind: "openai",
|
|
1237
|
+
company: "openai",
|
|
1238
|
+
baseUrl: "https://api.openai.com/v1",
|
|
1239
|
+
keyEnv: "HEYSPEX_OPENAI_API_KEY",
|
|
1240
|
+
source: "carried over from cloud-clients.mjs PROVIDERS (no vendor re-verification in this pass)",
|
|
1241
|
+
verifiedOn: "2026-06-28",
|
|
1242
|
+
roles: { default: "gpt-4o" },
|
|
1243
|
+
models: {
|
|
1244
|
+
"gpt-4o": { contextWindow: 128e3, tier: "strong", capabilities: ["api", "metered", "tools", "vision", "json"], pricePer1M: { input: 2.5, output: 10 } }
|
|
1245
|
+
},
|
|
1246
|
+
aliases: {},
|
|
1247
|
+
retired: [],
|
|
1248
|
+
providerKeys: { openai: { model: "gpt-4o" } }
|
|
1249
|
+
},
|
|
1250
|
+
gemini: {
|
|
1251
|
+
kind: "openai",
|
|
1252
|
+
company: "google",
|
|
1253
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
|
|
1254
|
+
keyEnv: "HEYSPEX_GEMINI_API_KEY",
|
|
1255
|
+
source: "carried over from cloud-clients.mjs PROVIDERS (no vendor re-verification in this pass)",
|
|
1256
|
+
verifiedOn: "2026-06-28",
|
|
1257
|
+
roles: { default: "gemini-2.5-pro", strong: "gemini-2.5-pro" },
|
|
1258
|
+
models: {
|
|
1259
|
+
"gemini-2.5-pro": { contextWindow: 1048576, tier: "strong", capabilities: ["api", "metered", "tools", "vision", "json", "thinking"], pricePer1M: { input: 1.25, output: 10 } }
|
|
1260
|
+
},
|
|
1261
|
+
aliases: {},
|
|
1262
|
+
retired: [],
|
|
1263
|
+
providerKeys: { gemini: { model: "gemini-2.5-pro" } }
|
|
1264
|
+
},
|
|
1265
|
+
openrouter: {
|
|
1266
|
+
kind: "openai",
|
|
1267
|
+
company: "openrouter",
|
|
1268
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
1269
|
+
keyEnv: "OPENROUTER_API_KEY",
|
|
1270
|
+
source: "carried over from cloud-clients.mjs PROVIDERS (OpenRouter list prices as dated per model)",
|
|
1271
|
+
verifiedOn: "2026-07-10",
|
|
1272
|
+
_note: "Same-family fallbacks hosted via OpenRouter (council key resilience, 2026-07-08 dogfood audit): a customer fleet with ONE OpenRouter key can still seat all three decorrelated council families \u2014 the MODEL family is what decorrelates blind spots, not the API host. grok-4.3 is the decorrelated director-seat primary (owner directive 2026-07-10, HS-E1O0); glm-5.2 is the 'beat Opus cheaper' candidate (HS-XNJI), not yet wired into any tier.",
|
|
1273
|
+
roles: { cheap: "openai/gpt-4o-mini", director: "x-ai/grok-4.3", directorFallback: "z-ai/glm-5.2", geminiFallback: "google/gemini-2.5-pro", gpt4oFallback: "openai/gpt-4o" },
|
|
1274
|
+
models: {
|
|
1275
|
+
"openai/gpt-4o-mini": { contextWindow: 128e3, tier: "cheap", capabilities: ["api", "metered", "tools", "json"], pricePer1M: { input: 0.15, output: 0.6 } },
|
|
1276
|
+
"openai/gpt-4o": { contextWindow: 128e3, tier: "strong", capabilities: ["api", "metered", "tools", "vision", "json"], pricePer1M: { input: 2.5, output: 10 } },
|
|
1277
|
+
"google/gemini-2.5-pro": { contextWindow: 1048576, tier: "strong", capabilities: ["api", "metered", "tools", "vision", "json"], pricePer1M: { input: 1.25, output: 10 } },
|
|
1278
|
+
"z-ai/glm-5.2": { displayName: "GLM-5.2 (Z.ai, open-weight 744B)", contextWindow: 2e5, tier: "strong", capabilities: ["api", "metered", "tools", "json"], verifiedOn: "2026-06-30", pricePer1M: { input: 0.95, output: 3 } },
|
|
1279
|
+
"x-ai/grok-4.3": { displayName: "Grok 4.3 (xAI)", contextWindow: 256e3, tier: "strong", capabilities: ["api", "metered", "tools", "json", "thinking", "structured-outputs"], verifiedOn: "2026-07-10", pricePer1M: { input: 1.25, output: 2.5 } }
|
|
1280
|
+
},
|
|
1281
|
+
aliases: {},
|
|
1282
|
+
retired: [],
|
|
1283
|
+
providerKeys: {
|
|
1284
|
+
openrouter: { model: "openai/gpt-4o-mini" },
|
|
1285
|
+
"gemini-openrouter": { model: "google/gemini-2.5-pro" },
|
|
1286
|
+
"gpt4o-openrouter": { model: "openai/gpt-4o" },
|
|
1287
|
+
"glm-5.2": { model: "z-ai/glm-5.2" },
|
|
1288
|
+
"grok-4.3": { model: "x-ai/grok-4.3" }
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
"github-models": {
|
|
1292
|
+
kind: "openai",
|
|
1293
|
+
company: "github",
|
|
1294
|
+
baseUrl: "https://models.github.ai/inference",
|
|
1295
|
+
keyEnv: "HEYSPEX_GITHUB_TOKEN",
|
|
1296
|
+
keyEnvFallback: "GITHUB_TOKEN",
|
|
1297
|
+
source: "GitHub Models bills via Copilot credits; gpt-4o list price mirrored for the ledger (estimate)",
|
|
1298
|
+
verifiedOn: "2026-06-28",
|
|
1299
|
+
_note: "Copilot-ecosystem credits as a cloud tier (HS-32OM fleet onboarding). Model ids are publisher-namespaced.",
|
|
1300
|
+
roles: { default: "openai/gpt-4o" },
|
|
1301
|
+
models: {
|
|
1302
|
+
"openai/gpt-4o": { contextWindow: 128e3, tier: "strong", capabilities: ["api", "metered", "tools", "vision", "json"], pricePer1M: { input: 2.5, output: 10 } }
|
|
1303
|
+
},
|
|
1304
|
+
aliases: {},
|
|
1305
|
+
retired: [],
|
|
1306
|
+
providerKeys: { "github-models": { model: "openai/gpt-4o" } }
|
|
1307
|
+
}
|
|
1308
|
+
},
|
|
1309
|
+
local: {
|
|
1310
|
+
ollama: {
|
|
1311
|
+
kind: "ollama",
|
|
1312
|
+
_comment: "Local fleet models. WHERE each fits (canHost) and WHO uses it (purposes) live in routing-table.json; per-machine speeds live in fleet-manifest.mjs STATIC_SPEEDS. Ids are Ollama tags exactly as `ollama list` prints them. Local models have no PROVIDERS entry \u2014 callModel treats an unknown id as an Ollama id.",
|
|
1313
|
+
roles: {
|
|
1314
|
+
builder: "gpt-oss:20b",
|
|
1315
|
+
coder: "qwen3-coder:30b",
|
|
1316
|
+
judgeFallback: "qwen3-coder:30b",
|
|
1317
|
+
vision: "qwen3.6:27b",
|
|
1318
|
+
small: "qwen3:8b"
|
|
1319
|
+
},
|
|
1320
|
+
models: {
|
|
1321
|
+
"gpt-oss:20b": { contextWindow: 131072, vramRequiredMb: 13800, architecture: "gptoss", capabilities: ["tools", "thinking"] },
|
|
1322
|
+
"qwen3-coder:30b": { contextWindow: 262144, vramRequiredMb: 18600, architecture: "qwen3moe", capabilities: ["tools"] },
|
|
1323
|
+
"qwen3.6:27b": { contextWindow: 262144, vramRequiredMb: 17400, architecture: "qwen35", capabilities: ["tools", "thinking", "vision"] },
|
|
1324
|
+
"qwen2.5-coder:32b": { contextWindow: 131072, vramRequiredMb: 19900, architecture: "qwen2", capabilities: ["tools", "insert"] },
|
|
1325
|
+
"qwen3:8b": { contextWindow: 32768, vramRequiredMb: 5200, architecture: "qwen3", capabilities: ["tools", "thinking"] },
|
|
1326
|
+
"deepseek-r1:70b": { contextWindow: 131072, vramRequiredMb: 42500, architecture: "deepseek", capabilities: ["tools", "thinking"], note: "Q2_K quant \u2014 lower quality than Q4_K_M." },
|
|
1327
|
+
"llama3.3:70b": { contextWindow: 131072, vramRequiredMb: 42500, architecture: "llama3", capabilities: ["tools"] },
|
|
1328
|
+
"VibeThinker-3B": { contextWindow: 8192, vramRequiredMb: 3300, architecture: "qwen2", capabilities: [], note: "Fits anywhere \u226412GB." }
|
|
1329
|
+
},
|
|
1330
|
+
aliases: {},
|
|
1331
|
+
retired: []
|
|
1332
|
+
},
|
|
1333
|
+
vllm: {
|
|
1334
|
+
kind: "openai",
|
|
1335
|
+
_comment: "Night Action vLLM + DSpark pilot (RTX 3060, matched draft `deepseek-ai/dspark_qwen3_8b_block7`). AWQ quant target (bf16 ~16GB does not fit 12GB VRAM alongside the draft; AWQ ~5.6GB does). Reference: HS-XNJI Experiment 1/2. Local, free, no key \u2014 vLLM OpenAI-compat endpoint on port 8000.",
|
|
1336
|
+
baseUrl: "http://192.168.1.252:8000/v1",
|
|
1337
|
+
keyEnv: null,
|
|
1338
|
+
roles: { default: "Qwen/Qwen3-8B-AWQ" },
|
|
1339
|
+
models: {
|
|
1340
|
+
"Qwen/Qwen3-8B-AWQ": { contextWindow: 32768, vramRequiredMb: 5600, architecture: "qwen3", tier: "local", capabilities: ["api", "tools", "thinking"] }
|
|
1341
|
+
},
|
|
1342
|
+
aliases: {},
|
|
1343
|
+
retired: [],
|
|
1344
|
+
providerKeys: { "qwen3-8b-dspark": { model: "Qwen/Qwen3-8B-AWQ", local: true } }
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
|
|
1349
|
+
// dev-factory/lib/model-catalog.mjs
|
|
1350
|
+
var MODEL_CATALOG_PATH = new URL("../model-catalog.json", import.meta.url).href;
|
|
1351
|
+
function loadModelCatalog(doc = (
|
|
1352
|
+
/** @type {any} */
|
|
1353
|
+
model_catalog_default
|
|
1354
|
+
)) {
|
|
1355
|
+
return doc;
|
|
1356
|
+
}
|
|
1357
|
+
var MODEL_CATALOG = Object.freeze(loadModelCatalog());
|
|
1358
|
+
function buildIndex(catalog) {
|
|
1359
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1360
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
1361
|
+
const retired = /* @__PURE__ */ new Map();
|
|
1362
|
+
const providers = /* @__PURE__ */ new Map();
|
|
1363
|
+
for (const [locality, block] of Object.entries({ cloud: catalog.cloud ?? {}, local: catalog.local ?? {} })) {
|
|
1364
|
+
for (const [provider, p] of Object.entries(block)) {
|
|
1365
|
+
if (provider.startsWith("_")) continue;
|
|
1366
|
+
providers.set(provider, { ...p, provider, locality, kind: p.kind ?? locality });
|
|
1367
|
+
for (const [id, m] of Object.entries(p.models ?? {})) {
|
|
1368
|
+
byId.set(id, Object.freeze({ id, kind: locality, provider, tier: m.tier ?? (locality === "local" ? "local" : null), ...m }));
|
|
1369
|
+
}
|
|
1370
|
+
for (const [alias, target] of Object.entries(p.aliases ?? {})) aliases.set(alias, { target, provider, kind: locality });
|
|
1371
|
+
for (const r of p.retired ?? []) retired.set(r.id, { ...r, provider, kind: locality });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return { byId, aliases, retired, providers };
|
|
1375
|
+
}
|
|
1376
|
+
var INDEX = buildIndex(MODEL_CATALOG);
|
|
1377
|
+
var ALL_MODEL_IDS = Object.freeze([...INDEX.byId.keys()]);
|
|
1378
|
+
var MODEL_ALIASES = Object.freeze(Object.fromEntries([...INDEX.aliases].map(([a, v]) => [a, v.target])));
|
|
1379
|
+
var RETIRED_MODEL_IDS = Object.freeze([...INDEX.retired.keys()]);
|
|
1380
|
+
var PROVIDER_NAMES = Object.freeze([...INDEX.providers.keys()]);
|
|
1381
|
+
function resolveModelId(id) {
|
|
1382
|
+
if (typeof id !== "string" || !id) return id;
|
|
1383
|
+
const hit = INDEX.aliases.get(id);
|
|
1384
|
+
return hit ? hit.target : id;
|
|
1385
|
+
}
|
|
1386
|
+
function modelInfo(id) {
|
|
1387
|
+
return INDEX.byId.get(resolveModelId(id)) ?? null;
|
|
1388
|
+
}
|
|
1389
|
+
function providerOf(id) {
|
|
1390
|
+
const m = modelInfo(id);
|
|
1391
|
+
return m ? INDEX.providers.get(m.provider) ?? null : null;
|
|
1392
|
+
}
|
|
1393
|
+
function providerInfo(name) {
|
|
1394
|
+
return INDEX.providers.get(name) ?? null;
|
|
1395
|
+
}
|
|
1396
|
+
function isLocalModel(id) {
|
|
1397
|
+
return modelInfo(id)?.kind === "local";
|
|
1398
|
+
}
|
|
1399
|
+
function roleBlock(name, extra = {}) {
|
|
1400
|
+
const p = INDEX.providers.get(name) ?? {};
|
|
1401
|
+
const roles = p.roles ?? {};
|
|
1402
|
+
const upper = Object.fromEntries(Object.entries(roles).map(([k, v]) => [k.replace(/([a-z])([A-Z])/g, "$1_$2").toUpperCase(), v]));
|
|
1403
|
+
return Object.freeze({
|
|
1404
|
+
provider: name,
|
|
1405
|
+
kind: p.kind ?? null,
|
|
1406
|
+
company: p.company ?? name,
|
|
1407
|
+
baseUrl: p.baseUrl ?? null,
|
|
1408
|
+
keyEnv: p.keyEnv ?? null,
|
|
1409
|
+
keyEnvFallback: p.keyEnvFallback ?? null,
|
|
1410
|
+
verifiedOn: p.verifiedOn ?? MODEL_CATALOG.verified,
|
|
1411
|
+
roles: Object.freeze({ ...roles }),
|
|
1412
|
+
ids: Object.freeze(Object.keys(p.models ?? {})),
|
|
1413
|
+
aliases: Object.freeze({ ...p.aliases ?? {} }),
|
|
1414
|
+
retired: Object.freeze((p.retired ?? []).map((r) => r.id)),
|
|
1415
|
+
...upper,
|
|
1416
|
+
// e.g. roles.chatDefault → CHAT_DEFAULT, roles.cheap → CHEAP
|
|
1417
|
+
...extra
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
var DS = MODEL_CATALOG.cloud?.deepseek ?? {};
|
|
1421
|
+
var DEEPSEEK = roleBlock("deepseek", {
|
|
1422
|
+
FLASH: DS.roles?.cheap,
|
|
1423
|
+
PRO: DS.roles?.strong,
|
|
1424
|
+
anthropicBaseUrl: DS.anthropicBaseUrl ?? null,
|
|
1425
|
+
peakHoursUtc: DS.peakHoursUtc ?? null
|
|
1426
|
+
});
|
|
1427
|
+
var ANTHROPIC = roleBlock("anthropic", { cliAliases: Object.freeze({ ...MODEL_CATALOG.cloud?.anthropic?.cliAliases ?? {} }) });
|
|
1428
|
+
var OPENAI = roleBlock("openai");
|
|
1429
|
+
var GEMINI = roleBlock("gemini");
|
|
1430
|
+
var OPENROUTER = roleBlock("openrouter");
|
|
1431
|
+
var GITHUB_MODELS = roleBlock("github-models");
|
|
1432
|
+
var OLLAMA = roleBlock("ollama");
|
|
1433
|
+
var VLLM = roleBlock("vllm");
|
|
1434
|
+
function isDeepSeekPeak(date = /* @__PURE__ */ new Date()) {
|
|
1435
|
+
const p = DEEPSEEK.peakHoursUtc;
|
|
1436
|
+
if (!p) return true;
|
|
1437
|
+
const day = date.getUTCDay();
|
|
1438
|
+
if (Array.isArray(p.days) && !p.days.includes(day)) return false;
|
|
1439
|
+
const mins = date.getUTCHours() * 60 + date.getUTCMinutes();
|
|
1440
|
+
const toMin = (hhmm) => {
|
|
1441
|
+
const [h, m] = String(hhmm).split(":").map(Number);
|
|
1442
|
+
return h * 60 + (m || 0);
|
|
1443
|
+
};
|
|
1444
|
+
return (p.ranges ?? []).some(([a, b]) => mins >= toMin(a) && mins < toMin(b));
|
|
1445
|
+
}
|
|
1446
|
+
function deepseekPricePer1M(id, { cacheHit = false, peak = null, at = /* @__PURE__ */ new Date() } = {}) {
|
|
1447
|
+
const m = modelInfo(id);
|
|
1448
|
+
if (!m || m.provider !== "deepseek" || !m.pricePer1M?.peak) return null;
|
|
1449
|
+
const corner = peak ?? isDeepSeekPeak(at) ? m.pricePer1M.peak : m.pricePer1M.offPeak;
|
|
1450
|
+
return { input: cacheHit ? corner.cacheHit : corner.cacheMiss, output: corner.output };
|
|
1451
|
+
}
|
|
1452
|
+
function listPricePer1M(id) {
|
|
1453
|
+
const m = modelInfo(id);
|
|
1454
|
+
if (!m) return null;
|
|
1455
|
+
if (m.kind === "local") return { input: 0, output: 0 };
|
|
1456
|
+
if (m.provider === "deepseek") return deepseekPricePer1M(id, { peak: true });
|
|
1457
|
+
if (m.pricePer1M && Number.isFinite(m.pricePer1M.input)) return { input: m.pricePer1M.input, output: m.pricePer1M.output };
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
function providerEntries() {
|
|
1461
|
+
const out = {};
|
|
1462
|
+
for (const [name, p] of INDEX.providers) {
|
|
1463
|
+
if (!p.providerKeys || !p.baseUrl) continue;
|
|
1464
|
+
const base = { kind: p.kind ?? "openai", baseUrl: p.baseUrl, keyEnv: p.keyEnv ?? null };
|
|
1465
|
+
if (p.keyEnvFallback) base.keyEnvFallback = p.keyEnvFallback;
|
|
1466
|
+
if (p.company && p.company !== name) base.company = p.company;
|
|
1467
|
+
if (p.betaBaseUrl) base.betaBaseUrl = p.betaBaseUrl;
|
|
1468
|
+
if (p.userIdField) base.userIdField = p.userIdField;
|
|
1469
|
+
const entryFor = (modelId, overrides = {}) => {
|
|
1470
|
+
const target = resolveModelId(modelId);
|
|
1471
|
+
const m = modelInfo(target);
|
|
1472
|
+
const price = listPricePer1M(target) ?? { input: 0, output: 0 };
|
|
1473
|
+
const e = { ...base, model: modelId, priceIn: price.input, priceOut: price.output, tier: m?.tier ?? (p.locality === "local" ? "local" : "strong") };
|
|
1474
|
+
if (m?.kind === "local" || overrides.local) e.local = true;
|
|
1475
|
+
if (m?.noTemp || overrides.noTemp) e.noTemp = true;
|
|
1476
|
+
if (overrides.legacyAliasOf) e.legacyAliasOf = overrides.legacyAliasOf;
|
|
1477
|
+
return e;
|
|
1478
|
+
};
|
|
1479
|
+
for (const [key, spec] of Object.entries(p.providerKeys)) out[key] = entryFor(spec.model, spec);
|
|
1480
|
+
if (p.emitAliasesAsProviderKeys) {
|
|
1481
|
+
for (const [alias, target] of Object.entries(p.aliases ?? {})) {
|
|
1482
|
+
if (out[alias] || Object.values(p.providerKeys).some((s) => s.model === alias)) continue;
|
|
1483
|
+
out[alias] = entryFor(target, { legacyAliasOf: target });
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return out;
|
|
1488
|
+
}
|
|
1489
|
+
function fleetManifestModelRows() {
|
|
1490
|
+
const contextWindows = {};
|
|
1491
|
+
const costEstimates = {};
|
|
1492
|
+
for (const id of ALL_MODEL_IDS) {
|
|
1493
|
+
const m = modelInfo(id);
|
|
1494
|
+
contextWindows[id] = {
|
|
1495
|
+
contextWindow: m.contextWindow,
|
|
1496
|
+
vramRequiredMb: m.vramRequiredMb ?? null,
|
|
1497
|
+
architecture: m.architecture ?? m.provider,
|
|
1498
|
+
capabilities: m.capabilities ?? []
|
|
1499
|
+
};
|
|
1500
|
+
const price = listPricePer1M(id);
|
|
1501
|
+
if (m.kind === "local") {
|
|
1502
|
+
costEstimates[id] = { kind: "free", note: `local model, zero API cost.${m.note ? " " + m.note : ""}` };
|
|
1503
|
+
} else if (m.provider === "deepseek") {
|
|
1504
|
+
costEstimates[id] = {
|
|
1505
|
+
kind: "metered",
|
|
1506
|
+
usdPerMInput: price.input,
|
|
1507
|
+
usdPerMOutput: price.output,
|
|
1508
|
+
note: `${m.displayName ?? id}: peak cache-miss list rates (model-catalog.json, verified ${providerInfo("deepseek")?.verifiedOn}); off-peak halves, cache hits ~50x cheaper`
|
|
1509
|
+
};
|
|
1510
|
+
} else if (price) {
|
|
1511
|
+
costEstimates[id] = {
|
|
1512
|
+
kind: "metered",
|
|
1513
|
+
usdPerMInput: price.input,
|
|
1514
|
+
usdPerMOutput: price.output,
|
|
1515
|
+
note: `${m.displayName ?? id}: list rates (model-catalog.json, verified ${providerOf(id)?.verifiedOn ?? MODEL_CATALOG.verified})`
|
|
1516
|
+
};
|
|
1517
|
+
} else {
|
|
1518
|
+
costEstimates[id] = { kind: "metered", note: m.note ?? "pricing not recorded in model-catalog.json" };
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return { contextWindows, costEstimates };
|
|
1522
|
+
}
|
|
1523
|
+
function runSelfTest({ log = console.log } = {}) {
|
|
1524
|
+
let ok = true;
|
|
1525
|
+
const check = (name, cond) => {
|
|
1526
|
+
log(` ${cond ? "\u2713" : "\u2717"} ${name}`);
|
|
1527
|
+
if (!cond) ok = false;
|
|
1528
|
+
};
|
|
1529
|
+
log("== model-catalog self-test ==\n");
|
|
1530
|
+
check("catalog has a verified date", /^\d{4}-\d{2}-\d{2}$/.test(String(MODEL_CATALOG.verified)));
|
|
1531
|
+
check(
|
|
1532
|
+
"every provider role points at a current id of that provider",
|
|
1533
|
+
[...INDEX.providers.values()].every((p) => Object.values(p.roles ?? {}).every((id) => id in (p.models ?? {}) || (p.aliases ?? {})[id]))
|
|
1534
|
+
);
|
|
1535
|
+
check(
|
|
1536
|
+
"every providerKeys.model is a current id or a declared alias of that provider",
|
|
1537
|
+
[...INDEX.providers.values()].every((p) => Object.values(p.providerKeys ?? {}).every((s) => s.model in (p.models ?? {}) || (p.aliases ?? {})[s.model]))
|
|
1538
|
+
);
|
|
1539
|
+
check(
|
|
1540
|
+
"no alias target is itself an alias or retired",
|
|
1541
|
+
Object.values(MODEL_ALIASES).every((t) => INDEX.byId.has(t) && !INDEX.retired.has(t))
|
|
1542
|
+
);
|
|
1543
|
+
check(
|
|
1544
|
+
"every retired id has an alias to its successor",
|
|
1545
|
+
RETIRED_MODEL_IDS.every((id) => MODEL_ALIASES[id])
|
|
1546
|
+
);
|
|
1547
|
+
check(
|
|
1548
|
+
"no current id is also listed as retired",
|
|
1549
|
+
ALL_MODEL_IDS.every((id) => !INDEX.retired.has(id))
|
|
1550
|
+
);
|
|
1551
|
+
check(
|
|
1552
|
+
"every model has a positive context window",
|
|
1553
|
+
ALL_MODEL_IDS.every((id) => modelInfo(id).contextWindow > 0)
|
|
1554
|
+
);
|
|
1555
|
+
check(
|
|
1556
|
+
"every cloud model has a list price",
|
|
1557
|
+
ALL_MODEL_IDS.filter((id) => !isLocalModel(id)).every((id) => listPricePer1M(id)?.input > 0 && listPricePer1M(id)?.output > 0)
|
|
1558
|
+
);
|
|
1559
|
+
check(
|
|
1560
|
+
"DeepSeek prices are four-corner, positive, and off-peak is half of peak",
|
|
1561
|
+
DEEPSEEK.ids.every((id) => {
|
|
1562
|
+
const p = modelInfo(id).pricePer1M;
|
|
1563
|
+
return ["cacheHit", "cacheMiss", "output"].every((k) => p?.peak?.[k] > 0 && Math.abs(p.peak[k] - 2 * p.offPeak[k]) < 1e-9);
|
|
1564
|
+
})
|
|
1565
|
+
);
|
|
1566
|
+
check(
|
|
1567
|
+
"every local model has a VRAM figure",
|
|
1568
|
+
ALL_MODEL_IDS.filter(isLocalModel).every((id) => Number.isFinite(modelInfo(id).vramRequiredMb))
|
|
1569
|
+
);
|
|
1570
|
+
check(
|
|
1571
|
+
"resolveModelId maps a retired id to its successor",
|
|
1572
|
+
RETIRED_MODEL_IDS.every((id) => ALL_MODEL_IDS.includes(resolveModelId(id)))
|
|
1573
|
+
);
|
|
1574
|
+
check("providerEntries covers every providerKey and every DeepSeek alias, with prices", (() => {
|
|
1575
|
+
const e = providerEntries();
|
|
1576
|
+
const keys = [...INDEX.providers.values()].flatMap((p) => Object.keys(p.providerKeys ?? {}));
|
|
1577
|
+
return keys.every((k) => e[k]?.model && e[k].baseUrl && Number.isFinite(e[k].priceIn)) && Object.keys(DEEPSEEK.aliases).every((a) => e[a]?.legacyAliasOf || e.deepseek?.model === a);
|
|
1578
|
+
})());
|
|
1579
|
+
check(
|
|
1580
|
+
"isDeepSeekPeak: Tue 02:30 UTC is peak, Sat 02:30 UTC is not, Tue 12:00 UTC is not",
|
|
1581
|
+
isDeepSeekPeak(/* @__PURE__ */ new Date("2026-09-15T02:30:00Z")) && !isDeepSeekPeak(/* @__PURE__ */ new Date("2026-09-19T02:30:00Z")) && !isDeepSeekPeak(/* @__PURE__ */ new Date("2026-09-15T12:00:00Z"))
|
|
1582
|
+
);
|
|
1583
|
+
check(
|
|
1584
|
+
"fleetManifestModelRows covers every catalog id",
|
|
1585
|
+
Object.keys(fleetManifestModelRows().contextWindows).length === ALL_MODEL_IDS.length
|
|
1586
|
+
);
|
|
1587
|
+
check(
|
|
1588
|
+
"Claude CLI alias words resolve to current Anthropic ids",
|
|
1589
|
+
Object.values(ANTHROPIC.cliAliases).every((id) => ANTHROPIC.ids.includes(id))
|
|
1590
|
+
);
|
|
1591
|
+
log(`
|
|
1592
|
+
${ok ? "\u2713 self-test PASS \u2014 catalog invariants verified." : "\u2717 self-test FAIL"}`);
|
|
1593
|
+
return ok ? 0 : 1;
|
|
1594
|
+
}
|
|
1595
|
+
var isMain = typeof process !== "undefined" && Array.isArray(process.argv) && Boolean(process.argv[1]) && /model-catalog\.mjs$/.test(String(process.argv[1]).replace(/\\/g, "/"));
|
|
1596
|
+
if (isMain) {
|
|
1597
|
+
const argv = process.argv.slice(2);
|
|
1598
|
+
if (argv.includes("--self-test")) process.exit(runSelfTest());
|
|
1599
|
+
if (argv.includes("--print")) {
|
|
1600
|
+
console.log(JSON.stringify({ DEEPSEEK, ANTHROPIC, OPENAI, GEMINI, OPENROUTER, GITHUB_MODELS, OLLAMA, VLLM, ALL_MODEL_IDS, MODEL_ALIASES, RETIRED_MODEL_IDS, providerEntries: providerEntries() }, null, 2));
|
|
1601
|
+
process.exit(0);
|
|
1602
|
+
}
|
|
1603
|
+
if (argv.length) {
|
|
1604
|
+
console.error("usage: node dev-factory/lib/model-catalog.mjs --self-test | --print");
|
|
1605
|
+
process.exit(64);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// spex-console/src/lib/model-catalog.ts
|
|
1610
|
+
var ANTHROPIC2 = ANTHROPIC;
|
|
1611
|
+
var OLLAMA2 = OLLAMA;
|
|
1612
|
+
|
|
1613
|
+
// spex-console/src/lib/vault-id.ts
|
|
1614
|
+
var FILE_KEY_RE = /^(?=[0-9]*[A-Z])[A-Z0-9]{4,}$/;
|
|
1615
|
+
function normalizeBareSpecIdToken(input) {
|
|
1616
|
+
const token = String(input ?? "").trim().toUpperCase();
|
|
1617
|
+
if (!token) return null;
|
|
1618
|
+
if (token.startsWith("HS-")) return token;
|
|
1619
|
+
const head = token.split("-")[0];
|
|
1620
|
+
return FILE_KEY_RE.test(head) ? `HS-${token}` : null;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// spex-console/src/lib/launch-preferences.ts
|
|
1624
|
+
var NEXT_LAUNCH_KINDS = ["milestone", "epic", "sprint", "task"];
|
|
1625
|
+
function isNextLaunchKind(value) {
|
|
1626
|
+
return NEXT_LAUNCH_KINDS.includes(value);
|
|
1627
|
+
}
|
|
1628
|
+
function launchUseCaseForKind(kind) {
|
|
1629
|
+
return kind === "task" ? "task-implementation" : kind;
|
|
1630
|
+
}
|
|
1631
|
+
var USE_CASE_SETTINGS_KEY = {
|
|
1632
|
+
"task-creation": "taskCreation",
|
|
1633
|
+
"task-implementation": "taskImplementation",
|
|
1634
|
+
handoff: "handoff",
|
|
1635
|
+
milestone: "milestone",
|
|
1636
|
+
epic: "epic",
|
|
1637
|
+
sprint: "sprint"
|
|
1638
|
+
};
|
|
1639
|
+
function launchSettingsBucketKey(useCase) {
|
|
1640
|
+
return USE_CASE_SETTINGS_KEY[useCase];
|
|
1641
|
+
}
|
|
1642
|
+
var LAUNCH_SETTINGS_BUCKET_KEYS = Object.values(
|
|
1643
|
+
USE_CASE_SETTINGS_KEY
|
|
1644
|
+
);
|
|
1645
|
+
var DEFAULT_LAUNCH_MODEL_PREFERENCES = {
|
|
1646
|
+
// Every Claude default is a version-less ALIAS, never a pinned id (owner
|
|
1647
|
+
// ruling 2026-09-01: "I don't like seeing these numbers hard-set"). Claude
|
|
1648
|
+
// renumbers a family every few months, and a pinned default silently keeps
|
|
1649
|
+
// opening last season's model until someone notices and edits this file.
|
|
1650
|
+
// An alias resolves to the newest id in its family at launch time
|
|
1651
|
+
// (`launch-model-aliases.ts`), so the shipped default never goes stale.
|
|
1652
|
+
// Anyone who WANTS a pinned id can still set one — `hs next model epic
|
|
1653
|
+
// claude-opus-4-8` stores it byte-for-byte. Non-Claude values have no alias
|
|
1654
|
+
// vocabulary and stay exact: `composer-2.5-fast` is a Cursor model, and
|
|
1655
|
+
// `qwen3-coder:30b` a local fleet one.
|
|
1656
|
+
taskCreation: {
|
|
1657
|
+
claudeCode: "claude-opus",
|
|
1658
|
+
cursorAgent: "claude-opus",
|
|
1659
|
+
fleet: OLLAMA2.CODER,
|
|
1660
|
+
defaultAgent: "claude-code"
|
|
1661
|
+
},
|
|
1662
|
+
taskImplementation: {
|
|
1663
|
+
claudeCode: "claude-sonnet",
|
|
1664
|
+
cursorAgent: "composer-2.5-fast",
|
|
1665
|
+
fleet: OLLAMA2.CODER,
|
|
1666
|
+
defaultAgent: "claude-code"
|
|
1667
|
+
},
|
|
1668
|
+
// A handoff resume re-enters existing work — same weight class as task
|
|
1669
|
+
// implementation (Sonnet), Claude Code only by default (no --tool flag on
|
|
1670
|
+
// `hs handoff`). Settings key: `launchModels.handoff.*`.
|
|
1671
|
+
handoff: {
|
|
1672
|
+
claudeCode: "claude-sonnet",
|
|
1673
|
+
cursorAgent: "composer-2.5-fast",
|
|
1674
|
+
fleet: OLLAMA2.CODER,
|
|
1675
|
+
defaultAgent: "claude-code"
|
|
1676
|
+
},
|
|
1677
|
+
// Launching a milestone, epic or sprint means BREAKING IT DOWN into child
|
|
1678
|
+
// work, not writing code — the same job `taskCreation` already weights at
|
|
1679
|
+
// Opus, so these three inherit that weight class rather than task
|
|
1680
|
+
// implementation's Sonnet. Settings keys: `launchModels.{milestone,epic,sprint}.*`.
|
|
1681
|
+
milestone: {
|
|
1682
|
+
claudeCode: "claude-opus",
|
|
1683
|
+
cursorAgent: "claude-opus",
|
|
1684
|
+
fleet: OLLAMA2.CODER,
|
|
1685
|
+
defaultAgent: "claude-code"
|
|
1686
|
+
},
|
|
1687
|
+
epic: {
|
|
1688
|
+
claudeCode: "claude-opus",
|
|
1689
|
+
cursorAgent: "claude-opus",
|
|
1690
|
+
fleet: OLLAMA2.CODER,
|
|
1691
|
+
defaultAgent: "claude-code"
|
|
1692
|
+
},
|
|
1693
|
+
sprint: {
|
|
1694
|
+
claudeCode: "claude-opus",
|
|
1695
|
+
cursorAgent: "claude-opus",
|
|
1696
|
+
fleet: OLLAMA2.CODER,
|
|
1697
|
+
defaultAgent: "claude-code"
|
|
1698
|
+
},
|
|
1699
|
+
closeTerminalOnTaskComplete: true,
|
|
1700
|
+
autoClaimOnLaunch: false,
|
|
1701
|
+
// Handheld is the safest default (human in the loop); mvp matches the active
|
|
1702
|
+
// operating mode. Both are user-settable in Settings.
|
|
1703
|
+
defaultRunMode: "handheld",
|
|
1704
|
+
defaultReviewProfile: "mvp"
|
|
1705
|
+
};
|
|
1706
|
+
var CLAUDE_LAUNCH_MODEL_OPTIONS = [
|
|
1707
|
+
{ id: ANTHROPIC2.STRONG, label: "Claude Opus 5" },
|
|
1708
|
+
{ id: ANTHROPIC2.MID, label: "Claude Sonnet 5" },
|
|
1709
|
+
{ id: "claude-fable-5", label: "Claude Fable 5" },
|
|
1710
|
+
{ id: ANTHROPIC2.PREVIOUS_STRONG, label: "Claude Opus 4.8" },
|
|
1711
|
+
{ id: ANTHROPIC2.PREVIOUS_MID, label: "Claude Sonnet 4.6" },
|
|
1712
|
+
{ id: ANTHROPIC2.CHEAP, label: "Claude Haiku 4.5" }
|
|
1713
|
+
];
|
|
1714
|
+
var CURSOR_LAUNCH_MODEL_OPTIONS = [
|
|
1715
|
+
{ id: "auto", label: "Auto" },
|
|
1716
|
+
{ id: "composer-2.5-fast", label: "Composer 2.5 Fast" },
|
|
1717
|
+
{ id: ANTHROPIC2.PREVIOUS_STRONG, label: "Claude Opus 4.8 (via Cursor)" },
|
|
1718
|
+
{ id: ANTHROPIC2.PREVIOUS_MID, label: "Claude Sonnet 4.6 (via Cursor)" }
|
|
1719
|
+
];
|
|
1720
|
+
var CLAUDE_PERMISSION_MODE_OPTIONS = [
|
|
1721
|
+
"acceptEdits",
|
|
1722
|
+
"auto",
|
|
1723
|
+
"bypassPermissions",
|
|
1724
|
+
"manual",
|
|
1725
|
+
"dontAsk",
|
|
1726
|
+
"plan"
|
|
1727
|
+
];
|
|
1728
|
+
var CURSOR_PERMISSION_MODE_OPTIONS = [];
|
|
1729
|
+
|
|
1730
|
+
// spex-console/src/lib/model-registry/types.ts
|
|
1731
|
+
function isLaunchBackend(v) {
|
|
1732
|
+
return v === "anthropic" || v === "ollama" || v === "deepseek";
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// spex-console/src/lib/cli/backend.ts
|
|
1736
|
+
var LAUNCH_BACKENDS = ["anthropic", "ollama", "deepseek"];
|
|
1737
|
+
var BACKEND_VALID_SET_MESSAGE = `valid backends: ${LAUNCH_BACKENDS.join(", ")}`;
|
|
1738
|
+
function isKnownBackendValue(raw) {
|
|
1739
|
+
return isLaunchBackend(raw);
|
|
1740
|
+
}
|
|
1741
|
+
function unknownBackendMessage(raw) {
|
|
1742
|
+
return `unknown backend "${raw}" \u2014 ${BACKEND_VALID_SET_MESSAGE}`;
|
|
1743
|
+
}
|
|
1744
|
+
function maskBackendEnvValue(key, value) {
|
|
1745
|
+
if (key !== "ANTHROPIC_API_KEY") return value;
|
|
1746
|
+
const last4 = value.length > 4 ? value.slice(-4) : value;
|
|
1747
|
+
return `****${last4}`;
|
|
1748
|
+
}
|
|
1749
|
+
async function resolveKindBackend(kind, deps) {
|
|
1750
|
+
const res = await controlRequest(deps, {
|
|
1751
|
+
method: "POST",
|
|
1752
|
+
path: "/api/agent/start",
|
|
1753
|
+
body: { prompt: "/hs-next", launchUseCase: launchUseCaseForKind(kind), dryRun: true }
|
|
1754
|
+
});
|
|
1755
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
1756
|
+
if (res.status !== 200) return { ok: false, error: writeFailed(res.status, res.body) };
|
|
1757
|
+
const c = res.body ?? {};
|
|
1758
|
+
return {
|
|
1759
|
+
ok: true,
|
|
1760
|
+
value: {
|
|
1761
|
+
kind,
|
|
1762
|
+
agent: typeof c.agent === "string" ? c.agent : "claude-code",
|
|
1763
|
+
backend: typeof c.backend === "string" ? c.backend : "anthropic",
|
|
1764
|
+
backendSource: typeof c.backendSource === "string" ? c.backendSource : "?"
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
async function resolveAllKindsBackend(deps) {
|
|
1769
|
+
const out = [];
|
|
1770
|
+
for (const kind of NEXT_LAUNCH_KINDS) {
|
|
1771
|
+
const r = await resolveKindBackend(kind, deps);
|
|
1772
|
+
if (!r.ok) return r;
|
|
1773
|
+
out.push(r.value);
|
|
1774
|
+
}
|
|
1775
|
+
return { ok: true, value: out };
|
|
1776
|
+
}
|
|
1777
|
+
async function resolveBackendRows(kind, deps) {
|
|
1778
|
+
if (kind) {
|
|
1779
|
+
const one = await resolveKindBackend(kind, deps);
|
|
1780
|
+
return one.ok ? { ok: true, rows: [one.value] } : one;
|
|
1781
|
+
}
|
|
1782
|
+
const all = await resolveAllKindsBackend(deps);
|
|
1783
|
+
return all.ok ? { ok: true, rows: all.value } : all;
|
|
1784
|
+
}
|
|
1785
|
+
async function runNextBackend(args, deps) {
|
|
1786
|
+
if (args.value === void 0) {
|
|
1787
|
+
const resolved = await resolveBackendRows(args.kind, deps);
|
|
1788
|
+
if (!resolved.ok) return resolved.error;
|
|
1789
|
+
return {
|
|
1790
|
+
code: EXIT.OK,
|
|
1791
|
+
message: resolved.rows.map(
|
|
1792
|
+
(r) => `${r.kind.padEnd(9)} backend: ${r.backend} (source: ${r.backendSource}, agent: ${r.agent})`
|
|
1793
|
+
).join("\n"),
|
|
1794
|
+
payload: { ok: true, kinds: resolved.rows },
|
|
1795
|
+
isError: false
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
if (!args.kind) {
|
|
1799
|
+
return {
|
|
1800
|
+
code: EXIT.USAGE,
|
|
1801
|
+
message: `hs next backend needs a kind before a value \u2014 which kind of launch is "${args.value}" for? e.g. hs next backend epic ${args.value} (kinds: ${NEXT_LAUNCH_KINDS.join(", ")})`,
|
|
1802
|
+
payload: { ok: false, error: "kind_required", value: args.value },
|
|
1803
|
+
isError: true
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (!isKnownBackendValue(args.value)) {
|
|
1807
|
+
return {
|
|
1808
|
+
code: EXIT.INVALID_ARG,
|
|
1809
|
+
message: unknownBackendMessage(args.value),
|
|
1810
|
+
payload: { ok: false, error: "invalid_backend", value: args.value, kind: args.kind },
|
|
1811
|
+
isError: true
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
const settingsKey = launchSettingsBucketKey(launchUseCaseForKind(args.kind));
|
|
1815
|
+
const res = await controlRequest(deps, {
|
|
1816
|
+
method: "PUT",
|
|
1817
|
+
path: "/api/settings",
|
|
1818
|
+
body: { launchModels: { [settingsKey]: { claudeCodeBackend: args.value } } }
|
|
1819
|
+
});
|
|
1820
|
+
if (!res.ok) return res.error;
|
|
1821
|
+
if (res.status !== 200) return writeFailed(res.status, res.body);
|
|
1822
|
+
return {
|
|
1823
|
+
code: EXIT.OK,
|
|
1824
|
+
message: `${args.kind} default backend set -> ${args.value}.`,
|
|
1825
|
+
payload: { ok: true, kind: args.kind, backend: args.value },
|
|
1826
|
+
isError: false
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// spex-console/src/lib/cli/next.ts
|
|
1831
|
+
var ALL_LAUNCH_MODEL_OPTIONS = [...CLAUDE_LAUNCH_MODEL_OPTIONS, ...CURSOR_LAUNCH_MODEL_OPTIONS];
|
|
1832
|
+
var ALL_LAUNCH_PERMISSION_MODE_OPTIONS = [
|
|
1833
|
+
...CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
1834
|
+
...CURSOR_PERMISSION_MODE_OPTIONS
|
|
1835
|
+
];
|
|
1836
|
+
function isKnownModelValue(value, options) {
|
|
1837
|
+
return options.some((o) => o.id === value) || isModelAliasValue(value);
|
|
1838
|
+
}
|
|
1839
|
+
function validModelOptionsList(options) {
|
|
1840
|
+
return [...options.map((o) => o.id), ...MODEL_ALIAS_INPUT_FORMS];
|
|
1841
|
+
}
|
|
1842
|
+
function isKnownPermissionModeValue(value, options) {
|
|
1843
|
+
return options.includes(value);
|
|
1844
|
+
}
|
|
1845
|
+
var FRONT_TOKENS = {
|
|
1846
|
+
handheld: "handheld",
|
|
1847
|
+
assist: "assisted",
|
|
1848
|
+
assisted: "assisted",
|
|
1849
|
+
auto: "autonomous",
|
|
1850
|
+
autonomous: "autonomous"
|
|
1851
|
+
};
|
|
1852
|
+
function buildNextPrompt(args) {
|
|
1853
|
+
const parts = ["/hs-next"];
|
|
1854
|
+
if (args.front && args.front !== "handheld") parts.push(args.front);
|
|
1855
|
+
if (args.specId) parts.push(args.specId);
|
|
1856
|
+
return parts.join(" ");
|
|
1857
|
+
}
|
|
1858
|
+
function validateLaunchFlags(args) {
|
|
1859
|
+
if (args.dryRun && !args.launch) {
|
|
1860
|
+
return { code: EXIT.USAGE, message: "--dry-run requires --launch" };
|
|
1861
|
+
}
|
|
1862
|
+
if (args.force && !args.launch) {
|
|
1863
|
+
return {
|
|
1864
|
+
code: EXIT.USAGE,
|
|
1865
|
+
message: "--force requires --launch (e.g. hs next <id> --launch --force)"
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
if (args.autoClaim !== void 0 && !args.launch) {
|
|
1869
|
+
return {
|
|
1870
|
+
code: EXIT.USAGE,
|
|
1871
|
+
message: "--claim/--no-claim require --launch (set the default with: hs next claim on|off)"
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
if (args.modelOverride !== void 0 && !args.launch) {
|
|
1875
|
+
return {
|
|
1876
|
+
code: EXIT.USAGE,
|
|
1877
|
+
message: "--model requires --launch (e.g. hs next <id> --model <value> --launch)"
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
if (args.modelOverride !== void 0 && !isKnownModelValue(args.modelOverride, ALL_LAUNCH_MODEL_OPTIONS)) {
|
|
1881
|
+
return {
|
|
1882
|
+
code: EXIT.INVALID_ARG,
|
|
1883
|
+
message: `unknown model "${args.modelOverride}". Valid options: ${validModelOptionsList(ALL_LAUNCH_MODEL_OPTIONS).join(", ")}`
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
if (args.permissionModeOverride !== void 0 && !args.launch) {
|
|
1887
|
+
return {
|
|
1888
|
+
code: EXIT.USAGE,
|
|
1889
|
+
message: "--permission-mode requires --launch (e.g. hs next <id> --permission-mode <mode> --launch)"
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
if (args.permissionModeOverride !== void 0 && !isKnownPermissionModeValue(args.permissionModeOverride, ALL_LAUNCH_PERMISSION_MODE_OPTIONS)) {
|
|
1893
|
+
return {
|
|
1894
|
+
code: EXIT.INVALID_ARG,
|
|
1895
|
+
message: `unknown permission mode "${args.permissionModeOverride}". Valid modes: ${ALL_LAUNCH_PERMISSION_MODE_OPTIONS.join(", ")}`
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
if (args.backendOverride !== void 0 && !args.launch) {
|
|
1899
|
+
return {
|
|
1900
|
+
code: EXIT.USAGE,
|
|
1901
|
+
message: "--backend requires --launch (e.g. hs next <id> --backend <value> --launch)"
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
if (args.backendOverride !== void 0 && !isKnownBackendValue(args.backendOverride)) {
|
|
1905
|
+
return {
|
|
1906
|
+
code: EXIT.INVALID_ARG,
|
|
1907
|
+
message: unknownBackendMessage(args.backendOverride)
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
function parseDefaultsTail(sub, argv) {
|
|
1913
|
+
const args = { sub, launch: false, json: false };
|
|
1914
|
+
const positionals = [];
|
|
1915
|
+
for (let i = 1; i < argv.length; i++) {
|
|
1916
|
+
const arg = argv[i];
|
|
1917
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
1918
|
+
else if (arg === "--json") args.json = true;
|
|
1919
|
+
else if (arg.startsWith("-")) {
|
|
1920
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
1921
|
+
} else positionals.push(arg);
|
|
1922
|
+
}
|
|
1923
|
+
if (positionals.length > 2) {
|
|
1924
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${positionals[2]}` } };
|
|
1925
|
+
}
|
|
1926
|
+
const [first, second] = positionals;
|
|
1927
|
+
if (first !== void 0) {
|
|
1928
|
+
if (!isNextLaunchKind(first)) {
|
|
1929
|
+
return {
|
|
1930
|
+
error: {
|
|
1931
|
+
code: EXIT.USAGE,
|
|
1932
|
+
message: `"${first}" is not a launch kind. Usage: hs next ${sub} [<kind>] [<value>] \u2014 kinds: ${NEXT_LAUNCH_KINDS.join(", ")}`
|
|
1933
|
+
}
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
args.kind = first;
|
|
1937
|
+
}
|
|
1938
|
+
if (second !== void 0) {
|
|
1939
|
+
if (sub === "model") args.modelValue = second;
|
|
1940
|
+
else args.permissionModeValue = second;
|
|
1941
|
+
}
|
|
1942
|
+
return { args };
|
|
1943
|
+
}
|
|
1944
|
+
function parseBackendTail(argv) {
|
|
1945
|
+
const args = { sub: "backend", launch: false, json: false };
|
|
1946
|
+
const positionals = [];
|
|
1947
|
+
for (let i = 1; i < argv.length; i++) {
|
|
1948
|
+
const arg = argv[i];
|
|
1949
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
1950
|
+
else if (arg === "--json") args.json = true;
|
|
1951
|
+
else if (arg.startsWith("-")) {
|
|
1952
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
1953
|
+
} else positionals.push(arg);
|
|
1954
|
+
}
|
|
1955
|
+
if (positionals.length > 2) {
|
|
1956
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${positionals[2]}` } };
|
|
1957
|
+
}
|
|
1958
|
+
const [first, second] = positionals;
|
|
1959
|
+
if (first !== void 0) {
|
|
1960
|
+
if (!isNextLaunchKind(first)) {
|
|
1961
|
+
return {
|
|
1962
|
+
error: {
|
|
1963
|
+
code: EXIT.USAGE,
|
|
1964
|
+
message: `"${first}" is not a launch kind. Usage: hs next backend [<kind>] [<value>] \u2014 kinds: ${NEXT_LAUNCH_KINDS.join(", ")}`
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
args.kind = first;
|
|
1969
|
+
}
|
|
1970
|
+
if (second !== void 0) args.backendValue = second;
|
|
1971
|
+
return { args };
|
|
1972
|
+
}
|
|
1973
|
+
function parseClaimTail(argv) {
|
|
1974
|
+
const args = { sub: "claim", launch: false, json: false };
|
|
1975
|
+
const positionals = [];
|
|
1976
|
+
for (let i = 1; i < argv.length; i++) {
|
|
1977
|
+
const arg = argv[i];
|
|
1978
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
1979
|
+
else if (arg === "--json") args.json = true;
|
|
1980
|
+
else if (arg.startsWith("-")) {
|
|
1981
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
1982
|
+
} else positionals.push(arg);
|
|
1983
|
+
}
|
|
1984
|
+
if (positionals.length > 1) {
|
|
1985
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${positionals[1]}` } };
|
|
1986
|
+
}
|
|
1987
|
+
const value = positionals[0];
|
|
1988
|
+
if (value !== void 0) {
|
|
1989
|
+
const normalized = value.toLowerCase();
|
|
1990
|
+
if (normalized === "on" || normalized === "auto" || normalized === "true") {
|
|
1991
|
+
args.claimValue = true;
|
|
1992
|
+
} else if (normalized === "off" || normalized === "manual" || normalized === "ask" || normalized === "false") {
|
|
1993
|
+
args.claimValue = false;
|
|
1994
|
+
} else {
|
|
1995
|
+
return {
|
|
1996
|
+
error: {
|
|
1997
|
+
code: EXIT.USAGE,
|
|
1998
|
+
message: `"${value}" is not a claim mode. Use: hs next claim on|off (aliases: auto | manual).`
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return { args };
|
|
2004
|
+
}
|
|
2005
|
+
function parseFrontTail(argv) {
|
|
2006
|
+
const args = { sub: "front", launch: false, json: false };
|
|
2007
|
+
const positionals = [];
|
|
2008
|
+
for (let i = 1; i < argv.length; i++) {
|
|
2009
|
+
const arg = argv[i];
|
|
2010
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
2011
|
+
else if (arg === "--json") args.json = true;
|
|
2012
|
+
else if (arg.startsWith("-")) {
|
|
2013
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
2014
|
+
} else positionals.push(arg);
|
|
2015
|
+
}
|
|
2016
|
+
if (positionals.length > 1) {
|
|
2017
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${positionals[1]}` } };
|
|
2018
|
+
}
|
|
2019
|
+
const value = positionals[0];
|
|
2020
|
+
if (value !== void 0) {
|
|
2021
|
+
const normalized = FRONT_TOKENS[value.toLowerCase()];
|
|
2022
|
+
if (!normalized) {
|
|
2023
|
+
return {
|
|
2024
|
+
error: {
|
|
2025
|
+
code: EXIT.USAGE,
|
|
2026
|
+
message: `"${value}" is not a front. Use: hs next front handheld|assist|auto.`
|
|
2027
|
+
}
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
args.frontValue = normalized;
|
|
2031
|
+
}
|
|
2032
|
+
return { args };
|
|
2033
|
+
}
|
|
2034
|
+
function parseNextConsoleArgs(argv) {
|
|
2035
|
+
const first = argv[0];
|
|
2036
|
+
if (first === "--help" || first === "-h" || first === void 0) return { help: true };
|
|
2037
|
+
if (first === "model" || first === "permission-mode") return parseDefaultsTail(first, argv);
|
|
2038
|
+
if (first === "backend") return parseBackendTail(argv);
|
|
2039
|
+
if (first === "claim") return parseClaimTail(argv);
|
|
2040
|
+
if (first === "front") return parseFrontTail(argv);
|
|
2041
|
+
const args = { sub: "launch", launch: false, json: false };
|
|
2042
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2043
|
+
const arg = argv[i];
|
|
2044
|
+
if (arg === "--help" || arg === "-h") return { help: true };
|
|
2045
|
+
else if (arg === "--launch") args.launch = true;
|
|
2046
|
+
else if (arg === "--dry-run") args.dryRun = true;
|
|
2047
|
+
else if (arg === "--force") args.force = true;
|
|
2048
|
+
else if (arg === "--claim") args.autoClaim = true;
|
|
2049
|
+
else if (arg === "--no-claim") args.autoClaim = false;
|
|
2050
|
+
else if (arg === "--json") args.json = true;
|
|
2051
|
+
else if (arg === "--model" || arg.startsWith("--model=")) {
|
|
2052
|
+
const v = arg.startsWith("--model=") ? arg.slice("--model=".length) : argv[++i];
|
|
2053
|
+
if (v === void 0) {
|
|
2054
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--model requires a value" } };
|
|
2055
|
+
}
|
|
2056
|
+
args.modelOverride = v;
|
|
2057
|
+
} else if (arg === "--permission-mode" || arg.startsWith("--permission-mode=")) {
|
|
2058
|
+
const v = arg.startsWith("--permission-mode=") ? arg.slice("--permission-mode=".length) : argv[++i];
|
|
2059
|
+
if (v === void 0) {
|
|
2060
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--permission-mode requires a value" } };
|
|
2061
|
+
}
|
|
2062
|
+
args.permissionModeOverride = v;
|
|
2063
|
+
} else if (arg === "--backend" || arg.startsWith("--backend=")) {
|
|
2064
|
+
const v = arg.startsWith("--backend=") ? arg.slice("--backend=".length) : argv[++i];
|
|
2065
|
+
if (v === void 0) {
|
|
2066
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--backend requires a value" } };
|
|
2067
|
+
}
|
|
2068
|
+
args.backendOverride = v;
|
|
2069
|
+
} else if (arg === "--front" || arg.startsWith("--front=")) {
|
|
2070
|
+
const v = arg.startsWith("--front=") ? arg.slice("--front=".length) : argv[++i];
|
|
2071
|
+
if (v === void 0) {
|
|
2072
|
+
return { error: { code: EXIT.INVALID_ARG, message: "--front requires a value" } };
|
|
2073
|
+
}
|
|
2074
|
+
const normalized = FRONT_TOKENS[v.toLowerCase()];
|
|
2075
|
+
if (!normalized) {
|
|
2076
|
+
return {
|
|
2077
|
+
error: {
|
|
2078
|
+
code: EXIT.USAGE,
|
|
2079
|
+
message: `unknown front: ${v} (expected ${Object.keys(FRONT_TOKENS).join(" | ")})`
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
args.front = normalized;
|
|
2084
|
+
} else if (arg.startsWith("-")) {
|
|
2085
|
+
return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
|
|
2086
|
+
} else if (FRONT_TOKENS[arg.toLowerCase()] !== void 0 && args.front === void 0) {
|
|
2087
|
+
args.front = FRONT_TOKENS[arg.toLowerCase()];
|
|
2088
|
+
} else if (args.specId === void 0) {
|
|
2089
|
+
const normalized = normalizeBareSpecIdToken(arg);
|
|
2090
|
+
if (!normalized) {
|
|
2091
|
+
return {
|
|
2092
|
+
error: {
|
|
2093
|
+
code: EXIT.USAGE,
|
|
2094
|
+
message: `"${arg}" is not a spec id (HS-CJV2) or a bare key (CJV2).`
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
args.specId = normalized;
|
|
2099
|
+
} else {
|
|
2100
|
+
const normalized = normalizeBareSpecIdToken(arg);
|
|
2101
|
+
if (!normalized) {
|
|
2102
|
+
return { error: { code: EXIT.USAGE, message: `unexpected argument: ${arg}` } };
|
|
2103
|
+
}
|
|
2104
|
+
(args.specIds ??= []).push(normalized);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
if (args.specIds && args.front !== "assisted" && args.front !== "autonomous") {
|
|
2108
|
+
return {
|
|
2109
|
+
error: {
|
|
2110
|
+
code: EXIT.USAGE,
|
|
2111
|
+
message: `unexpected argument: ${args.specIds[0]} \u2014 several ids are accepted only for an assisted/autonomous run (hs next assist|auto <milestone\u2026> --launch); a handheld launch opens one agent on one spec.`
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
const flagError = validateLaunchFlags(args);
|
|
2116
|
+
if (flagError) return { error: flagError };
|
|
2117
|
+
return { args };
|
|
2118
|
+
}
|
|
2119
|
+
async function runNextLaunch(args, deps) {
|
|
2120
|
+
if (args.front === "assisted" || args.front === "autonomous") return runNextArm(args, deps);
|
|
2121
|
+
const prompt = buildNextPrompt(args);
|
|
2122
|
+
const body = {
|
|
2123
|
+
prompt,
|
|
2124
|
+
...args.dryRun ? { dryRun: true } : {},
|
|
2125
|
+
...args.force ? { force: true } : {},
|
|
2126
|
+
...args.autoClaim !== void 0 ? { autoClaim: args.autoClaim } : {},
|
|
2127
|
+
...args.modelOverride !== void 0 ? { modelOverride: args.modelOverride } : {},
|
|
2128
|
+
...args.permissionModeOverride !== void 0 ? { permissionModeOverride: args.permissionModeOverride } : {},
|
|
2129
|
+
...args.backendOverride !== void 0 ? { backendOverride: args.backendOverride } : {}
|
|
2130
|
+
};
|
|
2131
|
+
const res = await controlRequest(deps, { method: "POST", path: "/api/agent/start", body });
|
|
2132
|
+
if (!res.ok) return res.error;
|
|
2133
|
+
if (res.status === 400) {
|
|
2134
|
+
const errorPayload = res.body ?? {};
|
|
2135
|
+
if (errorPayload.error === "backend_not_ready") {
|
|
2136
|
+
const message = typeof errorPayload.message === "string" ? errorPayload.message : "";
|
|
2137
|
+
const fix = typeof errorPayload.fix === "string" ? errorPayload.fix : "";
|
|
2138
|
+
return {
|
|
2139
|
+
code: EXIT.WRITE_FAILED,
|
|
2140
|
+
message: message + (fix ? `
|
|
2141
|
+
${fix}` : ""),
|
|
2142
|
+
payload: { ok: false, error: "backend_not_ready", ...errorPayload },
|
|
2143
|
+
isError: true
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
if (res.status !== 200) return writeFailed(res.status, res.body);
|
|
2148
|
+
const payload = res.body ?? {};
|
|
2149
|
+
const agent = typeof payload.agent === "string" ? payload.agent : "?";
|
|
2150
|
+
const model = typeof payload.model === "string" ? payload.model : "?";
|
|
2151
|
+
const modelSource = typeof payload.modelSource === "string" ? payload.modelSource : "?";
|
|
2152
|
+
const modelDisplay = typeof payload.modelDisplay === "string" ? payload.modelDisplay : model;
|
|
2153
|
+
const useCase = typeof payload.launchUseCase === "string" ? payload.launchUseCase : "?";
|
|
2154
|
+
const autoClaim = payload.autoClaim === true;
|
|
2155
|
+
const launchPrompt = typeof payload.prompt === "string" ? payload.prompt : prompt;
|
|
2156
|
+
const permissionMode = typeof payload.permissionMode === "string" && payload.permissionMode ? payload.permissionMode : null;
|
|
2157
|
+
const permissionModeSource = typeof payload.permissionModeSource === "string" ? payload.permissionModeSource : null;
|
|
2158
|
+
const cursorPermissionModeNote = args.permissionModeOverride !== void 0 && agent === "cursor-agent" ? "\n note: cursor-agent has no --permission-mode equivalent; the requested override was ignored." : "";
|
|
2159
|
+
const backend = typeof payload.backend === "string" ? payload.backend : null;
|
|
2160
|
+
const backendSource = typeof payload.backendSource === "string" ? payload.backendSource : null;
|
|
2161
|
+
const backendNote = typeof payload.note === "string" ? payload.note : null;
|
|
2162
|
+
const backendEnvPreview = payload.backendEnvPreview && typeof payload.backendEnvPreview === "object" && !Array.isArray(payload.backendEnvPreview) ? payload.backendEnvPreview : null;
|
|
2163
|
+
const backendLines = backend ? [
|
|
2164
|
+
` backend: ${backend} (${backendSource ?? "?"})`,
|
|
2165
|
+
...backendEnvPreview ? Object.entries(backendEnvPreview).map(
|
|
2166
|
+
([k, v]) => ` ${k}=${maskBackendEnvValue(k, String(v))}`
|
|
2167
|
+
) : [],
|
|
2168
|
+
...backendNote ? [` note: ${backendNote}`] : []
|
|
2169
|
+
] : [];
|
|
2170
|
+
if (args.dryRun) {
|
|
2171
|
+
return {
|
|
2172
|
+
code: EXIT.OK,
|
|
2173
|
+
message: `[dry-run] would launch ${prompt}
|
|
2174
|
+
agent: ${agent}
|
|
2175
|
+
kind: ${useCase}
|
|
2176
|
+
model: ${modelDisplay} (source: ${modelSource})
|
|
2177
|
+
` + (permissionMode ? ` permission: ${permissionMode} (source: ${permissionModeSource})
|
|
2178
|
+
` : "") + (backendLines.length ? `${backendLines.join("\n")}
|
|
2179
|
+
` : "") + ` claim: ${autoClaim ? "auto (no handshake)" : "manual (agent asks first)"}
|
|
2180
|
+
prompt: ${launchPrompt}` + cursorPermissionModeNote,
|
|
2181
|
+
payload: {
|
|
2182
|
+
ok: true,
|
|
2183
|
+
dryRun: true,
|
|
2184
|
+
specId: args.specId ?? null,
|
|
2185
|
+
agent,
|
|
2186
|
+
launchUseCase: useCase,
|
|
2187
|
+
model,
|
|
2188
|
+
modelDisplay,
|
|
2189
|
+
modelSource,
|
|
2190
|
+
permissionMode,
|
|
2191
|
+
permissionModeSource,
|
|
2192
|
+
backend,
|
|
2193
|
+
backendSource,
|
|
2194
|
+
...backendNote ? { note: backendNote } : {},
|
|
2195
|
+
autoClaim,
|
|
2196
|
+
prompt
|
|
2197
|
+
},
|
|
2198
|
+
isError: false
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
if (payload.alreadyActive) {
|
|
2202
|
+
const reopen = `hs next ${args.specId ?? ""} --launch --force`.replace(/\s+/g, " ").trim();
|
|
2203
|
+
return {
|
|
2204
|
+
code: EXIT.OK,
|
|
2205
|
+
message: `${prompt} is already active (${String(payload.reason ?? "in_flight")}).
|
|
2206
|
+
re-open it anyway with: ${reopen}`,
|
|
2207
|
+
payload: { ok: true, specId: args.specId ?? null, ...payload },
|
|
2208
|
+
isError: false
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
return {
|
|
2212
|
+
code: EXIT.OK,
|
|
2213
|
+
message: `Launched ${prompt} -> ${agent} (${model}, ${useCase}).
|
|
2214
|
+
prompt: ${prompt}` + cursorPermissionModeNote,
|
|
2215
|
+
payload: { ok: true, specId: args.specId ?? null, ...payload },
|
|
2216
|
+
isError: false
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
async function resolveKind(kind, deps) {
|
|
2220
|
+
const res = await controlRequest(deps, {
|
|
2221
|
+
method: "POST",
|
|
2222
|
+
path: "/api/agent/start",
|
|
2223
|
+
body: {
|
|
2224
|
+
prompt: "/hs-next",
|
|
2225
|
+
launchUseCase: launchUseCaseForKind(kind),
|
|
2226
|
+
dryRun: true
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
if (!res.ok) return { ok: false, error: res.error };
|
|
2230
|
+
if (res.status !== 200) return { ok: false, error: writeFailed(res.status, res.body) };
|
|
2231
|
+
const c = res.body ?? {};
|
|
2232
|
+
const model = typeof c.model === "string" ? c.model : "?";
|
|
2233
|
+
return {
|
|
2234
|
+
ok: true,
|
|
2235
|
+
value: {
|
|
2236
|
+
kind,
|
|
2237
|
+
agent: typeof c.agent === "string" ? c.agent : "claude-code",
|
|
2238
|
+
model,
|
|
2239
|
+
modelDisplay: typeof c.modelDisplay === "string" ? c.modelDisplay : model,
|
|
2240
|
+
modelSource: typeof c.modelSource === "string" ? c.modelSource : "?",
|
|
2241
|
+
permissionMode: typeof c.permissionMode === "string" && c.permissionMode ? c.permissionMode : null,
|
|
2242
|
+
permissionModeSource: typeof c.permissionModeSource === "string" ? c.permissionModeSource : null,
|
|
2243
|
+
autoClaim: c.autoClaim === true
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
async function resolveAllKinds(deps) {
|
|
2248
|
+
const out = [];
|
|
2249
|
+
for (const kind of NEXT_LAUNCH_KINDS) {
|
|
2250
|
+
const r = await resolveKind(kind, deps);
|
|
2251
|
+
if (!r.ok) return r;
|
|
2252
|
+
out.push(r.value);
|
|
2253
|
+
}
|
|
2254
|
+
return { ok: true, value: out };
|
|
2255
|
+
}
|
|
2256
|
+
async function resolveRows(kind, deps) {
|
|
2257
|
+
if (kind) {
|
|
2258
|
+
const one = await resolveKind(kind, deps);
|
|
2259
|
+
return one.ok ? { ok: true, rows: [one.value] } : one;
|
|
2260
|
+
}
|
|
2261
|
+
const all = await resolveAllKinds(deps);
|
|
2262
|
+
return all.ok ? { ok: true, rows: all.value } : all;
|
|
2263
|
+
}
|
|
2264
|
+
async function runNextModel(args, deps) {
|
|
2265
|
+
if (args.modelValue === void 0) {
|
|
2266
|
+
const resolved2 = await resolveRows(args.kind, deps);
|
|
2267
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2268
|
+
return {
|
|
2269
|
+
code: EXIT.OK,
|
|
2270
|
+
message: resolved2.rows.map(
|
|
2271
|
+
(r) => `${r.kind.padEnd(9)} model: ${r.modelDisplay} (source: ${r.modelSource}, agent: ${r.agent})`
|
|
2272
|
+
).join("\n"),
|
|
2273
|
+
payload: { ok: true, kinds: resolved2.rows },
|
|
2274
|
+
isError: false
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
if (!args.kind) {
|
|
2278
|
+
return {
|
|
2279
|
+
code: EXIT.USAGE,
|
|
2280
|
+
message: `hs next model needs a kind before a value \u2014 which kind of launch is "${args.modelValue}" for? e.g. hs next model epic ${args.modelValue} (kinds: ${NEXT_LAUNCH_KINDS.join(", ")})`,
|
|
2281
|
+
payload: { ok: false, error: "kind_required", value: args.modelValue },
|
|
2282
|
+
isError: true
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
const resolved = await resolveKind(args.kind, deps);
|
|
2286
|
+
if (!resolved.ok) return resolved.error;
|
|
2287
|
+
const currentAgent = resolved.value.agent;
|
|
2288
|
+
const options = currentAgent === "cursor-agent" ? CURSOR_LAUNCH_MODEL_OPTIONS : CLAUDE_LAUNCH_MODEL_OPTIONS;
|
|
2289
|
+
if (!isKnownModelValue(args.modelValue, options)) {
|
|
2290
|
+
return {
|
|
2291
|
+
code: EXIT.INVALID_ARG,
|
|
2292
|
+
message: `unknown model "${args.modelValue}" for agent ${currentAgent}. Valid options: ${validModelOptionsList(options).join(", ")}`,
|
|
2293
|
+
payload: {
|
|
2294
|
+
ok: false,
|
|
2295
|
+
error: "invalid_model",
|
|
2296
|
+
value: args.modelValue,
|
|
2297
|
+
agent: currentAgent,
|
|
2298
|
+
kind: args.kind
|
|
2299
|
+
},
|
|
2300
|
+
isError: true
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
const valueToStore = canonicalizeModelAlias(args.modelValue) ?? args.modelValue;
|
|
2304
|
+
const bucketField = currentAgent === "cursor-agent" ? "cursorAgent" : currentAgent === "dev-factory-fleet" ? "fleet" : "claudeCode";
|
|
2305
|
+
const settingsKey = launchSettingsBucketKey(launchUseCaseForKind(args.kind));
|
|
2306
|
+
const res = await controlRequest(deps, {
|
|
2307
|
+
method: "PUT",
|
|
2308
|
+
path: "/api/settings",
|
|
2309
|
+
body: { launchModels: { [settingsKey]: { [bucketField]: valueToStore } } }
|
|
2310
|
+
});
|
|
2311
|
+
if (!res.ok) return res.error;
|
|
2312
|
+
if (res.status !== 200) return writeFailed(res.status, res.body);
|
|
2313
|
+
return {
|
|
2314
|
+
code: EXIT.OK,
|
|
2315
|
+
message: `${args.kind} default model set -> ${valueToStore} (agent: ${currentAgent}).`,
|
|
2316
|
+
payload: { ok: true, kind: args.kind, model: valueToStore, agent: currentAgent },
|
|
2317
|
+
isError: false
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
async function runNextPermissionMode(args, deps) {
|
|
2321
|
+
if (args.permissionModeValue === void 0) {
|
|
2322
|
+
const resolved2 = await resolveRows(args.kind, deps);
|
|
2323
|
+
if (!resolved2.ok) return resolved2.error;
|
|
2324
|
+
return {
|
|
2325
|
+
code: EXIT.OK,
|
|
2326
|
+
message: resolved2.rows.map(
|
|
2327
|
+
(r) => r.permissionMode ? `${r.kind.padEnd(9)} permission-mode: ${r.permissionMode} (source: ${r.permissionModeSource}, agent: ${r.agent})` : `${r.kind.padEnd(9)} permission-mode: unset (agent: ${r.agent}) \u2014 claude's own default prompting applies.`
|
|
2328
|
+
).join("\n"),
|
|
2329
|
+
payload: { ok: true, kinds: resolved2.rows },
|
|
2330
|
+
isError: false
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
if (!args.kind) {
|
|
2334
|
+
return {
|
|
2335
|
+
code: EXIT.USAGE,
|
|
2336
|
+
message: `hs next permission-mode needs a kind before a value \u2014 which kind of launch is "${args.permissionModeValue}" for? e.g. hs next permission-mode task ${args.permissionModeValue} (kinds: ${NEXT_LAUNCH_KINDS.join(", ")})`,
|
|
2337
|
+
payload: { ok: false, error: "kind_required", value: args.permissionModeValue },
|
|
2338
|
+
isError: true
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
const resolved = await resolveKind(args.kind, deps);
|
|
2342
|
+
if (!resolved.ok) return resolved.error;
|
|
2343
|
+
const currentAgent = resolved.value.agent;
|
|
2344
|
+
if (currentAgent !== "claude-code") {
|
|
2345
|
+
return {
|
|
2346
|
+
code: EXIT.USAGE,
|
|
2347
|
+
message: `hs next permission-mode is not supported for agent ${currentAgent} yet (no --permission-mode equivalent). The active ${args.kind} launch agent must be claude-code.`,
|
|
2348
|
+
payload: { ok: false, error: "unsupported_agent", agent: currentAgent, kind: args.kind },
|
|
2349
|
+
isError: true
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
if (!isKnownPermissionModeValue(args.permissionModeValue, CLAUDE_PERMISSION_MODE_OPTIONS)) {
|
|
2353
|
+
return {
|
|
2354
|
+
code: EXIT.INVALID_ARG,
|
|
2355
|
+
message: `unknown permission mode "${args.permissionModeValue}" for agent ${currentAgent}. Valid modes: ${CLAUDE_PERMISSION_MODE_OPTIONS.join(", ")}`,
|
|
2356
|
+
payload: {
|
|
2357
|
+
ok: false,
|
|
2358
|
+
error: "invalid_permission_mode",
|
|
2359
|
+
value: args.permissionModeValue,
|
|
2360
|
+
agent: currentAgent,
|
|
2361
|
+
kind: args.kind,
|
|
2362
|
+
validOptions: CLAUDE_PERMISSION_MODE_OPTIONS
|
|
2363
|
+
},
|
|
2364
|
+
isError: true
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
const settingsKey = launchSettingsBucketKey(launchUseCaseForKind(args.kind));
|
|
2368
|
+
const res = await controlRequest(deps, {
|
|
2369
|
+
method: "PUT",
|
|
2370
|
+
path: "/api/settings",
|
|
2371
|
+
body: { launchPermissions: { [settingsKey]: { claudeCode: args.permissionModeValue } } }
|
|
2372
|
+
});
|
|
2373
|
+
if (!res.ok) return res.error;
|
|
2374
|
+
if (res.status !== 200) return writeFailed(res.status, res.body);
|
|
2375
|
+
const bypassCaveat = args.permissionModeValue === "bypassPermissions" ? "\n caveat: bypassPermissions skips ALL permission prompts \u2014 trusted repos only." : "";
|
|
2376
|
+
return {
|
|
2377
|
+
code: EXIT.OK,
|
|
2378
|
+
message: `${args.kind} default permission-mode set -> ${args.permissionModeValue} (agent: ${currentAgent}).${bypassCaveat}`,
|
|
2379
|
+
payload: {
|
|
2380
|
+
ok: true,
|
|
2381
|
+
kind: args.kind,
|
|
2382
|
+
permissionMode: args.permissionModeValue,
|
|
2383
|
+
agent: currentAgent
|
|
2384
|
+
},
|
|
2385
|
+
isError: false
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
async function runNextClaim(args, deps) {
|
|
2389
|
+
if (args.claimValue === void 0) {
|
|
2390
|
+
const resolved = await resolveKind("task", deps);
|
|
2391
|
+
if (!resolved.ok) return resolved.error;
|
|
2392
|
+
const on = resolved.value.autoClaim;
|
|
2393
|
+
return {
|
|
2394
|
+
code: EXIT.OK,
|
|
2395
|
+
message: on ? "claim: auto \u2014 a launched agent claims its spec and starts, no handshake." : "claim: manual \u2014 a launched agent asks before it claims (the default).",
|
|
2396
|
+
payload: { ok: true, autoClaim: on },
|
|
2397
|
+
isError: false
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
const res = await controlRequest(deps, {
|
|
2401
|
+
method: "PUT",
|
|
2402
|
+
path: "/api/settings",
|
|
2403
|
+
body: { launchModels: { autoClaimOnLaunch: args.claimValue } }
|
|
2404
|
+
});
|
|
2405
|
+
if (!res.ok) return res.error;
|
|
2406
|
+
if (res.status !== 200) return writeFailed(res.status, res.body);
|
|
2407
|
+
return {
|
|
2408
|
+
code: EXIT.OK,
|
|
2409
|
+
message: args.claimValue ? "claim default set -> auto. Launched agents claim and start with no handshake.\n caveat: nothing pauses for you between launch and first edit \u2014 use --no-claim for a one-off ask." : "claim default set -> manual. Launched agents ask before they claim.",
|
|
2410
|
+
payload: { ok: true, autoClaim: args.claimValue },
|
|
2411
|
+
isError: false
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
var DISPATCHER_ENSURE_HINT = "node dev-factory/coo-dispatcher.mjs --ensure";
|
|
2415
|
+
var FRONT_DESCRIPTIONS = {
|
|
2416
|
+
handheld: "handheld \u2014 `hs next <id>` walks down to one task and opens one agent on it, with you in the loop (the default).",
|
|
2417
|
+
assisted: "assisted \u2014 `hs next <milestone>` arms a dispatcher run that pauses at decision gates for you.",
|
|
2418
|
+
autonomous: "autonomous \u2014 `hs next <milestone>` arms an unattended dispatcher run; you review the retrospective after."
|
|
2419
|
+
};
|
|
2420
|
+
function readFrontFromSettings(body) {
|
|
2421
|
+
const raw = body?.settings?.launchModels?.defaultRunMode;
|
|
2422
|
+
return raw === "assisted" || raw === "autonomous" ? raw : "handheld";
|
|
2423
|
+
}
|
|
2424
|
+
async function runNextFront(args, deps) {
|
|
2425
|
+
if (args.frontValue === void 0) {
|
|
2426
|
+
const res2 = await controlRequest(deps, { method: "GET", path: "/api/settings" });
|
|
2427
|
+
if (!res2.ok) return res2.error;
|
|
2428
|
+
if (res2.status !== 200) return writeFailed(res2.status, res2.body);
|
|
2429
|
+
const front = readFrontFromSettings(res2.body);
|
|
2430
|
+
return {
|
|
2431
|
+
code: EXIT.OK,
|
|
2432
|
+
message: `front: ${FRONT_DESCRIPTIONS[front]}
|
|
2433
|
+
override once: hs next assist|auto <id> (or --front <name>); set: hs next front <name>`,
|
|
2434
|
+
payload: { ok: true, front },
|
|
2435
|
+
isError: false
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
const res = await controlRequest(deps, {
|
|
2439
|
+
method: "PUT",
|
|
2440
|
+
path: "/api/settings",
|
|
2441
|
+
body: { launchModels: { defaultRunMode: args.frontValue } }
|
|
2442
|
+
});
|
|
2443
|
+
if (!res.ok) return res.error;
|
|
2444
|
+
if (res.status !== 200) return writeFailed(res.status, res.body);
|
|
2445
|
+
return {
|
|
2446
|
+
code: EXIT.OK,
|
|
2447
|
+
message: `front default set -> ${args.frontValue}. ${FRONT_DESCRIPTIONS[args.frontValue]}
|
|
2448
|
+
` + (args.frontValue === "handheld" ? " a bare `hs next <id>` asks to claim one task again." : " a bare `hs next <milestone>` now offers to arm the run; `hs next handheld <id>` still opens one agent."),
|
|
2449
|
+
payload: { ok: true, front: args.frontValue },
|
|
2450
|
+
isError: false
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
async function runNextArm(args, deps) {
|
|
2454
|
+
const front = args.front === "autonomous" ? "autonomous" : "assisted";
|
|
2455
|
+
const word = front === "autonomous" ? "auto" : "assist";
|
|
2456
|
+
const ids = [args.specId, ...args.specIds ?? []].filter(
|
|
2457
|
+
(v) => typeof v === "string" && v !== ""
|
|
2458
|
+
);
|
|
2459
|
+
if (ids.length === 0) {
|
|
2460
|
+
return {
|
|
2461
|
+
code: EXIT.USAGE,
|
|
2462
|
+
message: `an ${front} launch arms a dispatcher run and needs at least one milestone id: hs next ${word} <HS-MIL\u2026> --launch`,
|
|
2463
|
+
payload: { ok: false, error: "usage" },
|
|
2464
|
+
isError: true
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
const ignored = [
|
|
2468
|
+
args.autoClaim !== void 0 ? "--claim/--no-claim" : null,
|
|
2469
|
+
args.modelOverride !== void 0 ? "--model" : null,
|
|
2470
|
+
args.permissionModeOverride !== void 0 ? "--permission-mode" : null,
|
|
2471
|
+
args.backendOverride !== void 0 ? "--backend" : null
|
|
2472
|
+
].filter((v) => v !== null);
|
|
2473
|
+
const ignoredNote = ignored.length > 0 ? `
|
|
2474
|
+
note: ${ignored.join(", ")} apply to a handheld agent launch, not to a dispatcher run \u2014 ignored.` : "";
|
|
2475
|
+
if (args.dryRun) {
|
|
2476
|
+
return {
|
|
2477
|
+
code: EXIT.OK,
|
|
2478
|
+
message: `[dry-run] would arm ${front} run on ${ids.join(", ")}
|
|
2479
|
+
writes: dev-factory/coo-queue.json (this order) + the run front \u2014 no process is spawned
|
|
2480
|
+
then, if the dispatcher is not live: ${DISPATCHER_ENSURE_HINT}` + ignoredNote,
|
|
2481
|
+
payload: { ok: true, dryRun: true, front, milestoneIds: ids },
|
|
2482
|
+
isError: false
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
const res = await controlRequest(deps, {
|
|
2486
|
+
method: "POST",
|
|
2487
|
+
path: "/api/factory/run/launch",
|
|
2488
|
+
body: { milestoneId: ids[0], milestoneIds: ids, front, arm: true }
|
|
2489
|
+
});
|
|
2490
|
+
if (!res.ok) return res.error;
|
|
2491
|
+
const payload = res.body ?? {};
|
|
2492
|
+
const detail = typeof payload.detail === "string" ? payload.detail : "";
|
|
2493
|
+
if (res.status !== 200) {
|
|
2494
|
+
if (!detail) return writeFailed(res.status, res.body);
|
|
2495
|
+
const grainHint = /unknown or closed milestone/.test(detail) ? `
|
|
2496
|
+
${front} runs arm at milestone grain \u2014 pass a milestone id (see: hs milestone show <id>), or open one agent on a single spec with handheld: hs next <id> --launch` : "";
|
|
2497
|
+
return {
|
|
2498
|
+
code: res.status === 409 ? EXIT.CONFLICT : res.status === 501 ? EXIT.WRITE_FAILED : EXIT.INVALID_ARG,
|
|
2499
|
+
message: `arm refused: ${detail}${grainHint}`,
|
|
2500
|
+
payload,
|
|
2501
|
+
isError: true
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
const live = payload.dispatcherLive === true;
|
|
2505
|
+
const ensure = typeof payload.ensureCommand === "string" ? payload.ensureCommand : DISPATCHER_ENSURE_HINT;
|
|
2506
|
+
return {
|
|
2507
|
+
code: EXIT.OK,
|
|
2508
|
+
message: `armed ${front} run on ${ids.join(", ")}
|
|
2509
|
+
${detail}
|
|
2510
|
+
` + (live ? " dispatcher: live \u2014 it picks the queue up on its next tick" : ` dispatcher: not running \u2014 start it in a terminal: ${ensure}`) + "\n watch: hs factory run status" + ignoredNote,
|
|
2511
|
+
payload,
|
|
2512
|
+
isError: false
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
async function runNextConsole(args, deps) {
|
|
2516
|
+
if (args.sub === "model") return runNextModel(args, deps);
|
|
2517
|
+
if (args.sub === "front") return runNextFront(args, deps);
|
|
2518
|
+
if (args.sub === "permission-mode") return runNextPermissionMode(args, deps);
|
|
2519
|
+
if (args.sub === "backend")
|
|
2520
|
+
return runNextBackend({ kind: args.kind, value: args.backendValue }, deps);
|
|
2521
|
+
if (args.sub === "claim") return runNextClaim(args, deps);
|
|
2522
|
+
return runNextLaunch(args, deps);
|
|
2523
|
+
}
|
|
2524
|
+
var HELP2 = [
|
|
2525
|
+
"Usage:",
|
|
2526
|
+
" hs next [<id>] [--summary] [--json] Print the on-deck brief (no launch)",
|
|
2527
|
+
" hs next <id> --launch [--dry-run] Resolve + LAUNCH a session on that spec",
|
|
2528
|
+
" hs next <id> --launch --force Launch again even if a window is already opening",
|
|
2529
|
+
" hs next <id> --launch --claim Auto-claim this launch (no handshake)",
|
|
2530
|
+
" hs next <id> --launch --model <value> One-off model override for this launch",
|
|
2531
|
+
" hs next <id> --launch --permission-mode <mode> One-off permission-mode override",
|
|
2532
|
+
` hs next <id> --launch --backend <${LAUNCH_BACKENDS.join("|")}> One-off backend override`,
|
|
2533
|
+
" hs next model [<kind>] [<value>] Print (or set) a kind's default model",
|
|
2534
|
+
" hs next permission-mode [<kind>] [<value>] Print (or set) a kind's default permission-mode",
|
|
2535
|
+
" hs next backend [<kind>] [<value>] Print (or set) a kind's default backend",
|
|
2536
|
+
" hs next claim [on|off] Print (or set) the auto-claim default",
|
|
2537
|
+
" hs next front [handheld|assist|auto] Print (or set) the default front",
|
|
2538
|
+
" hs next assist|auto <milestone\u2026> --launch Arm a dispatcher run on one or more milestones",
|
|
2539
|
+
"",
|
|
2540
|
+
`Kinds: ${NEXT_LAUNCH_KINDS.join(", ")}. Each kind keeps its OWN default model and`,
|
|
2541
|
+
"permission mode, because launching a milestone or an epic means breaking it",
|
|
2542
|
+
"down while launching a task means writing code \u2014 different work, different",
|
|
2543
|
+
"weight class. `sprint` is settable now and inert until sprints ship: no",
|
|
2544
|
+
"SPRINT spec type is minted yet, so no launch resolves to it. Auto-claim is",
|
|
2545
|
+
"deliberately NOT per-kind \u2014 it answers 'am I still asked before work starts',",
|
|
2546
|
+
"which is one answer for the whole session.",
|
|
2547
|
+
"",
|
|
2548
|
+
"A bare key works everywhere an id does \u2014 `hs next CJV2` is `hs next HS-CJV2`.",
|
|
2549
|
+
"",
|
|
2550
|
+
"Fronts. handheld opens ONE agent on ONE spec with you in the loop \u2014 the",
|
|
2551
|
+
"launch above. assist / auto do not open a terminal: they arm a dispatcher",
|
|
2552
|
+
"run over the milestone(s) you name, by writing the same queue + front files",
|
|
2553
|
+
"the What's Next Run button writes for the already-running coo-dispatcher",
|
|
2554
|
+
"(assisted pauses at decision gates for you; autonomous applies the",
|
|
2555
|
+
"recommended option and reports after). Several milestone ids queue in the",
|
|
2556
|
+
"order given. `hs next front <name>` stores the default; a front word on the",
|
|
2557
|
+
"command overrides it for that call only. Cold-starting the dispatcher stays",
|
|
2558
|
+
"a terminal job \u2014 the reply prints the --ensure command when it is not live.",
|
|
2559
|
+
"",
|
|
2560
|
+
"Launch is OPT-IN here, unlike `hs handoff <CODE>` which launches by default:",
|
|
2561
|
+
"bare `hs next` already has a job \u2014 printing the on-deck brief \u2014 so it keeps",
|
|
2562
|
+
"doing that. `--launch` POSTs the SAME /api/agent/start the What's Next Launch",
|
|
2563
|
+
"button uses, never a second launcher and never a spawn from this CLI process.",
|
|
2564
|
+
"The route reads the KIND off the spec id and pulls that kind's model +",
|
|
2565
|
+
"permission mode from settings. `--dry-run` prints the resolved",
|
|
2566
|
+
"agent/kind/model/claim/prompt without spawning.",
|
|
2567
|
+
"",
|
|
2568
|
+
"One target opens ONE window: the route holds a short in-flight marker, so a",
|
|
2569
|
+
"repeat launch prints `already active` instead of opening a second terminal.",
|
|
2570
|
+
"Pass `--force` when you actually want it re-opened.",
|
|
2571
|
+
"",
|
|
2572
|
+
"`hs next model <kind> <value>` accepts an exact model id (e.g.",
|
|
2573
|
+
`${ANTHROPIC2.PREVIOUS_STRONG}, pinned byte-for-byte) OR a version-less alias that tracks`,
|
|
2574
|
+
"the latest of a family as Claude updates it: claude-opus, claude-sonnet,",
|
|
2575
|
+
"claude-fable, claude-haiku, or the bare opus/sonnet/fable/haiku forms. An",
|
|
2576
|
+
"alias is stored in its canonical claude-<family> form and resolved at",
|
|
2577
|
+
"launch/dry-run time \u2014 `hs next model` (no value) shows both the tracking",
|
|
2578
|
+
"form and what it resolves to right now, for every kind at once.",
|
|
2579
|
+
"",
|
|
2580
|
+
"`--model` / `--permission-mode` / `--backend` / `--claim` on a launch override",
|
|
2581
|
+
"the resolved value for THAT launch only, above the settings default \u2014 the",
|
|
2582
|
+
"stored defaults are untouched. Unknown values are refused before any",
|
|
2583
|
+
"request is sent. None of them is valid without --launch.",
|
|
2584
|
+
"",
|
|
2585
|
+
`Valid permission modes: ${CLAUDE_PERMISSION_MODE_OPTIONS.join(", ")}`,
|
|
2586
|
+
"(the exact `claude --help` vocabulary). Unset \u2014 no flag, no settings default",
|
|
2587
|
+
"\u2014 means no --permission-mode reaches the spawned claude CLI, its own default",
|
|
2588
|
+
"prompting mode. bypassPermissions means the spawned session skips ALL",
|
|
2589
|
+
"permission prompts \u2014 trusted repos only. cursor-agent has no equivalent",
|
|
2590
|
+
"flag; a requested override is ignored with a printed note, and setting a",
|
|
2591
|
+
"default on a cursor-active kind is refused with a plain not-supported-yet",
|
|
2592
|
+
"message.",
|
|
2593
|
+
"",
|
|
2594
|
+
`${BACKEND_VALID_SET_MESSAGE}. Which model service answers a claude-code`,
|
|
2595
|
+
"launch: `anthropic` (default, api.anthropic.com), `ollama` (a local model",
|
|
2596
|
+
"server on localhost:11434) or `deepseek` (DeepSeek's Anthropic-compatible",
|
|
2597
|
+
"endpoint). `hs next backend <kind> <value>` sets the per-kind default",
|
|
2598
|
+
"(`launchModels.<kind>.claudeCodeBackend`); `--backend <value>` on a launch",
|
|
2599
|
+
"overrides it for that launch only, never persisted. A dry-run prints the",
|
|
2600
|
+
"resolved backend, its source, and the environment it would set \u2014 any secret",
|
|
2601
|
+
"(ANTHROPIC_API_KEY) masked to its last four characters, never shown in full.",
|
|
2602
|
+
"Applies to the claude-code agent only; every other agent always resolves to",
|
|
2603
|
+
"anthropic.",
|
|
2604
|
+
"",
|
|
2605
|
+
"Flags:",
|
|
2606
|
+
" --launch Launch a session on the resolved target.",
|
|
2607
|
+
" --dry-run Resolve + print the launch target, don't spawn.",
|
|
2608
|
+
" --force Launch again even if this target already has a window opening.",
|
|
2609
|
+
" --claim Auto-claim on this launch (skip the confirm handshake).",
|
|
2610
|
+
" --no-claim Ask before claiming on this launch, whatever the default is.",
|
|
2611
|
+
" --front <name> handheld | assist | auto \u2014 forwarded into the prompt.",
|
|
2612
|
+
" --model <value> One-off model override for this launch only.",
|
|
2613
|
+
" --permission-mode <mode> One-off permission-mode override for this launch only.",
|
|
2614
|
+
` --backend <value> One-off backend override for this launch only (${LAUNCH_BACKENDS.join(" | ")}).`,
|
|
2615
|
+
" --json Emit a machine-readable JSON result.",
|
|
2616
|
+
" --help, -h Show this help.",
|
|
2617
|
+
"",
|
|
2618
|
+
"Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 12 console-unreachable \xB7",
|
|
2619
|
+
"13 server-error \xB7 14 neon-required"
|
|
2620
|
+
].join("\n");
|
|
2621
|
+
|
|
2622
|
+
// spex-console/bin/run-entry.ts
|
|
2623
|
+
import process2 from "node:process";
|
|
2624
|
+
var BinExit = class extends Error {
|
|
2625
|
+
code;
|
|
2626
|
+
constructor(code) {
|
|
2627
|
+
super(`exit ${code}`);
|
|
2628
|
+
this.name = "BinExit";
|
|
2629
|
+
this.code = code;
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2632
|
+
function runEntry(name, main2, options = {}) {
|
|
2633
|
+
const { fallbackCode = 1, unhandledLabel = "unhandled" } = options;
|
|
2634
|
+
void (async () => {
|
|
2635
|
+
try {
|
|
2636
|
+
await main2();
|
|
2637
|
+
} catch (err) {
|
|
2638
|
+
if (err instanceof BinExit) {
|
|
2639
|
+
process2.exitCode = err.code;
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2643
|
+
process2.stderr.write(`${name}: ${unhandledLabel}: ${message}
|
|
2644
|
+
`);
|
|
2645
|
+
process2.exitCode = fallbackCode;
|
|
2646
|
+
}
|
|
2647
|
+
})();
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// spex-console/bin/hs-next-launch.ts
|
|
2651
|
+
function readDiscovery(p) {
|
|
2652
|
+
try {
|
|
2653
|
+
return readFileSync10(p, "utf-8");
|
|
2654
|
+
} catch {
|
|
2655
|
+
return null;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
async function main() {
|
|
2659
|
+
const vaultRoot = process3.env.HS_VAULT_ROOT;
|
|
2660
|
+
if (!vaultRoot) {
|
|
2661
|
+
process3.stderr.write(
|
|
2662
|
+
"hs next: HS_VAULT_ROOT is not set (the dispatcher must resolve and pass it).\n"
|
|
2663
|
+
);
|
|
2664
|
+
process3.exitCode = EXIT.USAGE;
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
const parsed = parseNextConsoleArgs(process3.argv.slice(3));
|
|
2668
|
+
if (parsed.help) {
|
|
2669
|
+
process3.stdout.write(`${HELP2}
|
|
2670
|
+
`);
|
|
2671
|
+
process3.exitCode = EXIT.OK;
|
|
2672
|
+
return;
|
|
2673
|
+
}
|
|
2674
|
+
if (parsed.error) {
|
|
2675
|
+
process3.stderr.write(`hs next: ${parsed.error.message}
|
|
2676
|
+
`);
|
|
2677
|
+
process3.exitCode = parsed.error.code;
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
const consoleUrl = resolveSpexConsoleUrl(vaultRoot, readDiscovery);
|
|
2681
|
+
const scoped = resolveControlScopeFromMachine(vaultRoot);
|
|
2682
|
+
if (!scoped.ok) {
|
|
2683
|
+
process3.stderr.write(`hs next: ${scoped.error.message}
|
|
2684
|
+
`);
|
|
2685
|
+
process3.exitCode = scoped.error.code;
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
const result = await runNextConsole(parsed.args, {
|
|
2689
|
+
fetchImpl: fetch,
|
|
2690
|
+
consoleUrl,
|
|
2691
|
+
scope: scoped.scope
|
|
2692
|
+
});
|
|
2693
|
+
const sink = result.isError ? process3.stderr : process3.stdout;
|
|
2694
|
+
sink.write(`${parsed.args.json ? JSON.stringify(result.payload) : result.message}
|
|
2695
|
+
`);
|
|
2696
|
+
process3.exitCode = result.code;
|
|
2697
|
+
}
|
|
2698
|
+
runEntry("hs next", main, { fallbackCode: EXIT.WRITE_FAILED });
|