@garygentry/system1 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,914 @@
1
+ import { createRequire as __decideRequire } from "node:module"; const require = __decideRequire(import.meta.url);
2
+ import {
3
+ DEFAULT_ENDPOINT,
4
+ DEFAULT_MODEL_ID,
5
+ ENTRY_FIELDS,
6
+ PACK_NAMES,
7
+ activeTriggers,
8
+ allProfiles,
9
+ assertConsent,
10
+ assertExamples,
11
+ backlogPath,
12
+ checkBudget,
13
+ checkInput,
14
+ combineProjections,
15
+ createContext,
16
+ deciderFor,
17
+ describeExpectation,
18
+ detectHarness,
19
+ exampleFileProblem,
20
+ ledgerFor,
21
+ lintSpec,
22
+ listSpecs,
23
+ loadConfig,
24
+ loadSpec,
25
+ mapWithConcurrency,
26
+ meets,
27
+ mergeCandidates,
28
+ parseExpect,
29
+ parseFileRef,
30
+ parseRecordFilter,
31
+ parseRecordSort,
32
+ parseSplit,
33
+ prepare,
34
+ project,
35
+ readBacklog,
36
+ readField,
37
+ recordMatches,
38
+ resolveProfile,
39
+ resolveRequest,
40
+ route,
41
+ sumUsage,
42
+ unquoteKey,
43
+ userConfigDir,
44
+ verdictOf,
45
+ withBacklogLock,
46
+ writeBacklog
47
+ } from "./chunk-TMAJXDTE.mjs";
48
+ import {
49
+ CLI_PACKAGE,
50
+ VERSION
51
+ } from "./chunk-7OZLIHEM.mjs";
52
+ import {
53
+ DecisionsError,
54
+ isDecisionsError
55
+ } from "./chunk-MH6THLCO.mjs";
56
+
57
+ // ../core/dist/ping.js
58
+ function probeUrl(config) {
59
+ const origin = new URL(config.endpoint).origin;
60
+ return `${origin}/api/v1/models/${config.model}/endpoints`;
61
+ }
62
+ async function ping(config, options = {}) {
63
+ const doFetch = options.fetch ?? fetch;
64
+ const probe = probeUrl(config);
65
+ const base = {
66
+ model: config.model,
67
+ endpoint: config.endpoint,
68
+ probe,
69
+ keyPresent: config.apiKey !== void 0
70
+ };
71
+ const started = performance.now();
72
+ try {
73
+ const response = await doFetch(probe, {
74
+ signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
75
+ });
76
+ const latencyMs = Math.round(performance.now() - started);
77
+ if (!response.ok) {
78
+ return {
79
+ ...base,
80
+ ok: false,
81
+ latencyMs,
82
+ httpStatus: response.status,
83
+ error: `HTTP ${response.status}`
84
+ };
85
+ }
86
+ const body = await response.json();
87
+ const contextLength = body.data?.endpoints?.[0]?.context_length;
88
+ return {
89
+ ...base,
90
+ ok: true,
91
+ latencyMs,
92
+ httpStatus: response.status,
93
+ ...typeof contextLength === "number" ? { contextLength } : {}
94
+ };
95
+ } catch (error) {
96
+ return { ...base, ok: false, error: describe(error) };
97
+ }
98
+ }
99
+ function describe(error) {
100
+ if (error instanceof Error) {
101
+ const cause = error.cause;
102
+ return cause instanceof Error ? `${error.message}: ${cause.message}` : error.message;
103
+ }
104
+ return String(error);
105
+ }
106
+
107
+ // ../core/dist/tools/many.js
108
+ var EXCERPT = 120;
109
+ var SAMPLE = 5;
110
+ async function runMany(ctx, rawInput) {
111
+ const input = checkInput("many", rawInput);
112
+ const started = performance.now();
113
+ const req = resolveRequest(ctx, input);
114
+ const prepared = await prepare({
115
+ sources: req.sources,
116
+ split: req.split,
117
+ questions: req.questions,
118
+ profile: req.profile,
119
+ cwd: ctx.config.repoRoot,
120
+ exclude: ctx.config.egress.exclude,
121
+ filter: req.exclude,
122
+ oversize: "skip",
123
+ ...input.allowOutside ? { allowOutside: true } : {}
124
+ });
125
+ const base = {
126
+ ...req.spec ? { spec: req.spec.name } : {},
127
+ model: req.profile.id,
128
+ skipped: summariseSkipped(prepared.skipped),
129
+ redactions: { total: prepared.redactions.total, items: prepared.redactions.items },
130
+ projection: prepared.projection
131
+ };
132
+ if (input.dryRun) {
133
+ return {
134
+ ...base,
135
+ dryRun: true,
136
+ counts: counts(prepared.items.length, 0, 0, 0, 0, 0, prepared.skipped.length),
137
+ kept: [],
138
+ undecided: [],
139
+ failed: [],
140
+ usage: sumUsage([]),
141
+ wallClockMs: Math.round(performance.now() - started),
142
+ sampleIds: prepared.items.slice(0, SAMPLE).map((i) => i.id)
143
+ };
144
+ }
145
+ const decider = deciderFor(ctx, req.profile, input.mode ?? "auto");
146
+ if (decider.mode !== "replay") {
147
+ assertConsent(ctx.config.egress.consent, ctx.config.repoRoot);
148
+ checkBudget(prepared.projection, ctx.config.budget, input.confirm ?? false);
149
+ }
150
+ const concurrency = Math.min(input.concurrency ?? ctx.config.concurrency, ctx.config.concurrency);
151
+ const settled = await mapWithConcurrency(prepared.items, concurrency, (item) => decider.decide({ state: item.state, questions: req.questions, namespace: req.namespace }));
152
+ const done = [];
153
+ const failed = [];
154
+ settled.forEach((outcome, i) => {
155
+ const item = prepared.items[i];
156
+ if ("value" in outcome)
157
+ done.push({ item, result: outcome.value });
158
+ else
159
+ failed.push({ id: item.id, ...describe2(outcome.error) });
160
+ });
161
+ const [first] = failed;
162
+ if (done.length === 0 && first && failed.every((f) => f.code === first.code)) {
163
+ const { error } = settled.find((s) => "error" in s);
164
+ const details = isDecisionsError(error) ? error.details : {};
165
+ throw new DecisionsError(first.code === "error" ? "invalid-request" : first.code, failed.length === 1 ? error.message : `All ${failed.length} items failed: ${error.message}`, { ...details, failed: failed.length });
166
+ }
167
+ const rows = done.map(({ item, result }) => ({
168
+ ...row(item, result.answers),
169
+ flat: result.undecided
170
+ }));
171
+ const projected = project({
172
+ rows,
173
+ keep: req.keep,
174
+ keepAny: req.keepAny,
175
+ ...req.sort ? { sort: req.sort } : {},
176
+ ...input.limit !== void 0 ? { limit: input.limit } : {},
177
+ ...input.fields ? { fields: input.fields } : {},
178
+ undecidedOf: (r) => r.flat
179
+ });
180
+ const strip = ({ flat, ...rest }) => ({
181
+ ...rest,
182
+ ...flat.length ? { undecided: [...flat] } : {}
183
+ });
184
+ return {
185
+ ...base,
186
+ dryRun: false,
187
+ source: decider.mode === "replay" ? "replay" : "live",
188
+ counts: counts(prepared.items.length, projected.kept.length, projected.keptTotal, projected.undecided.length, projected.dropped, failed.length, prepared.skipped.length),
189
+ kept: projected.kept.map(strip),
190
+ undecided: projected.undecided.map(({ row: row2, questions }) => ({ ...strip(row2), questions })),
191
+ failed,
192
+ usage: sumUsage(done.map((d) => d.result.usage)),
193
+ wallClockMs: Math.round(performance.now() - started)
194
+ };
195
+ }
196
+ function row(item, answers) {
197
+ return {
198
+ id: item.id,
199
+ ...item.path ? { path: item.path } : {},
200
+ ...item.lines ? { lines: item.lines } : {},
201
+ ...traceable(item) ? {} : { excerpt: excerpt(item.state) },
202
+ answers
203
+ };
204
+ }
205
+ function traceable(item) {
206
+ if (item.path === void 0)
207
+ return false;
208
+ const oneLine = item.lines !== void 0 && item.lines.start === item.lines.end;
209
+ return item.id === item.path || !oneLine;
210
+ }
211
+ function excerpt(state) {
212
+ const text = (typeof state === "string" ? state : JSON.stringify(state)).replace(/\s+/g, " ").trim();
213
+ return text.length > EXCERPT ? `${text.slice(0, EXCERPT - 1)}\u2026` : text;
214
+ }
215
+ function counts(items, kept, keptTotal, undecided, dropped, failed, skipped) {
216
+ return { items, kept, keptTotal, undecided, dropped, failed, skipped };
217
+ }
218
+ function summariseSkipped(skipped) {
219
+ const byReason = {};
220
+ for (const s of skipped)
221
+ byReason[s.reason] = (byReason[s.reason] ?? 0) + 1;
222
+ const ordered = [...skipped].sort((a, b) => Number(b.reason === "excluded") - Number(a.reason === "excluded"));
223
+ return { total: skipped.length, byReason, sample: ordered.slice(0, SAMPLE) };
224
+ }
225
+ function describe2(error) {
226
+ return isDecisionsError(error) ? { code: error.code, message: error.message } : { code: "error", message: error.message };
227
+ }
228
+
229
+ // ../core/dist/tools/ask.js
230
+ async function runAsk(ctx, rawInput) {
231
+ const input = checkInput("ask", rawInput);
232
+ const req = resolveRequest(ctx, input);
233
+ const prepared = await prepare({
234
+ sources: req.sources,
235
+ split: req.split,
236
+ questions: req.questions,
237
+ profile: req.profile,
238
+ cwd: ctx.config.repoRoot,
239
+ exclude: ctx.config.egress.exclude,
240
+ filter: req.exclude,
241
+ ...input.allowOutside ? { allowOutside: true } : {}
242
+ });
243
+ const [item, ...rest] = prepared.items;
244
+ if (!item || rest.length > 0) {
245
+ const held = prepared.skipped.filter((s) => s.reason !== "filtered");
246
+ const left = prepared.skipped.length - held.length;
247
+ const withheld = (held.length ? ` (${held.length} withheld: ${held.map((s) => `${s.path} ${s.reason}`).join(", ")})` : "") + (left ? ` (${left} left out by --exclude)` : "");
248
+ throw new DecisionsError("invalid-request", item ? `ask needs exactly one state, but the source gave ${prepared.items.length}. Use \`decide many\` to fan out.` : `The source gave nothing to ask about${withheld}.`, { items: prepared.items.length });
249
+ }
250
+ const result = await deciderFor(ctx, req.profile, input.mode ?? "auto").decide({
251
+ state: item.state,
252
+ questions: req.questions,
253
+ namespace: req.namespace
254
+ });
255
+ const verdict = req.keep.length > 0 || req.keepAny.length > 0 ? verdictOf(result.answers, result.undecided, req.keep, req.keepAny).verdict : void 0;
256
+ return {
257
+ ...req.spec ? { spec: req.spec.name } : {},
258
+ id: item.id,
259
+ ...item.path ? { path: item.path } : {},
260
+ ...item.lines ? { lines: item.lines } : {},
261
+ source: result.source,
262
+ model: result.model,
263
+ servedBy: result.servedBy,
264
+ answers: result.answers,
265
+ undecided: result.undecided,
266
+ ...verdict ? { verdict } : {},
267
+ usage: result.usage,
268
+ latencyMs: result.latencyMs,
269
+ skipped: summariseSkipped(prepared.skipped),
270
+ redactions: { total: prepared.redactions.total, items: prepared.redactions.items }
271
+ };
272
+ }
273
+
274
+ // ../core/dist/tools/doctor.js
275
+ import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
276
+ import { homedir } from "node:os";
277
+ import { delimiter, join } from "node:path";
278
+ function guardCheck(config, env, home, cliPath) {
279
+ const enabled = PACK_NAMES.filter((name) => config.guard.packs[name].enabled);
280
+ if (enabled.length === 0)
281
+ return { name: "guard", status: "ok", detail: "no guard pack enabled (all dormant)" };
282
+ if (!config.egress.consent.granted) {
283
+ return {
284
+ name: "guard",
285
+ status: "warn",
286
+ detail: `${enabled.join(", ")} enabled, but this repo has no egress consent: dormant`,
287
+ fix: "grant consent (see the consent check), or turn it off with `decide guard disable <pack>`",
288
+ // The consent check already stands between this shell and a live decision.
289
+ advisory: true
290
+ };
291
+ }
292
+ const codex = codexHookTrust(env, home);
293
+ if (codex === "untrusted") {
294
+ return {
295
+ name: "guard",
296
+ status: "warn",
297
+ detail: `active: ${enabled.join(", ")}; but Codex hasn't trusted the system1 hooks, so it skips them without a word`,
298
+ fix: "open Codex interactively in this repo once and trust the system1 hooks when it asks (after a plugin update it may ask again)",
299
+ advisory: true
300
+ };
301
+ }
302
+ if (cliPath && /[\\/]_npx[\\/]/.test(cliPath)) {
303
+ return {
304
+ name: "guard",
305
+ status: "warn",
306
+ detail: `active: ${enabled.join(", ")}; but decide runs from npx's cache, which the hooks never download into: after a plugin update or an npm cache clean, the check is skipped silently until decide runs once`,
307
+ fix: "npm i -g @garygentry/system1",
308
+ advisory: true
309
+ };
310
+ }
311
+ return { name: "guard", status: "ok", detail: `active: ${enabled.join(", ")}` };
312
+ }
313
+ function codexHookTrust(env, home = homedir()) {
314
+ const file = join(env.CODEX_HOME?.trim() || join(home, ".codex"), "config.toml");
315
+ let text;
316
+ try {
317
+ text = readFileSync(file, "utf8");
318
+ } catch {
319
+ return "absent";
320
+ }
321
+ const section = (header) => {
322
+ const out = [];
323
+ const lines = text.split(/\r?\n/);
324
+ lines.forEach((line, n) => {
325
+ if (!header.test(line))
326
+ return;
327
+ const body = [];
328
+ for (const next of lines.slice(n + 1)) {
329
+ if (/^\s*\[/.test(next))
330
+ break;
331
+ body.push(next);
332
+ }
333
+ out.push(body.join("\n"));
334
+ });
335
+ return out;
336
+ };
337
+ const plugin = section(/^\s*\[plugins\."system1@[^"]+"\]\s*$/);
338
+ if (plugin.length === 0 || plugin.every((b) => /^\s*enabled\s*=\s*false\b/m.test(b))) {
339
+ return "absent";
340
+ }
341
+ const trust = section(/^\s*\[hooks\.state\."system1@[^"]+:[^"]*codex-hooks\.json:stop:[^"]*"\]\s*$/);
342
+ return trust.some((b) => /^\s*trusted_hash\s*=/m.test(b)) ? "trusted" : "untrusted";
343
+ }
344
+ function routeCheck(config) {
345
+ try {
346
+ route("", config);
347
+ if (!config.enabled) {
348
+ return {
349
+ name: "route",
350
+ status: "ok",
351
+ detail: "routing hints are off (route.enabled or SYSTEM1_ROUTE)"
352
+ };
353
+ }
354
+ const names = activeTriggers(config).map((t) => t.name);
355
+ return {
356
+ name: "route",
357
+ status: "ok",
358
+ detail: `routing hints on: ${names.length ? names.join(", ") : "no triggers"}`
359
+ };
360
+ } catch (error) {
361
+ return {
362
+ name: "route",
363
+ status: "warn",
364
+ detail: `routing hints are silent: ${error instanceof Error ? error.message : String(error)}`,
365
+ fix: 'correct route: in the config file (test it with `decide route --text "\u2026"`)'
366
+ };
367
+ }
368
+ }
369
+ function backlogProblem(error) {
370
+ const problems = isDecisionsError(error) ? error.details?.problems : void 0;
371
+ const first = Array.isArray(problems) ? problems[0] : void 0;
372
+ const count = Array.isArray(problems) && problems.length > 1 ? ` (+${problems.length - 1} more)` : "";
373
+ return `the backlog doesn't validate: ${first ?? (error instanceof Error ? error.message : String(error))}${count}`;
374
+ }
375
+ function backlogCheck(repoRoot) {
376
+ const file = backlogPath(repoRoot);
377
+ if (!existsSync(file))
378
+ return { name: "backlog", status: "ok", detail: "no scout backlog yet" };
379
+ try {
380
+ const { opportunities } = readBacklog(file);
381
+ return {
382
+ name: "backlog",
383
+ status: "ok",
384
+ detail: `${opportunities.length} opportunit${opportunities.length === 1 ? "y" : "ies"} in ${file}`
385
+ };
386
+ } catch (error) {
387
+ return {
388
+ name: "backlog",
389
+ status: "warn",
390
+ detail: backlogProblem(error),
391
+ fix: "`decide opportunities check` lists every problem; correct the file by hand or move it aside (decide never repairs it)"
392
+ };
393
+ }
394
+ }
395
+ var CODEX_RULE = 'prefix_rule(pattern = ["decide"], decision = "allow")';
396
+ async function runDoctor(options) {
397
+ const { env } = options;
398
+ const harness = detectHarness(env) ?? null;
399
+ const checks = [
400
+ {
401
+ name: "cli",
402
+ status: "ok",
403
+ detail: `decide ${VERSION} on node ${process.version}${options.cliPath ? ` (${options.cliPath})` : ""}`
404
+ },
405
+ pathCheck(harness, env)
406
+ ];
407
+ const onPath = which("decide", env.PATH ?? "");
408
+ if (onPath && options.probeVersion) {
409
+ checks.push(await versionCheck(onPath, options.probeVersion, env));
410
+ }
411
+ const report = (ctx2, live2) => ({
412
+ healthy: !checks.some((c) => c.status === "fail"),
413
+ live: live2,
414
+ harness,
415
+ session: ctx2?.config.session ?? null,
416
+ version: VERSION,
417
+ node: process.version,
418
+ checks
419
+ });
420
+ let ctx;
421
+ try {
422
+ ctx = createContext(options);
423
+ } catch (error) {
424
+ checks.push({
425
+ name: "config",
426
+ status: "fail",
427
+ detail: error instanceof Error ? error.message : String(error),
428
+ fix: "correct the config file or environment variable named above"
429
+ });
430
+ return report(void 0, false);
431
+ }
432
+ const { config } = ctx;
433
+ if (config.warnings.length > 0) {
434
+ checks.push({
435
+ name: "config-keys",
436
+ status: "warn",
437
+ detail: `ignored: ${config.warnings.join("; ")}`,
438
+ fix: "correct or remove these keys (docs/configuration.md lists every key)"
439
+ });
440
+ }
441
+ const credentials = join(userConfigDir(env, options.home), "credentials");
442
+ checks.push(config.apiKey && config.apiKeyQuoted ? {
443
+ name: "key",
444
+ status: "warn",
445
+ detail: `OPENROUTER_API_KEY present (${config.apiKeySource}), but wrapped in quotes; they were removed`,
446
+ fix: config.apiKeySource === "env" ? "set OPENROUTER_API_KEY without the surrounding quotes" : `remove the extra quotes around openrouter_api_key in ${credentials}`,
447
+ advisory: true
448
+ } : config.apiKey ? {
449
+ name: "key",
450
+ status: "ok",
451
+ detail: `OPENROUTER_API_KEY present (${config.apiKeySource})`
452
+ } : {
453
+ name: "key",
454
+ status: "warn",
455
+ detail: "no API key: only replay works",
456
+ // The file this shell would read: XDG_CONFIG_HOME moves it.
457
+ fix: `set OPENROUTER_API_KEY, or write \`openrouter_api_key: <key>\` to ${credentials} (create its directory first; chmod 600)`
458
+ });
459
+ checks.push(config.egress.consent.granted ? { name: "consent", status: "ok", detail: `egress consent granted for ${config.repoRoot}` } : {
460
+ name: "consent",
461
+ status: "warn",
462
+ detail: `no egress consent for ${config.repoRoot}: live calls are refused`,
463
+ fix: "if the user agrees, they grant it themselves (an agent must not): `decide config egress allow` in a terminal in this repo, or through their agent prompt's shell escape (not as a chat message) with --confirm added"
464
+ });
465
+ checks.push(routeCheck(config.route));
466
+ checks.push(guardCheck(config, env, options.home, options.cliPath));
467
+ checks.push(backlogCheck(config.repoRoot));
468
+ let reach;
469
+ try {
470
+ reach = await ping({
471
+ endpoint: config.endpoint,
472
+ model: config.model,
473
+ apiKey: config.apiKey,
474
+ replay: config.replay
475
+ }, ctx.fetch ? { fetch: ctx.fetch } : {});
476
+ } catch (error) {
477
+ checks.push({
478
+ name: "config",
479
+ status: "fail",
480
+ detail: `endpoint ${config.endpoint}: ${error instanceof Error ? error.message : String(error)}`,
481
+ fix: "correct SYSTEM1_ENDPOINT or `endpoint` in the config file"
482
+ });
483
+ return report(ctx, false);
484
+ }
485
+ if (reach.ok) {
486
+ checks.push({
487
+ name: "network",
488
+ status: "ok",
489
+ detail: `${config.model} reachable in ${reach.latencyMs} ms`
490
+ });
491
+ } else {
492
+ const sandboxed = env.CODEX_SANDBOX_NETWORK_DISABLED === "1";
493
+ checks.push({
494
+ name: "network",
495
+ // Replay needs no network, so an unreachable endpoint only warns there.
496
+ status: config.replay ? "warn" : "fail",
497
+ detail: `${reach.probe}: ${reach.error ?? "unreachable"}${sandboxed ? " (Codex sandbox has network disabled)" : ""}`,
498
+ fix: networkFix(reach.httpStatus, harness, sandboxed, env)
499
+ });
500
+ }
501
+ const live = !config.replay && config.apiKey !== void 0 && config.egress.consent.granted && reach.ok === true;
502
+ return report(ctx, live);
503
+ }
504
+ function pathCheck(harness, env) {
505
+ const onPath = which("decide", env.PATH ?? "");
506
+ if (onPath)
507
+ return { name: "path", status: "ok", detail: `decide on PATH: ${onPath}` };
508
+ if (harness === "claude") {
509
+ return {
510
+ name: "path",
511
+ status: "warn",
512
+ detail: "decide is not on PATH, so the System 1 plugin's bin/ is not loaded",
513
+ fix: `npm i -g ${CLI_PACKAGE}@${VERSION} (also puts decide in your own terminal), or install the System 1 plugin from its marketplace`
514
+ };
515
+ }
516
+ return {
517
+ name: "path",
518
+ status: "warn",
519
+ detail: "decide is not on PATH. Codex and Pi do not add plugin bin/ to PATH",
520
+ fix: `npm i -g ${CLI_PACKAGE}@${VERSION}`
521
+ };
522
+ }
523
+ async function versionCheck(path, probe, env) {
524
+ let raw;
525
+ try {
526
+ raw = await probe(path);
527
+ } catch {
528
+ }
529
+ const found = raw?.trim() || void 0;
530
+ if (raw !== void 0 && found === void 0 && env.CODEX_SANDBOX_NETWORK_DISABLED === "1") {
531
+ return {
532
+ name: "path-version",
533
+ status: "ok",
534
+ detail: "not checked: the Codex sandbox hides a child process's output"
535
+ };
536
+ }
537
+ if (found === void 0) {
538
+ return {
539
+ name: "path-version",
540
+ status: "warn",
541
+ detail: `could not read the version of ${path}`,
542
+ fix: `run \`${path} version\` to see why; reinstall it if it fails`
543
+ };
544
+ }
545
+ if (found === VERSION) {
546
+ return {
547
+ name: "path-version",
548
+ status: "ok",
549
+ detail: `decide on PATH is ${found}, same as this one`
550
+ };
551
+ }
552
+ return {
553
+ name: "path-version",
554
+ status: "warn",
555
+ detail: `decide on PATH is ${found}, but this one is ${VERSION}: agents will run the one on PATH`,
556
+ fix: `npm i -g ${CLI_PACKAGE}@${VERSION}, or remove the other decide (${path})`
557
+ };
558
+ }
559
+ function networkFix(httpStatus, harness, sandboxed, env) {
560
+ if (httpStatus === 404)
561
+ return "the endpoint does not know this model: check SYSTEM1_MODEL / `model` and SYSTEM1_ENDPOINT / `endpoint`";
562
+ if (httpStatus !== void 0 && httpStatus >= 500)
563
+ return `the provider answered HTTP ${httpStatus}: retry later`;
564
+ if (httpStatus !== void 0)
565
+ return `the endpoint answered HTTP ${httpStatus}: check SYSTEM1_ENDPOINT / \`endpoint\``;
566
+ if (sandboxed) {
567
+ return `add \`${CODEX_RULE}\` to ${env.CODEX_HOME?.trim() || "~/.codex"}/rules/system1.rules, then restart Codex (it covers commands that start with decide; a pipe into decide stays offline)`;
568
+ }
569
+ if (harness === "claude")
570
+ return "allow outbound access to openrouter.ai in Claude Code's sandbox settings";
571
+ return "check that this machine can reach openrouter.ai (proxy, firewall, DNS)";
572
+ }
573
+ function which(name, path) {
574
+ for (const dir of path.split(delimiter)) {
575
+ if (!dir)
576
+ continue;
577
+ const candidate = join(dir, name);
578
+ try {
579
+ accessSync(candidate, constants.X_OK);
580
+ if (statSync(candidate).isFile())
581
+ return candidate;
582
+ } catch {
583
+ }
584
+ }
585
+ return void 0;
586
+ }
587
+
588
+ // ../core/dist/tools/opportunities.js
589
+ import { existsSync as existsSync2 } from "node:fs";
590
+ function runOpportunitiesAdd(ctx, rawInput, now = () => /* @__PURE__ */ new Date()) {
591
+ const input = checkInput("opportunities-add", rawInput);
592
+ const file = backlogPath(ctx.config.repoRoot);
593
+ const merged = withBacklogLock(file, () => {
594
+ const result = mergeCandidates(readBacklog(file), input.candidates, now().toISOString());
595
+ writeBacklog(file, result.backlog);
596
+ return result;
597
+ });
598
+ return {
599
+ file,
600
+ added: merged.added,
601
+ updated: merged.updated,
602
+ staled: merged.staled,
603
+ total: merged.backlog.opportunities.length
604
+ };
605
+ }
606
+ function runOpportunitiesList(ctx, rawInput = {}) {
607
+ const input = checkInput("opportunities-list", rawInput);
608
+ const filters = (input.keep ?? []).map(parseRecordFilter);
609
+ const sort = parseRecordSort(input.sort ?? "projected:desc");
610
+ assertFields(input.fields);
611
+ const file = backlogPath(ctx.config.repoRoot);
612
+ const { opportunities } = readBacklog(file);
613
+ const matched = opportunities.filter((o) => filters.every((f) => recordMatches(o, f))).sort((a, b) => compare(readField(a, sort.field), readField(b, sort.field), sort.direction));
614
+ const shown = input.limit === void 0 ? matched : matched.slice(0, input.limit);
615
+ return {
616
+ file,
617
+ total: opportunities.length,
618
+ matched: matched.length,
619
+ basis: "projected",
620
+ opportunities: shown.map((o) => pick(o, input.fields))
621
+ };
622
+ }
623
+ function runOpportunitiesCheck(ctx, rawInput = {}) {
624
+ checkInput("opportunities-check", rawInput);
625
+ const file = backlogPath(ctx.config.repoRoot);
626
+ const backlog = readBacklog(file);
627
+ const byStatus = {};
628
+ for (const o of backlog.opportunities)
629
+ byStatus[o.status] = (byStatus[o.status] ?? 0) + 1;
630
+ return { file, exists: existsSync2(file), entries: backlog.opportunities.length, byStatus };
631
+ }
632
+ function compare(a, b, direction) {
633
+ if (a === void 0 || b === void 0)
634
+ return a === b ? 0 : a === void 0 ? 1 : -1;
635
+ const order = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b));
636
+ return direction === "asc" ? order : -order;
637
+ }
638
+ function pick(o, fields) {
639
+ if (!fields)
640
+ return o;
641
+ const out = { id: o.id };
642
+ for (const f of fields)
643
+ if (Object.hasOwn(o, f))
644
+ out[f] = o[f];
645
+ return out;
646
+ }
647
+ function assertFields(fields = []) {
648
+ const unknown = fields.filter((f) => !ENTRY_FIELDS.includes(f));
649
+ if (unknown.length) {
650
+ throw new DecisionsError("invalid-request", `--fields: no field ${unknown.join(", ")}. Fields: ${ENTRY_FIELDS.join(", ")}`);
651
+ }
652
+ }
653
+
654
+ // ../core/dist/tools/route.js
655
+ function runRoute(options, rawInput) {
656
+ const problems = checkRouteInput(rawInput);
657
+ if (problems.length > 0) {
658
+ throw new DecisionsError("invalid-request", `Invalid route input:
659
+ ${problems.join("\n ")}`, {
660
+ problems
661
+ });
662
+ }
663
+ const input = rawInput;
664
+ const config = loadConfig(options);
665
+ const { user, repo } = config.layers;
666
+ return {
667
+ ...route(input.prompt, config.route),
668
+ files: { ...user ? { user } : {}, ...repo ? { repo } : {} }
669
+ };
670
+ }
671
+ function checkRouteInput(input) {
672
+ if (typeof input !== "object" || input === null || Array.isArray(input))
673
+ return ["/ must be an object"];
674
+ const extra = Object.keys(input).filter((k) => k !== "prompt");
675
+ return [
676
+ ...typeof input.prompt === "string" ? [] : ["/prompt must be a string"],
677
+ ...extra.map((k) => `/${k} is not allowed`)
678
+ ];
679
+ }
680
+
681
+ // ../core/dist/tools/spec-check.js
682
+ async function runSpecCheck(ctx, rawInput) {
683
+ const input = checkInput("spec-check", rawInput);
684
+ const spec = loadSpec(input.spec, ctx.specDirs, ctx.cwd);
685
+ const lint = lintSpec(spec);
686
+ const examples = spec.examples ?? [];
687
+ if (examples.length === 0) {
688
+ throw new DecisionsError("invalid-request", `Spec "${spec.name}" has no examples to check. Add some under examples: (id, state or file, expect).`);
689
+ }
690
+ assertExamples(spec);
691
+ const profile = resolveProfile(input.model ?? ctx.config.model, allProfiles(ctx.config));
692
+ const root = ctx.config.repoRoot;
693
+ const prepared = [];
694
+ for (const example of examples) {
695
+ const expect = example.expect ? parseExpect(example.expect, spec.questions, example.id) : {};
696
+ const refused = example.file !== void 0 ? exampleFileProblem(example.file, root) : void 0;
697
+ if (refused) {
698
+ prepared.push({ id: example.id, expect, reason: `${example.file} ${refused}` });
699
+ continue;
700
+ }
701
+ const source = example.file !== void 0 ? { kind: "file", ...parseFileRef(example.file) } : {
702
+ kind: "text",
703
+ // A structured state is sent as its JSON text.
704
+ text: typeof example.state === "string" ? example.state : JSON.stringify(example.state),
705
+ id: example.id
706
+ };
707
+ try {
708
+ const prep = await prepare({
709
+ sources: [source],
710
+ split: parseSplit("file"),
711
+ questions: spec.questions,
712
+ profile,
713
+ cwd: root,
714
+ exclude: ctx.config.egress.exclude
715
+ });
716
+ const [item, ...rest] = prep.items;
717
+ const why = prep.skipped[0];
718
+ prepared.push(item && rest.length === 0 ? { id: example.id, expect, prep } : {
719
+ id: example.id,
720
+ expect,
721
+ reason: why ? `${why.path} ${why.reason}` : `gave ${prep.items.length} states, not 1`
722
+ });
723
+ } catch (error) {
724
+ if (!isDecisionsError(error) || !["state-too-large", "source-error"].includes(error.code)) {
725
+ throw error;
726
+ }
727
+ prepared.push({ id: example.id, expect, reason: error.message.split("\n")[0] });
728
+ }
729
+ }
730
+ const decider = deciderFor(ctx, profile, input.mode ?? "replay");
731
+ if (decider.mode !== "replay") {
732
+ assertConsent(ctx.config.egress.consent, root);
733
+ const parts = prepared.flatMap((p) => p.prep ? [p.prep.projection] : []);
734
+ checkBudget(combineProjections(profile, parts), ctx.config.budget, input.confirm ?? false);
735
+ }
736
+ const results = [];
737
+ const usages = [];
738
+ let misses = 0;
739
+ for (const { id, expect, prep, reason } of prepared) {
740
+ const item = prep?.items[0];
741
+ if (!prep || !item) {
742
+ results.push({ id, status: "withheld", reason: reason ?? "not sent" });
743
+ continue;
744
+ }
745
+ try {
746
+ const r = await decider.decide({
747
+ state: item.state,
748
+ questions: spec.questions,
749
+ namespace: spec.name
750
+ });
751
+ usages.push(r.usage);
752
+ results.push(judge(id, r.answers, r.undecided, expect));
753
+ } catch (error) {
754
+ if (isDecisionsError(error) && error.code === "replay-miss") {
755
+ misses += 1;
756
+ continue;
757
+ }
758
+ throw error;
759
+ }
760
+ }
761
+ if (misses > 0) {
762
+ throw new DecisionsError("replay-miss", `${misses} of ${examples.length} examples of "${spec.name}" have no recorded answer. Record them with \`decide spec check ${spec.name} --live\` (needs a key and repo consent).`, { misses });
763
+ }
764
+ const counts2 = {
765
+ examples: results.length,
766
+ pass: 0,
767
+ fail: 0,
768
+ undecided: 0,
769
+ captured: 0,
770
+ withheld: 0
771
+ };
772
+ for (const r of results)
773
+ counts2[r.status] += 1;
774
+ return {
775
+ spec: spec.name,
776
+ file: spec.file,
777
+ model: profile.id,
778
+ source: decider.mode === "replay" ? "replay" : "live",
779
+ passed: counts2.fail === 0 && counts2.undecided === 0 && counts2.withheld === 0,
780
+ counts: counts2,
781
+ examples: results,
782
+ usage: sumUsage(usages),
783
+ skipped: summariseSkipped(prepared.flatMap((p) => p.prep?.skipped ?? [])),
784
+ lint
785
+ };
786
+ }
787
+ function judge(id, answers, flat, expect) {
788
+ const entries = Object.entries(expect);
789
+ if (entries.length === 0) {
790
+ return { id, status: "captured", answers, ...flat.length ? { undecided: flat } : {} };
791
+ }
792
+ const failures = [];
793
+ const undecided = [];
794
+ for (const [question, expectation] of entries) {
795
+ const answer = answers[question];
796
+ const isFlat = flat.includes(question);
797
+ if (expectation.kind === "undecided") {
798
+ if (!isFlat)
799
+ failures.push({ question, expected: "undecided" });
800
+ } else if (isFlat) {
801
+ undecided.push(question);
802
+ } else if (!answer || !meets(question, answer, expectation)) {
803
+ failures.push({ question, expected: describeExpectation(expectation) });
804
+ }
805
+ }
806
+ const status = failures.length ? "fail" : undecided.length ? "undecided" : "pass";
807
+ return {
808
+ id,
809
+ status,
810
+ answers,
811
+ ...failures.length ? { failures } : {},
812
+ ...undecided.length ? { undecided } : {}
813
+ };
814
+ }
815
+
816
+ // ../core/dist/tools/spec-lint.js
817
+ import { basename, extname } from "node:path";
818
+ function runSpecLint(ctx, rawInput = {}) {
819
+ const input = checkInput("spec-lint", rawInput);
820
+ const specs = input.spec ? [named(ctx, input.spec)] : listSpecs(ctx.specDirs).filter((s) => !s.shadowed).map((s) => s.error ? { name: s.name, file: s.file, origin: s.origin, findings: [], invalid: s.error } : (
821
+ // Loaded by path, so keep the layer it was listed from.
822
+ { ...entry(loadSpec(s.file, ctx.specDirs, ctx.cwd)), origin: s.origin }
823
+ ));
824
+ const all = specs.flatMap((s) => s.findings);
825
+ const counts2 = {
826
+ specs: specs.length,
827
+ errors: all.filter((f) => f.severity === "error").length,
828
+ warnings: all.filter((f) => f.severity === "warning").length,
829
+ invalid: specs.filter((s) => s.invalid).length
830
+ };
831
+ return { specs, counts: counts2, passed: counts2.errors === 0 && counts2.invalid === 0 };
832
+ }
833
+ function named(ctx, ref) {
834
+ try {
835
+ return entry(loadSpec(ref, ctx.specDirs, ctx.cwd));
836
+ } catch (error) {
837
+ const file = isDecisionsError(error) ? error.details?.file : void 0;
838
+ if (typeof file !== "string")
839
+ throw error;
840
+ return {
841
+ name: basename(file, extname(file)),
842
+ file,
843
+ origin: "path",
844
+ findings: [],
845
+ invalid: error.message
846
+ };
847
+ }
848
+ }
849
+ function entry(spec) {
850
+ return { name: spec.name, file: spec.file, origin: spec.origin, findings: lintSpec(spec) };
851
+ }
852
+
853
+ // ../core/dist/tools/usage.js
854
+ function runUsage(ctx, rawInput = {}) {
855
+ const input = checkInput("usage", rawInput);
856
+ const since = input.since ? new Date(input.since) : void 0;
857
+ if (since && Number.isNaN(since.getTime())) {
858
+ throw new DecisionsError("invalid-request", `--since "${input.since}" is not a date`);
859
+ }
860
+ const session = input.session === "current" ? currentSession(ctx) : input.session;
861
+ const ledger = ledgerFor(ctx);
862
+ return {
863
+ basis: "measured",
864
+ ...ledger.summary({
865
+ ...session ? { session } : {},
866
+ ...since ? { since } : {}
867
+ }),
868
+ ledger: ledger.file,
869
+ ...session ? { session } : {},
870
+ ...input.since ? { since: input.since } : {}
871
+ };
872
+ }
873
+ function currentSession(ctx) {
874
+ if (ctx.config.session)
875
+ return ctx.config.session;
876
+ throw new DecisionsError("invalid-request", "no current session: not running under Claude Code, Codex or Pi, and SYSTEM1_SESSION is not set");
877
+ }
878
+
879
+ // ../core/dist/wiring.js
880
+ var DEFAULT_MODEL = DEFAULT_MODEL_ID;
881
+ function resolveConnection(env = process.env) {
882
+ return {
883
+ endpoint: nonEmpty(env.SYSTEM1_ENDPOINT) ?? DEFAULT_ENDPOINT,
884
+ model: nonEmpty(env.SYSTEM1_MODEL) ?? DEFAULT_MODEL,
885
+ apiKey: envKey(env),
886
+ replay: /^(1|true|yes)$/i.test(env.SYSTEM1_REPLAY ?? "")
887
+ };
888
+ }
889
+ function envKey(env) {
890
+ try {
891
+ return unquoteKey(env.OPENROUTER_API_KEY)?.value;
892
+ } catch {
893
+ return void 0;
894
+ }
895
+ }
896
+ function nonEmpty(value) {
897
+ return value === void 0 || value.trim() === "" ? void 0 : value.trim();
898
+ }
899
+
900
+ export {
901
+ probeUrl,
902
+ ping,
903
+ runMany,
904
+ runAsk,
905
+ runDoctor,
906
+ runOpportunitiesAdd,
907
+ runOpportunitiesList,
908
+ runOpportunitiesCheck,
909
+ runRoute,
910
+ runSpecCheck,
911
+ runSpecLint,
912
+ runUsage,
913
+ resolveConnection
914
+ };