@garygentry/system1 0.1.0 → 0.3.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.
@@ -6,7 +6,7 @@ import {
6
6
  VERSION,
7
7
  emit,
8
8
  extractFormat
9
- } from "./chunk-7LVHD6A5.mjs";
9
+ } from "./chunk-NUIZJRI6.mjs";
10
10
 
11
11
  // src/bin.ts
12
12
  import { readFileSync as readFileSync2 } from "node:fs";
@@ -50,7 +50,8 @@ Commands:
50
50
  usage Measured spend from the ledger (--session, --since)
51
51
  config Show resolved config; \`config egress allow|deny|status\` for consent
52
52
  spec list | show <name> | validate [name|path] | check <name> [--live]
53
- schema Print the JSON Schema of a tool's input (ask, many, usage, spec-check)
53
+ schema Print the JSON Schema of a tool's input (ask, many, usage, spec-check, route)
54
+ route Does a prompt call for the ask skill? (--text|--stdin|--hook; local, sends nothing)
54
55
  ping Check the endpoint is reachable (no key, no spend)
55
56
  doctor Check decide works from this shell; prints the fix for each problem
56
57
  version Print the version
@@ -91,21 +92,23 @@ async function main(argv, io) {
91
92
  }
92
93
  switch (command) {
93
94
  case "ask":
94
- return (await import("./decide-PW7REJ6Y.mjs")).runAskCommand(rest, io, format);
95
+ return (await import("./decide-XVVRKQ3V.mjs")).runAskCommand(rest, io, format);
95
96
  case "many":
96
- return (await import("./decide-PW7REJ6Y.mjs")).runManyCommand(rest, io, format);
97
+ return (await import("./decide-XVVRKQ3V.mjs")).runManyCommand(rest, io, format);
97
98
  case "usage":
98
- return (await import("./misc-6VZKJEN4.mjs")).runUsageCommand(rest, io, format);
99
+ return (await import("./misc-FHOVQJ2G.mjs")).runUsageCommand(rest, io, format);
99
100
  case "config":
100
- return (await import("./config-QR35VKVT.mjs")).runConfigCommand(rest, io, format);
101
+ return (await import("./config-REALJZDR.mjs")).runConfigCommand(rest, io, format);
101
102
  case "spec":
102
- return (await import("./spec-Z2LZJ5EZ.mjs")).runSpecCommand(rest, io, format);
103
+ return (await import("./spec-734MPBKB.mjs")).runSpecCommand(rest, io, format);
103
104
  case "schema":
104
- return (await import("./misc-6VZKJEN4.mjs")).runSchemaCommand(rest, io, format);
105
+ return (await import("./misc-FHOVQJ2G.mjs")).runSchemaCommand(rest, io, format);
106
+ case "route":
107
+ return (await import("./route-NMFOTXLI.mjs")).runRouteCommand(rest, io, format);
105
108
  case "ping":
106
- return (await import("./misc-6VZKJEN4.mjs")).runPingCommand(io, format);
109
+ return (await import("./misc-FHOVQJ2G.mjs")).runPingCommand(io, format);
107
110
  case "doctor":
108
- return (await import("./doctor-HAT6XVTW.mjs")).runDoctorCommand(io, format);
111
+ return (await import("./doctor-TKFVFCZV.mjs")).runDoctorCommand(io, format);
109
112
  case "version":
110
113
  case "--version":
111
114
  case "-v":
@@ -2,10 +2,10 @@ import { createRequire as __decideRequire } from "node:module"; const require =
2
2
  import {
3
3
  parseFileRef,
4
4
  parseQuestionSet
5
- } from "./chunk-V7OTO67I.mjs";
5
+ } from "./chunk-S5AIBUL7.mjs";
6
6
  import {
7
7
  DecisionsError
8
- } from "./chunk-7LVHD6A5.mjs";
8
+ } from "./chunk-NUIZJRI6.mjs";
9
9
 
10
10
  // src/args.ts
11
11
  import { readFileSync } from "node:fs";
@@ -145,7 +145,7 @@ function extractFormat(argv, fallback) {
145
145
  }
146
146
 
147
147
  // ../core/dist/version.js
148
- var VERSION = "0.1.0";
148
+ var VERSION = "0.3.0";
149
149
  var CLI_PACKAGE = "@garygentry/system1";
150
150
 
151
151
  export {
@@ -9,7 +9,7 @@ import {
9
9
  __require,
10
10
  __toESM,
11
11
  isDecisionsError
12
- } from "./chunk-7LVHD6A5.mjs";
12
+ } from "./chunk-NUIZJRI6.mjs";
13
13
 
14
14
  // ../../node_modules/.pnpm/yaml@2.9.1/node_modules/yaml/dist/nodes/identity.js
15
15
  var require_identity = __commonJS({
@@ -9165,6 +9165,107 @@ function escapeRegExp(text) {
9165
9165
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9166
9166
  }
9167
9167
 
9168
+ // ../core/dist/route/route.js
9169
+ var ROUTE_DEFAULTS = {
9170
+ enabled: true,
9171
+ builtin: true,
9172
+ disable: [],
9173
+ triggers: [],
9174
+ ignore: []
9175
+ };
9176
+ var both = (...parts) => `^${parts.map((p) => `(?=[\\s\\S]*?(${p}))`).join("")}`;
9177
+ var ITEMS = "tickets?|reviews?|commits?|failures?|errors?|lines|rows?|entries|items?|issues?|comments?|messages?|emails?|commands?|records?|files?|tests?|alerts?|findings?|results?|prs?|pull requests?|responses?|reports?|incidents?|warnings?|changes?|hunks?|packages?|candidates?";
9178
+ var FILE = "\\b[\\w./-]+\\.(?:jsonl|ndjson|csv|tsv|txt|log|sh)\\b";
9179
+ var BATCH = `\\b(?:each|every|all\\s+(?:the\\s+|of\\s+the\\s+)?(?:\\d+\\s+)?(?:${ITEMS})|line\\s+by\\s+line|one\\s+by\\s+one)\\b|\\b\\d{2,}\\s+(?:[\\w-]+\\s+){0,2}(?:${ITEMS})\\b`;
9180
+ var RULESET = "\\b(?:rules?|criteri(?:a|on)|requirements?|checklist|polic(?:y|ies)|guidelines?|conventions?|standards?|our\\s+bar|TASK\\.md)\\b";
9181
+ var EVIDENCE = "\\b(?:diff|uncommitted|working\\s+tree|staged|changes?|patch|pr|pull\\s+request|branch|test[-\\s]output(?:\\.log)?|test\\s+results?|TASK\\.md)\\b";
9182
+ var GATES = "commit|merge|push|ship|release|approve|deploy|land|run";
9183
+ var BUILTIN_TRIGGERS = [
9184
+ {
9185
+ // "triage every CI failure", "go through tickets.jsonl and flag any where…",
9186
+ // "which of these would pass on a rerun", "the ones complaining about…"
9187
+ name: "batch-judgement",
9188
+ pattern: both(
9189
+ // A verb glued to a hyphen is another word: "rate-limit", "score-board".
9190
+ `\\b(?:(?:classify|categori[sz]e|label|triage|bucket|grade|rate|score|rank|screen|vet|judge|assess)(?!-)|sort\\s+out|route\\s+(?:each|every|all|these|those|the)|flag\\s+(?:any|all|every|each|which|the\\s+ones|anything)|mark\\s+(?:any|all|every|each|which|the\\s+ones)|split\\s+(?:\\w+\\s+)?into|pull\\s+out\\s+(?:the|any|all|every)|tell\\s+me\\s+which|which\\s+of\\s+(?:them|these|those|the\\s+\\w+)|which\\s+ones|which\\s+(?:${ITEMS})\\s+(?:could|would|might|should|are|is|will|look|seem|need)|the\\s+ones\\s+(?:that|where|which|who|with|about|complaining|mentioning)|anything\\s+that\\s+(?:looks|seems|sounds|reads)|is\\s+each|are\\s+any\\s+of|whether\\s+each|verdicts?)\\b`,
9191
+ `${FILE}|${BATCH}`
9192
+ )
9193
+ },
9194
+ {
9195
+ // "250 packages. Which single one…", "which of the packages is the best fit"
9196
+ name: "pick-from-many",
9197
+ pattern: both("\\b(?:best\\s+(?:fit|match|one|option|choice)|which\\s+(?:single\\s+)?one|single\\s+best|pick\\s+(?:the|one)|choose\\s+(?:the|one))\\b", `${FILE}|\\b\\d{2,}\\s+(?:[\\w-]+\\s+){0,2}(?:${ITEMS}|libraries|options|helpers|modules|functions)\\b|\\bout\\s+of\\s+(?:all|the|these)\\b`)
9198
+ },
9199
+ {
9200
+ // "check the diff against these rules", "does it meet the acceptance
9201
+ // criteria", "grade each criterion pass or fail", "hold the diff to our rules"
9202
+ name: "criteria-check",
9203
+ pattern: both("\\b(?:against\\s+(?:our|the|these|this|my|each|every)?\\s*(?:[\\w-]+\\s+){0,2}(?:rules?|criteri(?:a|on)|requirements?|checklist|polic(?:y|ies)|guidelines?|conventions?|standards?|spec)|hold\\s+(?:it|this|them|the\\s+[\\w-]+(?:\\s+[\\w-]+)?)\\s+(?:up\\s+)?(?:to|against)|meets?|satisf(?:y|ies)|violat\\w*|compl(?:y|ies|iant)|pass(?:es)?\\s+(?:our|the)\\s+(?:bar|rules|checks?|checklist)|pass\\s+or\\s+fail|pass/fail|per[-\\s]criteri(?:on|a)|tick\\s+off|(?:one|a|separate)\\s+verdict|verdicts?\\s+(?:for|on)|grade\\s+(?:each|every|them|it)|(?:each|every)\\s+(?:acceptance\\s+)?(?:criteri(?:on|a)|rule|requirement|checklist\\s+item|item))\\b", RULESET)
9204
+ },
9205
+ {
9206
+ // "is the task actually done?", "am i done?", "ready to merge?", "can I ship this"
9207
+ name: "done-check",
9208
+ pattern: both("\\b(?:am\\s+i\\s+(?:actually\\s+|really\\s+)?done|are\\s+we\\s+(?:actually\\s+|really\\s+)?done|is\\s+(?:it|this|that|(?:the|my|our)\\s+\\w+(?!\\s+(?:description|message|title|name|docs?|readme|summary|text|comment)))(?:\\s+[\\w.]+){0,4}?\\s+(?:actually\\s+|really\\s+|fully\\s+|truly\\s+)?(?:done|complete|finished)|(?:ready|good|ok|safe)\\s+to\\s+(?:merge|ship|commit|push|land|release)|can\\s+(?:i|we)\\s+(?:ship|merge|land|commit|push)\\s+(?:this|it)|(?:about|going)\\s+to\\s+(?:open\\s+a\\s+pr|merge|ship|push|commit|release))\\b", `${EVIDENCE}|${FILE}`, "\\?|\\b(?:check|verify|confirm|tell\\s+me|make\\s+sure|grade|hold)\\b")
9209
+ },
9210
+ {
9211
+ // "before I commit… is it safe?", "which commands could lose data before the bot runs it"
9212
+ name: "gate-check",
9213
+ pattern: both(`\\bbefore\\s+(?:i|we|the\\s+\\w+)\\s+(?:${GATES})s?\\b`, `${RULESET}|\\b(?:safe|destructive|verdict|violat\\w*|could\\s+(?:lose|break|delete|destroy|wipe))\\b`)
9214
+ }
9215
+ ];
9216
+ var BUILTIN_IGNORE = [
9217
+ "\\b(?:don'?t|do\\s+not|without)\\s+(?:use\\s+|using\\s+)?(?:system\\s?1|decide)\\b"
9218
+ ];
9219
+ var DEFAULT_MESSAGE = "System 1 routing hint ({triggers}): the user wants closed verdicts over text. Load the system1:ask skill now, before you read the files or the diff, and let `decide` give the verdicts; do not grade them yourself. Skip this only if the user said not to use System 1.";
9220
+ function compilePattern(pattern, where) {
9221
+ try {
9222
+ return new RegExp(pattern, "i");
9223
+ } catch (error) {
9224
+ throw new DecisionsError("config-error", `${where}: invalid regular expression ${JSON.stringify(pattern)}: ${error.message}`, { pattern });
9225
+ }
9226
+ }
9227
+ function activeTriggers(config) {
9228
+ const unknown = config.disable.filter((n) => !BUILTIN_TRIGGERS.some((t) => t.name === n));
9229
+ if (unknown.length > 0) {
9230
+ throw new DecisionsError("config-error", `route.disable names unknown built-in trigger(s): ${unknown.join(", ")}. Built-ins: ${BUILTIN_TRIGGERS.map((t) => t.name).join(", ")}`);
9231
+ }
9232
+ const builtins = config.builtin ? BUILTIN_TRIGGERS.filter((t) => !config.disable.includes(t.name)) : [];
9233
+ return [
9234
+ ...builtins.map((t) => ({ ...t, source: "builtin" })),
9235
+ ...config.triggers.map((t) => ({ ...t, source: "config" }))
9236
+ ];
9237
+ }
9238
+ function route(prompt, config) {
9239
+ const triggers = activeTriggers(config);
9240
+ const ignore = [...config.builtin ? BUILTIN_IGNORE : [], ...config.ignore];
9241
+ const compiled = triggers.map((t) => ({
9242
+ ...t,
9243
+ re: compilePattern(t.pattern, `route trigger "${t.name}"`)
9244
+ }));
9245
+ const vetoes = ignore.map((p) => ({ p, re: compilePattern(p, "route.ignore") }));
9246
+ if (!config.enabled)
9247
+ return { enabled: false, matched: false, triggers: [] };
9248
+ const hits = [];
9249
+ for (const t of compiled) {
9250
+ const m = t.re.exec(prompt);
9251
+ const text = m?.[0] || m?.slice(1).filter(Boolean).join(" \u2026 ");
9252
+ if (m)
9253
+ hits.push({ name: t.name, source: t.source, text: (text ?? "").slice(0, 120) });
9254
+ }
9255
+ if (hits.length === 0)
9256
+ return { enabled: true, matched: false, triggers: [] };
9257
+ const veto = vetoes.find((v) => v.re.test(prompt));
9258
+ if (veto)
9259
+ return { enabled: true, matched: false, triggers: hits, ignoredBy: veto.p };
9260
+ const names = hits.map((h) => h.name).join(", ");
9261
+ return {
9262
+ enabled: true,
9263
+ matched: true,
9264
+ triggers: hits,
9265
+ message: (config.message ?? DEFAULT_MESSAGE).replaceAll("{triggers}", names)
9266
+ };
9267
+ }
9268
+
9168
9269
  // ../core/dist/model/validate.js
9169
9270
  function assertQuestionSet(questions) {
9170
9271
  const problems = [];
@@ -9423,7 +9524,8 @@ var DEFAULTS = {
9423
9524
  timeoutMs: DEFAULT_TIMEOUT_MS,
9424
9525
  budget: { maxCalls: 200, maxUsd: 0.05 },
9425
9526
  egress: { consent: { granted: false }, exclude: [] },
9426
- profiles: []
9527
+ profiles: [],
9528
+ route: ROUTE_DEFAULTS
9427
9529
  };
9428
9530
  function findRepoRoot(cwd) {
9429
9531
  let dir = resolve(cwd);
@@ -9470,7 +9572,8 @@ function loadConfig(options = {}) {
9470
9572
  ...stringList(repo, "egress.exclude", repoFile)
9471
9573
  ]
9472
9574
  },
9473
- profiles: [...profileList(user, userFile), ...profileList(repo, repoFile)]
9575
+ profiles: [...profileList(user, userFile), ...profileList(repo, repoFile)],
9576
+ route: readRoute(user, userFile, repo, repoFile, env)
9474
9577
  };
9475
9578
  const key = resolveApiKey(env, userDir);
9476
9579
  const session = detectSession(env);
@@ -9520,9 +9623,9 @@ function pick(key, type, ...layers2) {
9520
9623
  }
9521
9624
  return void 0;
9522
9625
  }
9523
- function pickIn(section, key, ...layers2) {
9626
+ function pickIn(section2, key, ...layers2) {
9524
9627
  for (const layer of layers2) {
9525
- const value = layer?.[section]?.[key];
9628
+ const value = layer?.[section2]?.[key];
9526
9629
  if (typeof value === "number" && value >= 0)
9527
9630
  return value;
9528
9631
  }
@@ -9580,6 +9683,69 @@ function profileList(layer, file) {
9580
9683
  };
9581
9684
  });
9582
9685
  }
9686
+ function readRoute(user, userFile, repo, repoFile, env) {
9687
+ const sections = [
9688
+ { layer: section(user, "route", userFile), file: userFile },
9689
+ { layer: section(repo, "route", repoFile), file: repoFile }
9690
+ ];
9691
+ const known = ["enabled", "builtin", "disable", "triggers", "ignore", "message"];
9692
+ const route2 = { ...ROUTE_DEFAULTS, disable: [], triggers: [], ignore: [] };
9693
+ for (const { layer, file } of sections) {
9694
+ if (!layer)
9695
+ continue;
9696
+ const extra = Object.keys(layer).filter((k) => !known.includes(k));
9697
+ if (extra.length > 0) {
9698
+ throw new DecisionsError("config-error", `${file}: unknown route key(s) ${extra.join(", ")}. Known: ${known.join(", ")}`, { file });
9699
+ }
9700
+ for (const key of ["enabled", "builtin"]) {
9701
+ const v = layer[key];
9702
+ if (v === void 0)
9703
+ continue;
9704
+ if (typeof v !== "boolean")
9705
+ throw new DecisionsError("config-error", `${file}: route.${key} must be true or false`, {
9706
+ file
9707
+ });
9708
+ route2[key] = v;
9709
+ }
9710
+ if (layer.message !== void 0) {
9711
+ if (typeof layer.message !== "string" || !layer.message.trim())
9712
+ throw new DecisionsError("config-error", `${file}: route.message must be text`, { file });
9713
+ route2.message = layer.message;
9714
+ }
9715
+ for (const key of ["disable", "ignore"]) {
9716
+ const v = layer[key];
9717
+ if (v === void 0)
9718
+ continue;
9719
+ if (!Array.isArray(v) || !v.every((x) => typeof x === "string"))
9720
+ throw new DecisionsError("config-error", `${file}: route.${key} must be a list of strings`, { file });
9721
+ route2[key].push(...v);
9722
+ }
9723
+ if (layer.triggers !== void 0)
9724
+ route2.triggers.push(...triggerList(layer.triggers, file));
9725
+ }
9726
+ if (/^(0|off|false|no)$/i.test(env.SYSTEM1_ROUTE ?? ""))
9727
+ route2.enabled = false;
9728
+ return route2;
9729
+ }
9730
+ function section(layer, key, file) {
9731
+ const value = layer?.[key];
9732
+ if (value === void 0 || value === null)
9733
+ return void 0;
9734
+ if (typeof value !== "object" || Array.isArray(value))
9735
+ throw new DecisionsError("config-error", `${file}: ${key} must be a mapping`, { file });
9736
+ return value;
9737
+ }
9738
+ function triggerList(value, file) {
9739
+ if (!Array.isArray(value))
9740
+ throw new DecisionsError("config-error", `${file}: route.triggers must be a list`, { file });
9741
+ return value.map((t, i) => {
9742
+ const { name, pattern } = t ?? {};
9743
+ if (typeof name !== "string" || !name.trim() || typeof pattern !== "string" || !pattern) {
9744
+ throw new DecisionsError("config-error", `${file}: route.triggers[${i}] needs a name and a pattern (a regular expression)`, { file });
9745
+ }
9746
+ return { name, pattern };
9747
+ });
9748
+ }
9583
9749
  function resolveApiKey(env, userDir) {
9584
9750
  const fromEnv = nonEmpty2(env.OPENROUTER_API_KEY);
9585
9751
  if (fromEnv)
@@ -9618,7 +9784,7 @@ function setConsent(repoRoot, granted, by, now = /* @__PURE__ */ new Date()) {
9618
9784
  function assertConsent(consent, repoRoot) {
9619
9785
  if (consent.granted)
9620
9786
  return;
9621
- throw new DecisionsError("egress-refused", `This repo has not agreed to send content to the decision model's provider. Run \`decide config egress allow\` in ${repoRoot} (or use the setup skill) to consent. Replay of recorded answers works without consent.`, { repoRoot });
9787
+ throw new DecisionsError("egress-refused", `This repo has not agreed to send content to the decision model's provider. Consent is the user's: they run \`decide config egress allow\` in ${repoRoot} themselves (the setup skill explains what gets sent). Replay of recorded answers works without consent.`, { repoRoot });
9622
9788
  }
9623
9789
 
9624
9790
  // ../core/dist/egress/exclude.js
@@ -18822,7 +18988,12 @@ function resolvePath(cwd, path) {
18822
18988
  realAbsolute = realpathSync2(absolute);
18823
18989
  } catch {
18824
18990
  }
18825
- const real = toRel(cwd, realAbsolute);
18991
+ let realCwd = cwd;
18992
+ try {
18993
+ realCwd = realpathSync2(cwd);
18994
+ } catch {
18995
+ }
18996
+ const real = toRel(realCwd, realAbsolute);
18826
18997
  return {
18827
18998
  absolute,
18828
18999
  display: toRel(cwd, absolute),
@@ -18951,7 +19122,7 @@ function gitIgnored(cwd, paths) {
18951
19122
  if (error.status === 1)
18952
19123
  return [];
18953
19124
  if (error.signal === "SIGKILL") {
18954
- throw new DecisionsError("source-error", `git check-ignore did not answer within ${GIT_TIMEOUT_MS} ms. Name the files with --file, or run where git works.`);
19125
+ throw new DecisionsError("source-error", `git check-ignore did not answer within ${GIT_TIMEOUT_MS} ms, which usually means a sandbox is blocking child processes (\`decide doctor\` prints the fix for Codex). Name the files with --file, or run where git works.`);
18955
19126
  }
18956
19127
  throw new DecisionsError("source-error", `git check-ignore failed: ${String(error)}`);
18957
19128
  }
@@ -19564,11 +19735,16 @@ var SpecCheckInput = typebox_exports.Object({
19564
19735
  /** Proceed past the spend guard. */
19565
19736
  confirm: typebox_exports.Optional(typebox_exports.Boolean())
19566
19737
  }, strict);
19738
+ var RouteInput = typebox_exports.Object({
19739
+ /** The user's prompt, as the harness hook received it. */
19740
+ prompt: typebox_exports.String()
19741
+ }, strict);
19567
19742
  var TOOL_SCHEMAS = {
19568
19743
  ask: AskInput,
19569
19744
  many: ManyInput,
19570
19745
  usage: UsageInput,
19571
- "spec-check": SpecCheckInput
19746
+ "spec-check": SpecCheckInput,
19747
+ route: RouteInput
19572
19748
  };
19573
19749
 
19574
19750
  // ../core/dist/tools/context.js
@@ -19756,7 +19932,8 @@ function createDecider(options) {
19756
19932
  const started = performance.now();
19757
19933
  const record = fixtures?.lookup(namespace, request);
19758
19934
  if (!record) {
19759
- throw new DecisionsError("replay-miss", `No recorded answer for this request (namespace "${namespace}", key ${key.slice(0, 12)}\u2026). Record it with a live call, or set OPENROUTER_API_KEY.`, { namespace, key });
19935
+ const where = `(namespace "${namespace}", key ${key.slice(0, 12)}\u2026)`;
19936
+ throw new DecisionsError("replay-miss", transport ? `No recorded answer for this request ${where}. Record it with a live call (without --replay or SYSTEM1_REPLAY).` : `No API key is set, so only recorded answers can be used, and this request has none ${where}. Set OPENROUTER_API_KEY for a live answer (the setup skill shows where it goes).`, { namespace, key });
19760
19937
  }
19761
19938
  log2("replay", profile.id, ZERO);
19762
19939
  return {
@@ -20217,6 +20394,31 @@ async function runAsk(ctx, rawInput) {
20217
20394
  // ../core/dist/tools/doctor.js
20218
20395
  import { accessSync, constants, statSync as statSync4 } from "node:fs";
20219
20396
  import { delimiter, join as join5 } from "node:path";
20397
+ function routeCheck(config) {
20398
+ try {
20399
+ route("", config);
20400
+ if (!config.enabled) {
20401
+ return {
20402
+ name: "route",
20403
+ status: "ok",
20404
+ detail: "routing hints are off (route.enabled or SYSTEM1_ROUTE)"
20405
+ };
20406
+ }
20407
+ const names = activeTriggers(config).map((t) => t.name);
20408
+ return {
20409
+ name: "route",
20410
+ status: "ok",
20411
+ detail: `routing hints on: ${names.length ? names.join(", ") : "no triggers"}`
20412
+ };
20413
+ } catch (error) {
20414
+ return {
20415
+ name: "route",
20416
+ status: "warn",
20417
+ detail: `routing hints are silent: ${error instanceof Error ? error.message : String(error)}`,
20418
+ fix: 'correct route: in the config file (test it with `decide route --text "\u2026"`)'
20419
+ };
20420
+ }
20421
+ }
20220
20422
  var CODEX_RULE = 'prefix_rule(pattern = ["decide"], decision = "allow")';
20221
20423
  async function runDoctor(options) {
20222
20424
  const { env } = options;
@@ -20259,7 +20461,8 @@ async function runDoctor(options) {
20259
20461
  name: "key",
20260
20462
  status: "warn",
20261
20463
  detail: "no API key: only replay works",
20262
- fix: "set OPENROUTER_API_KEY, or write `openrouter_api_key: <key>` to ~/.config/system1/credentials (chmod 600)"
20464
+ // The file this shell would read: XDG_CONFIG_HOME moves it.
20465
+ fix: `set OPENROUTER_API_KEY, or write \`openrouter_api_key: <key>\` to ${join5(userConfigDir(env, options.home), "credentials")} (chmod 600)`
20263
20466
  });
20264
20467
  checks.push(config.egress.consent.granted ? { name: "consent", status: "ok", detail: `egress consent granted for ${config.repoRoot}` } : {
20265
20468
  name: "consent",
@@ -20267,6 +20470,7 @@ async function runDoctor(options) {
20267
20470
  detail: `no egress consent for ${config.repoRoot}: live calls are refused`,
20268
20471
  fix: "the user runs `decide config egress allow` in this repo, if they agree"
20269
20472
  });
20473
+ checks.push(routeCheck(config.route));
20270
20474
  let reach;
20271
20475
  try {
20272
20476
  reach = await ping({
@@ -20387,6 +20591,33 @@ function which(name, path) {
20387
20591
  return void 0;
20388
20592
  }
20389
20593
 
20594
+ // ../core/dist/tools/route.js
20595
+ function runRoute(options, rawInput) {
20596
+ const problems = checkRouteInput(rawInput);
20597
+ if (problems.length > 0) {
20598
+ throw new DecisionsError("invalid-request", `Invalid route input:
20599
+ ${problems.join("\n ")}`, {
20600
+ problems
20601
+ });
20602
+ }
20603
+ const input = rawInput;
20604
+ const config = loadConfig(options);
20605
+ const { user, repo } = config.layers;
20606
+ return {
20607
+ ...route(input.prompt, config.route),
20608
+ files: { ...user ? { user } : {}, ...repo ? { repo } : {} }
20609
+ };
20610
+ }
20611
+ function checkRouteInput(input) {
20612
+ if (typeof input !== "object" || input === null || Array.isArray(input))
20613
+ return ["/ must be an object"];
20614
+ const extra = Object.keys(input).filter((k) => k !== "prompt");
20615
+ return [
20616
+ ...typeof input.prompt === "string" ? [] : ["/prompt must be a string"],
20617
+ ...extra.map((k) => `/${k} is not allowed`)
20618
+ ];
20619
+ }
20620
+
20390
20621
  // ../core/dist/tools/spec-check.js
20391
20622
  async function runSpecCheck(ctx, rawInput) {
20392
20623
  const input = checkInput("spec-check", rawInput);
@@ -20577,6 +20808,7 @@ export {
20577
20808
  runMany,
20578
20809
  runAsk,
20579
20810
  runDoctor,
20811
+ runRoute,
20580
20812
  runSpecCheck,
20581
20813
  runUsage,
20582
20814
  resolveConnection
@@ -1,18 +1,18 @@
1
1
  import { createRequire as __decideRequire } from "node:module"; const require = __decideRequire(import.meta.url);
2
2
  import {
3
3
  typedParse
4
- } from "./chunk-A7ODZFMY.mjs";
4
+ } from "./chunk-4BSTNWLV.mjs";
5
5
  import {
6
6
  DEFAULT_EXCLUDES,
7
7
  createContext,
8
8
  loadConfig,
9
9
  repoConfigPath,
10
10
  setConsent
11
- } from "./chunk-V7OTO67I.mjs";
11
+ } from "./chunk-S5AIBUL7.mjs";
12
12
  import {
13
13
  DecisionsError,
14
14
  emit
15
- } from "./chunk-7LVHD6A5.mjs";
15
+ } from "./chunk-NUIZJRI6.mjs";
16
16
 
17
17
  // src/commands/config.ts
18
18
  function runConfigCommand(argv, io, format) {
@@ -24,11 +24,12 @@ function runConfigCommand(argv, io, format) {
24
24
  (r, f) => f === "brief" ? brief(r) : void 0
25
25
  );
26
26
  }
27
+ var CONFIG_OPTIONS = { confirm: { type: "boolean" }, by: { type: "string" } };
27
28
  function body(argv, io) {
28
29
  const { values, positionals } = typedParse({
29
30
  args: argv,
30
31
  allowPositionals: true,
31
- options: { confirm: { type: "boolean" }, by: { type: "string" } }
32
+ options: CONFIG_OPTIONS
32
33
  });
33
34
  const [section = "show", action = "status"] = positionals;
34
35
  const cwd = io.cwd ?? process.cwd();
@@ -112,5 +113,6 @@ function brief(r) {
112
113
  ].join("\n");
113
114
  }
114
115
  export {
116
+ CONFIG_OPTIONS,
115
117
  runConfigCommand
116
118
  };
@@ -6,17 +6,17 @@ import {
6
6
  import {
7
7
  buildInput,
8
8
  parseDecideFlags
9
- } from "./chunk-A7ODZFMY.mjs";
9
+ } from "./chunk-4BSTNWLV.mjs";
10
10
  import {
11
11
  createContext,
12
12
  runAsk,
13
13
  runMany
14
- } from "./chunk-V7OTO67I.mjs";
14
+ } from "./chunk-S5AIBUL7.mjs";
15
15
  import {
16
16
  DecisionsError,
17
17
  ENVELOPE_VERSION,
18
18
  emit
19
- } from "./chunk-7LVHD6A5.mjs";
19
+ } from "./chunk-NUIZJRI6.mjs";
20
20
 
21
21
  // src/commands/decide.ts
22
22
  import { readFileSync } from "node:fs";
@@ -1,10 +1,10 @@
1
1
  import { createRequire as __decideRequire } from "node:module"; const require = __decideRequire(import.meta.url);
2
2
  import {
3
3
  runDoctor
4
- } from "./chunk-V7OTO67I.mjs";
4
+ } from "./chunk-S5AIBUL7.mjs";
5
5
  import {
6
6
  emit
7
- } from "./chunk-7LVHD6A5.mjs";
7
+ } from "./chunk-NUIZJRI6.mjs";
8
8
 
9
9
  // src/commands/doctor.ts
10
10
  import { execFile } from "node:child_process";
@@ -25,7 +25,7 @@ function runDoctorCommand(io, format) {
25
25
  }
26
26
  function brief(r) {
27
27
  const lines = [
28
- `decide doctor: ${r.healthy ? "healthy" : "PROBLEMS FOUND"} \xB7 ${r.live ? "live ready" : "replay only"} \xB7 harness ${r.harness ?? "none"} \xB7 session ${r.session ?? "none"}`
28
+ `decide doctor: ${headline(r)} \xB7 ${r.live ? "live ready" : "replay only"} \xB7 harness ${r.harness ?? "none"} \xB7 session ${r.session ?? "none"}`
29
29
  ];
30
30
  for (const c of r.checks) {
31
31
  lines.push(` ${c.status.padEnd(4)} ${c.name}: ${c.detail}`);
@@ -33,6 +33,11 @@ function brief(r) {
33
33
  }
34
34
  return lines.join("\n");
35
35
  }
36
+ function headline(r) {
37
+ if (!r.healthy) return "PROBLEMS FOUND";
38
+ const missing = r.checks.filter((c) => c.status === "warn").map((c) => c.name);
39
+ return !r.live && missing.length > 0 ? `SETUP NEEDED (${missing.join(", ")})` : "healthy";
40
+ }
36
41
  function probeVersion(path, env, timeoutMs = 2e3) {
37
42
  return new Promise((resolve) => {
38
43
  let settled = false;
@@ -1,19 +1,19 @@
1
1
  import { createRequire as __decideRequire } from "node:module"; const require = __decideRequire(import.meta.url);
2
2
  import {
3
3
  typedParse
4
- } from "./chunk-A7ODZFMY.mjs";
4
+ } from "./chunk-4BSTNWLV.mjs";
5
5
  import {
6
6
  TOOL_SCHEMAS,
7
7
  createContext,
8
8
  ping,
9
9
  resolveConnection,
10
10
  runUsage
11
- } from "./chunk-V7OTO67I.mjs";
11
+ } from "./chunk-S5AIBUL7.mjs";
12
12
  import {
13
13
  DecisionsError,
14
14
  EXIT,
15
15
  emit
16
- } from "./chunk-7LVHD6A5.mjs";
16
+ } from "./chunk-NUIZJRI6.mjs";
17
17
 
18
18
  // src/commands/misc.ts
19
19
  function runSchemaCommand(argv, io, format) {
@@ -28,6 +28,7 @@ function runSchemaCommand(argv, io, format) {
28
28
  return TOOL_SCHEMAS[tool];
29
29
  });
30
30
  }
31
+ var USAGE_OPTIONS = { session: { type: "string" }, since: { type: "string" } };
31
32
  function runUsageCommand(argv, io, format) {
32
33
  return emit(
33
34
  io,
@@ -36,7 +37,7 @@ function runUsageCommand(argv, io, format) {
36
37
  () => {
37
38
  const { values } = typedParse({
38
39
  args: argv,
39
- options: { session: { type: "string" }, since: { type: "string" } }
40
+ options: USAGE_OPTIONS
40
41
  });
41
42
  const ctx = createContext({ ...io, cwd: io.cwd ?? process.cwd(), env: io.env });
42
43
  return runUsage(ctx, {
@@ -72,6 +73,7 @@ async function runPingCommand(io, format) {
72
73
  return failed ? EXIT.providerError : code;
73
74
  }
74
75
  export {
76
+ USAGE_OPTIONS,
75
77
  runPingCommand,
76
78
  runSchemaCommand,
77
79
  runUsageCommand
@@ -0,0 +1,64 @@
1
+ import { createRequire as __decideRequire } from "node:module"; const require = __decideRequire(import.meta.url);
2
+ import {
3
+ typedParse
4
+ } from "./chunk-4BSTNWLV.mjs";
5
+ import {
6
+ runRoute
7
+ } from "./chunk-S5AIBUL7.mjs";
8
+ import {
9
+ DecisionsError,
10
+ emit
11
+ } from "./chunk-NUIZJRI6.mjs";
12
+
13
+ // src/commands/route.ts
14
+ import { readFileSync } from "node:fs";
15
+ var ROUTE_OPTIONS = {
16
+ text: { type: "string" },
17
+ stdin: { type: "boolean" },
18
+ hook: { type: "boolean" }
19
+ };
20
+ function runRouteCommand(argv, io, format) {
21
+ return emit(
22
+ io,
23
+ "route",
24
+ format,
25
+ () => {
26
+ const { values } = typedParse({ args: argv, options: ROUTE_OPTIONS });
27
+ const given = [values.text !== void 0, values.stdin, values.hook].filter(Boolean).length;
28
+ if (given !== 1) {
29
+ throw new DecisionsError(
30
+ "invalid-request",
31
+ "Usage: decide route --text <prompt> | --stdin | --hook (exactly one)"
32
+ );
33
+ }
34
+ const read = io.readStdin ?? (() => readFileSync(0, "utf8"));
35
+ let prompt = values.text;
36
+ let cwd = io.cwd ?? process.cwd();
37
+ if (values.stdin) prompt = read();
38
+ if (values.hook) {
39
+ const event = parseHookEvent(read());
40
+ prompt = event.prompt;
41
+ if (event.cwd) cwd = event.cwd;
42
+ }
43
+ return runRoute({ cwd, env: io.env, ...io.home ? { home: io.home } : {} }, { prompt });
44
+ },
45
+ (r, f) => f === "brief" ? r.message ?? "" : void 0
46
+ );
47
+ }
48
+ function parseHookEvent(text) {
49
+ let event;
50
+ try {
51
+ event = JSON.parse(text);
52
+ } catch {
53
+ throw new DecisionsError("invalid-request", "--hook expects a JSON hook event on stdin");
54
+ }
55
+ const { prompt, cwd } = event ?? {};
56
+ if (typeof prompt !== "string") {
57
+ throw new DecisionsError("invalid-request", "--hook: the event has no `prompt` string");
58
+ }
59
+ return { prompt, ...typeof cwd === "string" && cwd ? { cwd } : {} };
60
+ }
61
+ export {
62
+ ROUTE_OPTIONS,
63
+ runRouteCommand
64
+ };
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-5LVOFZF3.mjs";
5
5
  import {
6
6
  typedParse
7
- } from "./chunk-A7ODZFMY.mjs";
7
+ } from "./chunk-4BSTNWLV.mjs";
8
8
  import {
9
9
  assertExamples,
10
10
  createContext,
@@ -12,11 +12,11 @@ import {
12
12
  listSpecs,
13
13
  loadSpec,
14
14
  runSpecCheck
15
- } from "./chunk-V7OTO67I.mjs";
15
+ } from "./chunk-S5AIBUL7.mjs";
16
16
  import {
17
17
  DecisionsError,
18
18
  emit
19
- } from "./chunk-7LVHD6A5.mjs";
19
+ } from "./chunk-NUIZJRI6.mjs";
20
20
 
21
21
  // src/commands/spec.ts
22
22
  function runSpecCommand(argv, io, format) {
@@ -105,19 +105,21 @@ function checkInput(argv) {
105
105
  ...values.model ? { model: values.model } : {}
106
106
  };
107
107
  }
108
+ var SPEC_OPTIONS = {
109
+ live: { type: "boolean" },
110
+ replay: { type: "boolean" },
111
+ confirm: { type: "boolean" },
112
+ model: { type: "string" }
113
+ };
108
114
  function parse(argv) {
109
115
  return typedParse({
110
116
  args: argv,
111
117
  allowPositionals: true,
112
118
  strict: true,
113
- options: {
114
- live: { type: "boolean" },
115
- replay: { type: "boolean" },
116
- confirm: { type: "boolean" },
117
- model: { type: "string" }
118
- }
119
+ options: SPEC_OPTIONS
119
120
  });
120
121
  }
121
122
  export {
123
+ SPEC_OPTIONS,
122
124
  runSpecCommand
123
125
  };
@@ -4,4 +4,4 @@ import { createRequire as __decideRequire } from "node:module"; const require =
4
4
  // src/entry.ts
5
5
  import module from "node:module";
6
6
  module.enableCompileCache?.();
7
- await import("./chunks/bin-IIUCANF7.mjs");
7
+ await import("./chunks/bin-TCIHP5PC.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@garygentry/system1",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The `decide` CLI: typed, calibrated decisions for coding agents, hooks and CI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,14 +20,14 @@
20
20
  ],
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "https://github.com/garygentry/system1",
23
+ "url": "git+https://github.com/garygentry/system1.git",
24
24
  "directory": "packages/cli"
25
25
  },
26
26
  "engines": {
27
27
  "node": ">=22"
28
28
  },
29
29
  "bin": {
30
- "decide": "./dist/bundle/decide.mjs"
30
+ "decide": "dist/bundle/decide.mjs"
31
31
  },
32
32
  "files": [
33
33
  "dist/bundle",