@garygentry/system1 0.3.2 → 0.4.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.
@@ -9,7 +9,7 @@ import {
9
9
  __require,
10
10
  __toESM,
11
11
  isDecisionsError
12
- } from "./chunk-4ZZ3ZSJN.mjs";
12
+ } from "./chunk-WZIVOY5Q.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({
@@ -8969,9 +8969,9 @@ var require_picomatch = __commonJS({
8969
8969
  var utils = require_utils();
8970
8970
  var constants2 = require_constants();
8971
8971
  var isObject2 = (val) => val && typeof val === "object" && !Array.isArray(val);
8972
- var picomatch3 = (glob2, options, returnState = false) => {
8972
+ var picomatch4 = (glob2, options, returnState = false) => {
8973
8973
  if (Array.isArray(glob2)) {
8974
- const fns = glob2.map((input) => picomatch3(input, options, returnState));
8974
+ const fns = glob2.map((input) => picomatch4(input, options, returnState));
8975
8975
  const arrayMatcher = (str) => {
8976
8976
  for (const isMatch of fns) {
8977
8977
  const state2 = isMatch(str);
@@ -8987,16 +8987,16 @@ var require_picomatch = __commonJS({
8987
8987
  }
8988
8988
  const opts = options || {};
8989
8989
  const posix2 = opts.windows;
8990
- const regex = isState ? picomatch3.compileRe(glob2, options) : picomatch3.makeRe(glob2, options, false, true);
8990
+ const regex = isState ? picomatch4.compileRe(glob2, options) : picomatch4.makeRe(glob2, options, false, true);
8991
8991
  const state = regex.state;
8992
8992
  delete regex.state;
8993
8993
  let isIgnored = () => false;
8994
8994
  if (opts.ignore) {
8995
8995
  const ignoreOpts = { ...options, ignore: null, onMatch: null, onResult: null };
8996
- isIgnored = picomatch3(opts.ignore, ignoreOpts, returnState);
8996
+ isIgnored = picomatch4(opts.ignore, ignoreOpts, returnState);
8997
8997
  }
8998
8998
  const matcher = (input, returnObject = false) => {
8999
- const { isMatch, match, output } = picomatch3.test(input, regex, options, { glob: glob2, posix: posix2 });
8999
+ const { isMatch, match, output } = picomatch4.test(input, regex, options, { glob: glob2, posix: posix2 });
9000
9000
  const result = { glob: glob2, state, regex, posix: posix2, input, output, match, isMatch };
9001
9001
  if (typeof opts.onResult === "function") {
9002
9002
  opts.onResult(result);
@@ -9022,7 +9022,7 @@ var require_picomatch = __commonJS({
9022
9022
  }
9023
9023
  return matcher;
9024
9024
  };
9025
- picomatch3.test = (input, regex, options, { glob: glob2, posix: posix2 } = {}) => {
9025
+ picomatch4.test = (input, regex, options, { glob: glob2, posix: posix2 } = {}) => {
9026
9026
  if (typeof input !== "string") {
9027
9027
  throw new TypeError("Expected input to be a string");
9028
9028
  }
@@ -9039,24 +9039,24 @@ var require_picomatch = __commonJS({
9039
9039
  }
9040
9040
  if (match === false || opts.capture === true) {
9041
9041
  if (opts.matchBase === true || opts.basename === true) {
9042
- match = picomatch3.matchBase(input, regex, options, posix2);
9042
+ match = picomatch4.matchBase(input, regex, options, posix2);
9043
9043
  } else {
9044
9044
  match = regex.exec(output);
9045
9045
  }
9046
9046
  }
9047
9047
  return { isMatch: Boolean(match), match, output };
9048
9048
  };
9049
- picomatch3.matchBase = (input, glob2, options, posix2 = options && options.windows) => {
9050
- const regex = glob2 instanceof RegExp ? glob2 : picomatch3.makeRe(glob2, options);
9049
+ picomatch4.matchBase = (input, glob2, options, posix2 = options && options.windows) => {
9050
+ const regex = glob2 instanceof RegExp ? glob2 : picomatch4.makeRe(glob2, options);
9051
9051
  return regex.test(utils.basename(input, { windows: posix2 }));
9052
9052
  };
9053
- picomatch3.isMatch = (str, patterns, options) => picomatch3(patterns, options)(str);
9054
- picomatch3.parse = (pattern, options) => {
9055
- if (Array.isArray(pattern)) return pattern.map((p) => picomatch3.parse(p, options));
9053
+ picomatch4.isMatch = (str, patterns, options) => picomatch4(patterns, options)(str);
9054
+ picomatch4.parse = (pattern, options) => {
9055
+ if (Array.isArray(pattern)) return pattern.map((p) => picomatch4.parse(p, options));
9056
9056
  return parse3(pattern, { ...options, fastpaths: false });
9057
9057
  };
9058
- picomatch3.scan = (input, options) => scan(input, options);
9059
- picomatch3.compileRe = (state, options, returnOutput = false, returnState = false) => {
9058
+ picomatch4.scan = (input, options) => scan(input, options);
9059
+ picomatch4.compileRe = (state, options, returnOutput = false, returnState = false) => {
9060
9060
  if (returnOutput === true) {
9061
9061
  return state.output;
9062
9062
  }
@@ -9067,13 +9067,13 @@ var require_picomatch = __commonJS({
9067
9067
  if (state && state.negated === true) {
9068
9068
  source = `^(?!${source}).*$`;
9069
9069
  }
9070
- const regex = picomatch3.toRegex(source, options);
9070
+ const regex = picomatch4.toRegex(source, options);
9071
9071
  if (returnState === true) {
9072
9072
  regex.state = state;
9073
9073
  }
9074
9074
  return regex;
9075
9075
  };
9076
- picomatch3.makeRe = (input, options = {}, returnOutput = false, returnState = false) => {
9076
+ picomatch4.makeRe = (input, options = {}, returnOutput = false, returnState = false) => {
9077
9077
  if (!input || typeof input !== "string") {
9078
9078
  throw new TypeError("Expected a non-empty string");
9079
9079
  }
@@ -9084,9 +9084,9 @@ var require_picomatch = __commonJS({
9084
9084
  if (!parsed.output) {
9085
9085
  parsed = parse3(input, options);
9086
9086
  }
9087
- return picomatch3.compileRe(parsed, options, returnOutput, returnState);
9087
+ return picomatch4.compileRe(parsed, options, returnOutput, returnState);
9088
9088
  };
9089
- picomatch3.toRegex = (source, options) => {
9089
+ picomatch4.toRegex = (source, options) => {
9090
9090
  try {
9091
9091
  const opts = options || {};
9092
9092
  return new RegExp(source, opts.flags || (opts.nocase ? "i" : ""));
@@ -9095,8 +9095,8 @@ var require_picomatch = __commonJS({
9095
9095
  return /$^/;
9096
9096
  }
9097
9097
  };
9098
- picomatch3.constants = constants2;
9099
- module.exports = picomatch3;
9098
+ picomatch4.constants = constants2;
9099
+ module.exports = picomatch4;
9100
9100
  }
9101
9101
  });
9102
9102
 
@@ -9106,14 +9106,14 @@ var require_picomatch2 = __commonJS({
9106
9106
  "use strict";
9107
9107
  var pico = require_picomatch();
9108
9108
  var utils = require_utils();
9109
- function picomatch3(glob2, options, returnState = false) {
9109
+ function picomatch4(glob2, options, returnState = false) {
9110
9110
  if (options && (options.windows === null || options.windows === void 0)) {
9111
9111
  options = { ...options, windows: utils.isWindows() };
9112
9112
  }
9113
9113
  return pico(glob2, options, returnState);
9114
9114
  }
9115
- Object.assign(picomatch3, pico);
9116
- module.exports = picomatch3;
9115
+ Object.assign(picomatch4, pico);
9116
+ module.exports = picomatch4;
9117
9117
  }
9118
9118
  });
9119
9119
 
@@ -9415,7 +9415,8 @@ var RETRY_STATUSES = /* @__PURE__ */ new Set([
9415
9415
  520,
9416
9416
  521,
9417
9417
  522,
9418
- 524
9418
+ 524,
9419
+ 529
9419
9420
  ]);
9420
9421
  var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
9421
9422
  function createOpenRouterTransport(options) {
@@ -9569,7 +9570,7 @@ function loadConfig(options = {}) {
9569
9570
  { layer: user, file: userFile }
9570
9571
  ];
9571
9572
  const pickFrom = (path, rule) => pick(path, rule, layers2, warnings);
9572
- const merged = {
9573
+ const merged2 = {
9573
9574
  model: pickFrom("model", TEXT) ?? DEFAULTS.model,
9574
9575
  endpoint: pickFrom("endpoint", URL_TEXT) ?? DEFAULTS.endpoint,
9575
9576
  concurrency: pickFrom("concurrency", WHOLE) ?? DEFAULTS.concurrency,
@@ -9591,9 +9592,9 @@ function loadConfig(options = {}) {
9591
9592
  const key = resolveApiKey(env, userDir);
9592
9593
  const session = detectSession(env);
9593
9594
  return {
9594
- ...merged,
9595
- model: nonEmpty2(env.SYSTEM1_MODEL) ?? merged.model,
9596
- endpoint: nonEmpty2(env.SYSTEM1_ENDPOINT) ?? merged.endpoint,
9595
+ ...merged2,
9596
+ model: nonEmpty2(env.SYSTEM1_MODEL) ?? merged2.model,
9597
+ endpoint: nonEmpty2(env.SYSTEM1_ENDPOINT) ?? merged2.endpoint,
9597
9598
  repoRoot,
9598
9599
  apiKey: key?.value,
9599
9600
  apiKeySource: key?.source,
@@ -9926,6 +9927,9 @@ function applyExcludes(items, extra = []) {
9926
9927
  }
9927
9928
  return { items: kept, excluded: [...excluded.values()] };
9928
9929
  }
9930
+ function isExcluded(path, extra = []) {
9931
+ return [...DEFAULT_EXCLUDES, ...extra].find((p) => (0, import_picomatch.default)(p, { dot: true })(path));
9932
+ }
9929
9933
 
9930
9934
  // ../core/dist/ping.js
9931
9935
  function probeUrl(config) {
@@ -13109,8 +13113,8 @@ function Compare(left, right) {
13109
13113
  // ../../node_modules/.pnpm/typebox@1.3.34/node_modules/typebox/build/type/engine/evaluate/broaden.mjs
13110
13114
  function BroadenFilter(type, types, result = [], all = types) {
13111
13115
  return guard_exports.ShiftLeft(types, (left, right) => {
13112
- const compare = Compare(type, left);
13113
- return guard_exports.IsEqual(compare, CompareResultLeftInside) || guard_exports.IsEqual(compare, CompareResultEqual) ? all : guard_exports.IsEqual(compare, CompareResultDisjoint) ? BroadenFilter(type, right, [...result, left], all) : BroadenFilter(type, right, result, all);
13116
+ const compare2 = Compare(type, left);
13117
+ return guard_exports.IsEqual(compare2, CompareResultLeftInside) || guard_exports.IsEqual(compare2, CompareResultEqual) ? all : guard_exports.IsEqual(compare2, CompareResultDisjoint) ? BroadenFilter(type, right, [...result, left], all) : BroadenFilter(type, right, result, all);
13114
13118
  }, () => [...result, type]);
13115
13119
  }
13116
13120
  function BroadenType(type, types, result) {
@@ -13556,8 +13560,8 @@ function CollapseIntersectProperties(left, right) {
13556
13560
  const leftProperties = leftKeys.reduce((result, key) => ({ ...result, [key]: left[key] }), {});
13557
13561
  const rightProperties = rightKeys.reduce((result, key) => ({ ...result, [key]: right[key] }), {});
13558
13562
  const sharedProperties = sharedKeys.reduce((result, key) => ({ ...result, [key]: EvaluateIntersect([left[key], right[key]]) }), {});
13559
- const unique = memory_exports.Assign(leftProperties, rightProperties);
13560
- const shared = memory_exports.Assign(unique, sharedProperties);
13563
+ const unique2 = memory_exports.Assign(leftProperties, rightProperties);
13564
+ const shared = memory_exports.Assign(unique2, sharedProperties);
13561
13565
  return shared;
13562
13566
  }
13563
13567
  function FromIntersect(types) {
@@ -14993,15 +14997,15 @@ function NextPendingResource(stack, schema) {
14993
14997
  return IsId(schema) ? false : stack.pendingResource;
14994
14998
  }
14995
14999
  function NextLexicalBase(stack, schema) {
14996
- const entry = ResourceEntry(stack, schema);
14997
- if (entry)
14998
- return entry.base;
15000
+ const entry2 = ResourceEntry(stack, schema);
15001
+ if (entry2)
15002
+ return entry2.base;
14999
15003
  return IsId(schema) ? NextUri(schema.$id, stack.lexicalBase).href : stack.lexicalBase;
15000
15004
  }
15001
15005
  function NextResourceBase(stack, schema) {
15002
- const entry = ResourceEntry(stack, schema);
15003
- if (entry)
15004
- return entry.base;
15006
+ const entry2 = ResourceEntry(stack, schema);
15007
+ if (entry2)
15008
+ return entry2.base;
15005
15009
  return IsId(schema) && stack.pendingResource ? NextUri(schema.$id, stack.resourceBase).href : stack.resourceBase;
15006
15010
  }
15007
15011
  function NextUseResourceBaseForReference(stack, schema) {
@@ -15013,9 +15017,9 @@ function NextReferenceBase(stack, schema) {
15013
15017
  return isRetrieved || useResourceBaseForReference ? NextResourceBase(stack, schema) : NextLexicalBase(stack, schema);
15014
15018
  }
15015
15019
  function NextLexicalSchema(stack, schema) {
15016
- const entry = ResourceEntry(stack, schema);
15017
- if (entry)
15018
- return entry.root;
15020
+ const entry2 = ResourceEntry(stack, schema);
15021
+ if (entry2)
15022
+ return entry2.root;
15019
15023
  return IsId(schema) ? schema : stack.lexicalSchema;
15020
15024
  }
15021
15025
  function HasStackKeywords(stack, schema) {
@@ -17975,10 +17979,10 @@ function FromArray13(context, type, value) {
17975
17979
  const repaired = maximum.map((value2) => FromType26(context, type.items, value2));
17976
17980
  if (!IsUniqueItems(type) || IsUniqueItems(type) && !guard_exports.IsEqual(type.uniqueItems, true))
17977
17981
  return repaired;
17978
- const unique = MakeUnique(repaired);
17979
- if (!Check2(context, type, unique))
17982
+ const unique2 = MakeUnique(repaired);
17983
+ if (!Check2(context, type, unique2))
17980
17984
  throw new RepairError(context, type, value, "Failed to repair Array due to uniqueItems constraint");
17981
- return unique;
17985
+ return unique2;
17982
17986
  }
17983
17987
 
17984
17988
  // ../../node_modules/.pnpm/typebox@1.3.34/node_modules/typebox/build/value/repair/from_enum.mjs
@@ -18122,14 +18126,14 @@ function FromUnknown2(context, type, value) {
18122
18126
 
18123
18127
  // ../../node_modules/.pnpm/typebox@1.3.34/node_modules/typebox/build/value/repair/from_type.mjs
18124
18128
  function AssertRepairableValue(context, type, value) {
18125
- const unsupported = globals_exports.IsDate(value) || globals_exports.IsMap(value) || globals_exports.IsSet(value) || globals_exports.IsTypeArray(value) || guard_exports.IsConstructor(value) || guard_exports.IsFunction(value);
18126
- if (unsupported) {
18129
+ const unsupported2 = globals_exports.IsDate(value) || globals_exports.IsMap(value) || globals_exports.IsSet(value) || globals_exports.IsTypeArray(value) || guard_exports.IsConstructor(value) || guard_exports.IsFunction(value);
18130
+ if (unsupported2) {
18127
18131
  throw new RepairError(context, type, value, "Value is not repairable");
18128
18132
  }
18129
18133
  }
18130
18134
  function AssertRepairableType(context, type, value) {
18131
- const unsupported = IsConstructor2(type) || IsFunction2(type) || IsNever(type);
18132
- if (unsupported) {
18135
+ const unsupported2 = IsConstructor2(type) || IsFunction2(type) || IsNever(type);
18136
+ if (unsupported2) {
18133
18137
  throw new RepairError(context, type, value, "Type is not repairable");
18134
18138
  }
18135
18139
  }
@@ -18186,16 +18190,34 @@ var import_yaml3 = __toESM(require_dist(), 1);
18186
18190
 
18187
18191
  // ../core/dist/project/project.js
18188
18192
  var FILTER = /^\s*([A-Za-z_][\w-]*)((?:\.[\w-]+)*)\s*(>=|<=|!=|>|<|=|\s+in\s+)\s*(.+?)\s*$/;
18189
- function parseFilter(text, questions) {
18193
+ function tokenizeFilter(text) {
18190
18194
  const m = FILTER.exec(text);
18191
- if (!m?.[1] || !m[3] || m[4] === void 0) {
18192
- throw new DecisionsError("invalid-request", `Cannot parse --keep "${text}". Use <question>[.<field>]<op><value>, e.g. relevant>=0.7 or kind in fix,feature`);
18193
- }
18194
- const question = m[1];
18195
+ if (!m?.[1] || !m[3] || m[4] === void 0)
18196
+ return void 0;
18195
18197
  const op = m[3].trim();
18196
- const field = m[2] ? m[2].slice(1) : defaultField(question, questions);
18197
18198
  const raw = m[4];
18198
18199
  const value = op === "in" ? raw.split(",").map((s) => s.trim()).filter(Boolean) : isNumeric(raw) ? Number(raw) : raw;
18200
+ return { name: m[1], path: m[2] ? m[2].slice(1) : "", op, value };
18201
+ }
18202
+ function tokenizeSort(text) {
18203
+ const m = /^\s*([A-Za-z_][\w-]*)((?:\.[\w-]+)*)(?::(asc|desc))?\s*$/.exec(text);
18204
+ if (!m?.[1])
18205
+ return void 0;
18206
+ return {
18207
+ name: m[1],
18208
+ path: m[2] ? m[2].slice(1) : "",
18209
+ direction: m[3] ?? "desc"
18210
+ };
18211
+ }
18212
+ function parseFilter(text, questions) {
18213
+ const t = tokenizeFilter(text);
18214
+ if (!t) {
18215
+ throw new DecisionsError("invalid-request", `Cannot parse --keep "${text}". Use <question>[.<field>]<op><value>, e.g. relevant>=0.7 or kind in fix,feature`);
18216
+ }
18217
+ const question = t.name;
18218
+ const op = t.op;
18219
+ const field = t.path || defaultField(question, questions);
18220
+ const value = t.value;
18199
18221
  const filter = { question, field, op, value, source: text };
18200
18222
  if (questions)
18201
18223
  assertKnown(question, questions, text);
@@ -18205,14 +18227,14 @@ function parseFilter(text, questions) {
18205
18227
  return filter;
18206
18228
  }
18207
18229
  function parseSort(text, questions) {
18208
- const m = /^\s*([A-Za-z_][\w-]*)((?:\.[\w-]+)*)(?::(asc|desc))?\s*$/.exec(text);
18209
- if (!m?.[1]) {
18230
+ const t = tokenizeSort(text);
18231
+ if (!t) {
18210
18232
  throw new DecisionsError("invalid-request", `Cannot parse --sort "${text}". Use <question>[.<field>][:asc|desc]`);
18211
18233
  }
18212
18234
  if (questions)
18213
- assertKnown(m[1], questions, text);
18214
- const field = m[2] ? m[2].slice(1) : sortField(m[1], questions);
18215
- return { question: m[1], field, direction: m[3] ?? "desc" };
18235
+ assertKnown(t.name, questions, text);
18236
+ const field = t.path || sortField(t.name, questions);
18237
+ return { question: t.name, field, direction: t.direction };
18216
18238
  }
18217
18239
  function matches(answers, filter) {
18218
18240
  const actual = read(answers[filter.question], filter.field);
@@ -18231,18 +18253,33 @@ function matches(answers, filter) {
18231
18253
  }
18232
18254
  }
18233
18255
  }
18256
+ function verdictOf(answers, flat, keep, keepAny = [], sortQuestion) {
18257
+ const acted = [...keep.map((f) => f.question), ...sortQuestion ? [sortQuestion] : []];
18258
+ const all = acted.length === 0 && keepAny.length === 0;
18259
+ const blocking = flat.filter((q) => all || acted.includes(q));
18260
+ if (blocking.length > 0)
18261
+ return { verdict: "undecided", questions: [...blocking] };
18262
+ if (!keep.every((f) => matches(answers, f)))
18263
+ return { verdict: "dropped", questions: [] };
18264
+ if (keepAny.length === 0)
18265
+ return { verdict: "kept", questions: [] };
18266
+ const decided = keepAny.filter((f) => !flat.includes(f.question));
18267
+ if (decided.some((f) => matches(answers, f)))
18268
+ return { verdict: "kept", questions: [] };
18269
+ const open = [...new Set(keepAny.filter((f) => flat.includes(f.question)).map((f) => f.question))];
18270
+ return open.length > 0 ? { verdict: "undecided", questions: open } : { verdict: "dropped", questions: [] };
18271
+ }
18234
18272
  function project(input) {
18235
18273
  const keep = input.keep ?? [];
18236
- const acted = [...keep.map((f) => f.question), ...input.sort ? [input.sort.question] : []];
18237
- const relevant = acted.length > 0 ? new Set(acted) : void 0;
18274
+ const keepAny = input.keepAny ?? [];
18238
18275
  const kept = [];
18239
18276
  const undecided = [];
18240
18277
  let dropped = 0;
18241
18278
  for (const row2 of input.rows) {
18242
- const flat = input.undecidedOf(row2).filter((q) => !relevant || relevant.has(q));
18243
- if (flat.length > 0)
18244
- undecided.push({ row: row2, questions: [...flat] });
18245
- else if (keep.every((f) => matches(row2.answers, f)))
18279
+ const v = verdictOf(row2.answers, input.undecidedOf(row2), keep, keepAny, input.sort?.question);
18280
+ if (v.verdict === "undecided")
18281
+ undecided.push({ row: row2, questions: v.questions });
18282
+ else if (v.verdict === "kept")
18246
18283
  kept.push(row2);
18247
18284
  else
18248
18285
  dropped += 1;
@@ -18625,21 +18662,21 @@ var Walker = class {
18625
18662
  if (controller.aborted || signal && signal.aborted || maxFiles && paths.length > maxFiles) return;
18626
18663
  const files = this.getArray(this.state.paths);
18627
18664
  for (let i = 0; i < entries.length; ++i) {
18628
- const entry = entries[i];
18629
- if (entry.isFile() || entry.isSymbolicLink() && !resolveSymlinks$1 && !excludeSymlinks) {
18630
- const filename = this.joinPath(entry.name, directoryPath);
18665
+ const entry2 = entries[i];
18666
+ if (entry2.isFile() || entry2.isSymbolicLink() && !resolveSymlinks$1 && !excludeSymlinks) {
18667
+ const filename = this.joinPath(entry2.name, directoryPath);
18631
18668
  this.pushFile(filename, files, this.state.counts, filters);
18632
- } else if (entry.isDirectory()) {
18633
- let path = joinDirectoryPath(entry.name, directoryPath, this.state.options.pathSeparator);
18634
- if (exclude && exclude(entry.name, path)) continue;
18669
+ } else if (entry2.isDirectory()) {
18670
+ let path = joinDirectoryPath(entry2.name, directoryPath, this.state.options.pathSeparator);
18671
+ if (exclude && exclude(entry2.name, path)) continue;
18635
18672
  this.pushDirectory(path, paths, filters);
18636
18673
  this.walkDirectory(this.state, path, path, depth - 1, this.walk);
18637
- } else if (this.resolveSymlink && entry.isSymbolicLink()) {
18638
- let path = joinPathWithBasePath(entry.name, directoryPath);
18674
+ } else if (this.resolveSymlink && entry2.isSymbolicLink()) {
18675
+ let path = joinPathWithBasePath(entry2.name, directoryPath);
18639
18676
  this.resolveSymlink(path, this.state, (stat2, resolvedPath) => {
18640
18677
  if (stat2.isDirectory()) {
18641
18678
  resolvedPath = normalizePath(resolvedPath, this.state.options);
18642
- if (exclude && exclude(entry.name, useRealPaths ? resolvedPath : path + pathSeparator)) return;
18679
+ if (exclude && exclude(entry2.name, useRealPaths ? resolvedPath : path + pathSeparator)) return;
18643
18680
  this.walkDirectory(this.state, resolvedPath, useRealPaths ? resolvedPath : path + pathSeparator, depth - 1, this.walk);
18644
18681
  } else {
18645
18682
  resolvedPath = useRealPaths ? resolvedPath : path;
@@ -19073,6 +19110,26 @@ async function glob(globInput, options) {
19073
19110
  return crawler ? formatPaths(await crawler.withPromise(), relative5) : [];
19074
19111
  }
19075
19112
 
19113
+ // ../core/dist/sources/filter.js
19114
+ var import_picomatch3 = __toESM(require_picomatch2(), 1);
19115
+ function applyFilter(items, patterns = []) {
19116
+ if (patterns.length === 0)
19117
+ return { items: [...items], filtered: [] };
19118
+ const matchers = patterns.map((p) => ({ p, test: (0, import_picomatch3.default)(p, { dot: true }) }));
19119
+ const kept = [];
19120
+ const filtered = /* @__PURE__ */ new Map();
19121
+ for (const item of items) {
19122
+ const paths = [item.path, item.realPath].filter((p) => p !== void 0);
19123
+ const hit = item.path === void 0 ? void 0 : matchers.find((m) => paths.some((p) => m.test(p)));
19124
+ if (hit && item.path !== void 0) {
19125
+ filtered.set(item.path, { path: item.path, reason: "filtered", detail: hit.p });
19126
+ } else {
19127
+ kept.push(item);
19128
+ }
19129
+ }
19130
+ return { items: kept, filtered: [...filtered.values()] };
19131
+ }
19132
+
19076
19133
  // ../core/dist/sources/read.js
19077
19134
  function resolvePath(cwd, path) {
19078
19135
  const absolute = resolve4(cwd, path);
@@ -19187,12 +19244,15 @@ async function readGlob(patterns, options) {
19187
19244
  });
19188
19245
  matches2.sort();
19189
19246
  const ignored = new Set(gitIgnored(options.cwd, matches2));
19190
- const kept = matches2.filter((m) => !ignored.has(m));
19191
- const result = readFiles(kept, options);
19247
+ const candidates = matches2.filter((m) => !ignored.has(m));
19248
+ const withheld = candidates.filter((m) => isExcluded(m, options.withhold));
19249
+ const unfiltered = applyFilter(candidates.filter((m) => !withheld.includes(m)).map((path) => ({ path })), options.filter);
19250
+ const result = readFiles([...withheld, ...unfiltered.items.map((m) => m.path)].sort(), options);
19192
19251
  return {
19193
19252
  documents: result.documents,
19194
19253
  skipped: [
19195
19254
  ...[...ignored].map((path) => ({ path, reason: "gitignored" })),
19255
+ ...unfiltered.filtered,
19196
19256
  ...result.skipped
19197
19257
  ]
19198
19258
  };
@@ -19535,6 +19595,8 @@ var SpecSchema = typebox_exports.Object({
19535
19595
  description: typebox_exports.String({ minLength: 1 }),
19536
19596
  questions: typebox_exports.Record(typebox_exports.String(), typebox_exports.Unknown()),
19537
19597
  keep: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
19598
+ /** Kept when any of these matches, as well as every `keep`. */
19599
+ keepAny: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
19538
19600
  sort: typebox_exports.Optional(typebox_exports.String()),
19539
19601
  policy: typebox_exports.Optional(typebox_exports.Object({ thresholds: typebox_exports.Optional(typebox_exports.Record(typebox_exports.String(), Threshold)) }, CLOSED)),
19540
19602
  source: typebox_exports.Optional(typebox_exports.Object({
@@ -19581,7 +19643,7 @@ function parseSpec(text, file, origin) {
19581
19643
  throw new DecisionsError("invalid-request", `${file}: ${error.message}`, { file });
19582
19644
  }
19583
19645
  const questions = spec.questions;
19584
- for (const text2 of spec.keep ?? [])
19646
+ for (const text2 of [...spec.keep ?? [], ...spec.keepAny ?? []])
19585
19647
  wrap(file, () => parseFilter(text2, questions));
19586
19648
  if (spec.sort)
19587
19649
  wrap(file, () => parseSort(spec.sort, questions));
@@ -19684,11 +19746,11 @@ function listSpecs(dirs) {
19684
19746
  for (const [origin, dir] of layers(dirs)) {
19685
19747
  if (!existsSync4(dir))
19686
19748
  continue;
19687
- for (const entry of readdirSync2(dir).sort()) {
19688
- if (!/\.(ya?ml|json)$/.test(entry))
19749
+ for (const entry2 of readdirSync2(dir).sort()) {
19750
+ if (!/\.(ya?ml|json)$/.test(entry2))
19689
19751
  continue;
19690
- const file = join2(dir, entry);
19691
- const name = basename2(entry, extname(entry));
19752
+ const file = join2(dir, entry2);
19753
+ const name = basename2(entry2, extname(entry2));
19692
19754
  const shadowed = seen.has(name);
19693
19755
  seen.add(name);
19694
19756
  try {
@@ -19728,6 +19790,360 @@ function wrap(file, fn) {
19728
19790
  }
19729
19791
  }
19730
19792
 
19793
+ // ../core/dist/model/schema.js
19794
+ var QuestionSchema = typebox_exports.Union([
19795
+ typebox_exports.Object({
19796
+ type: typebox_exports.Literal("noul"),
19797
+ instructions: typebox_exports.String({ minLength: 1 }),
19798
+ /** Optional `true`/`false` criteria that sharpen an ambiguous statement. */
19799
+ criteria: typebox_exports.Optional(typebox_exports.Record(typebox_exports.String(), typebox_exports.String()))
19800
+ }, { additionalProperties: false }),
19801
+ typebox_exports.Object({
19802
+ type: typebox_exports.Literal("choice"),
19803
+ instructions: typebox_exports.String({ minLength: 1 }),
19804
+ /** Option key to when it applies. At least two, including a way out. */
19805
+ criteria: typebox_exports.Record(typebox_exports.String(), typebox_exports.String(), { minProperties: 2 })
19806
+ }, { additionalProperties: false }),
19807
+ typebox_exports.Object({
19808
+ type: typebox_exports.Literal("score"),
19809
+ instructions: typebox_exports.String({ minLength: 1 }),
19810
+ /** Levels, lowest first and 0-indexed. */
19811
+ criteria: typebox_exports.Array(typebox_exports.String(), { minItems: 2 })
19812
+ }, { additionalProperties: false })
19813
+ ]);
19814
+
19815
+ // ../core/dist/opportunities/backlog.js
19816
+ import { createHash } from "node:crypto";
19817
+ import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync5, renameSync, rmSync, statSync as statSync4, writeFileSync as writeFileSync2 } from "node:fs";
19818
+ import { dirname as dirname4, join as join3 } from "node:path";
19819
+ var BACKLOG_FORMAT = 1;
19820
+ var CLOSED2 = { additionalProperties: false };
19821
+ var Text = typebox_exports.String({ minLength: 1 });
19822
+ var Usd = typebox_exports.Number({ minimum: 0, maximum: 1e6 });
19823
+ var Status = typebox_exports.Union([
19824
+ typebox_exports.Literal("new"),
19825
+ typebox_exports.Literal("stale"),
19826
+ typebox_exports.Literal("adopted"),
19827
+ typebox_exports.Literal("rejected")
19828
+ ]);
19829
+ var candidateFields = {
19830
+ /** `code`: a codebase (Mode A). `agents`: skills, hooks, agent configuration (Mode B). */
19831
+ mode: typebox_exports.Union([typebox_exports.Literal("code"), typebox_exports.Literal("agents")]),
19832
+ location: typebox_exports.Object({
19833
+ path: Text,
19834
+ lines: typebox_exports.Optional(typebox_exports.Object({ start: typebox_exports.Integer({ minimum: 1 }), end: typebox_exports.Integer({ minimum: 1 }) }, CLOSED2))
19835
+ }, CLOSED2),
19836
+ /** What does the job today, e.g. "gpt-4o call parsed to an enum". */
19837
+ mechanism: Text,
19838
+ shape: typebox_exports.Union([
19839
+ typebox_exports.Literal("single"),
19840
+ typebox_exports.Literal("fanout"),
19841
+ typebox_exports.Literal("cascade"),
19842
+ typebox_exports.Literal("pairwise")
19843
+ ]),
19844
+ /**
19845
+ * The text that triggered it, taken verbatim from the screened item. The id is
19846
+ * derived from it, so it must be copied, not paraphrased.
19847
+ */
19848
+ evidence: typebox_exports.String({ pattern: "\\S" }),
19849
+ /** A draft question set that would replace the mechanism. */
19850
+ questions: typebox_exports.Record(typebox_exports.String(), QuestionSchema, { minProperties: 1 }),
19851
+ /**
19852
+ * What replacing the mechanism would mainly buy. `cost`: cheaper calls.
19853
+ * `quality`: better or more consistent verdicts (a regex or heuristic that
19854
+ * costs nothing today, so its `savingUsd` can be negative). `latency`:
19855
+ * faster answers. Rank within a benefit, not across them.
19856
+ */
19857
+ benefit: typebox_exports.Union([typebox_exports.Literal("cost"), typebox_exports.Literal("quality"), typebox_exports.Literal("latency")]),
19858
+ /** The inputs of the projected saving. The saving itself is computed here. */
19859
+ projected: typebox_exports.Object({
19860
+ volume: typebox_exports.Number({ minimum: 0, maximum: 1e12 }),
19861
+ /** What `volume` counts per, e.g. "day", "PR", "run". */
19862
+ per: Text,
19863
+ currentCostPerItemUsd: Usd,
19864
+ decisionCostPerItemUsd: Usd,
19865
+ /** Where the figures came from. */
19866
+ note: typebox_exports.Optional(typebox_exports.String())
19867
+ }, CLOSED2),
19868
+ risk: typebox_exports.Object({
19869
+ level: typebox_exports.Union([typebox_exports.Literal("low"), typebox_exports.Literal("medium"), typebox_exports.Literal("high")]),
19870
+ /** What would go wrong if the replacement erred. */
19871
+ note: Text
19872
+ }, CLOSED2),
19873
+ /** The next step, e.g. "save the spec with design, then adopt". */
19874
+ next: Text,
19875
+ /** Defaults to `new`. `rejected` needs a `statusReason`. */
19876
+ status: typebox_exports.Optional(Status),
19877
+ statusReason: typebox_exports.Optional(Text),
19878
+ /** Which sweep produced it, and whether its screening answers were live or replayed. */
19879
+ source: typebox_exports.Object({
19880
+ sweep: Text,
19881
+ answers: typebox_exports.Union([typebox_exports.Literal("live"), typebox_exports.Literal("replay")])
19882
+ }, CLOSED2)
19883
+ };
19884
+ var CandidateSchema = typebox_exports.Object(candidateFields, CLOSED2);
19885
+ var OpportunitySchema = typebox_exports.Object({
19886
+ id: typebox_exports.String({ pattern: "^op-[0-9a-f]{12}$" }),
19887
+ ...candidateFields,
19888
+ status: Status,
19889
+ projected: typebox_exports.Object({
19890
+ ...candidateFields.projected.properties,
19891
+ basis: typebox_exports.Literal("projected"),
19892
+ /** volume × (current − decision), per `per`. Negative means it would cost more. */
19893
+ savingUsd: typebox_exports.Number()
19894
+ }, CLOSED2),
19895
+ firstSeenAt: typebox_exports.String(),
19896
+ seenAt: typebox_exports.String()
19897
+ }, CLOSED2);
19898
+ var BacklogSchema = typebox_exports.Object({
19899
+ version: typebox_exports.Literal(BACKLOG_FORMAT),
19900
+ opportunities: typebox_exports.Array(OpportunitySchema)
19901
+ }, CLOSED2);
19902
+ function backlogPath(repoRoot) {
19903
+ return join3(stateDir(repoRoot), "opportunities.json");
19904
+ }
19905
+ function opportunityId(mode, evidence) {
19906
+ const normal = evidence.replace(/\s+/g, " ").trim();
19907
+ return `op-${createHash("sha256").update(`${mode}
19908
+ ${normal}`).digest("hex").slice(0, 12)}`;
19909
+ }
19910
+ function projectedSaving(p) {
19911
+ return round(p.volume * (p.currentCostPerItemUsd - p.decisionCostPerItemUsd));
19912
+ }
19913
+ function backlogProblems(value) {
19914
+ const problems = [...value_exports.Errors(BacklogSchema, value)].map((e) => `${e.instancePath || "/"} ${e.message}`);
19915
+ if (problems.length)
19916
+ return problems;
19917
+ const backlog = value;
19918
+ const seen = /* @__PURE__ */ new Set();
19919
+ backlog.opportunities.forEach((o, i) => {
19920
+ if (seen.has(o.id))
19921
+ problems.push(`/opportunities/${i}/id ${o.id} appears twice`);
19922
+ seen.add(o.id);
19923
+ if (o.id !== opportunityId(o.mode, o.evidence)) {
19924
+ problems.push(`/opportunities/${i}/id does not match its mode and evidence`);
19925
+ }
19926
+ if (o.projected.savingUsd !== projectedSaving(o.projected)) {
19927
+ problems.push(`/opportunities/${i}/projected/savingUsd does not match its inputs`);
19928
+ }
19929
+ problems.push(...candidateProblems(o, `/opportunities/${i}`));
19930
+ });
19931
+ return problems;
19932
+ }
19933
+ function candidateProblems(c, at) {
19934
+ const problems = [];
19935
+ if (c.status === "rejected" && !c.statusReason) {
19936
+ problems.push(`${at}/statusReason is required when status is rejected`);
19937
+ }
19938
+ if (!Number.isFinite(projectedSaving(c.projected))) {
19939
+ problems.push(`${at}/projected gives a saving that isn't a finite number`);
19940
+ }
19941
+ if (c.location.lines && c.location.lines.end < c.location.lines.start) {
19942
+ problems.push(`${at}/location/lines end is before start`);
19943
+ }
19944
+ try {
19945
+ assertQuestionSet(c.questions);
19946
+ } catch (error) {
19947
+ problems.push(`${at}/questions ${error.message}`);
19948
+ }
19949
+ return problems;
19950
+ }
19951
+ function readBacklog(file) {
19952
+ if (!existsSync5(file))
19953
+ return { version: BACKLOG_FORMAT, opportunities: [] };
19954
+ let text;
19955
+ try {
19956
+ text = readFileSync5(file, "utf8");
19957
+ } catch (error) {
19958
+ throw malformed2(file, [`cannot read it: ${error.message}`]);
19959
+ }
19960
+ let value;
19961
+ try {
19962
+ value = JSON.parse(text);
19963
+ } catch (error) {
19964
+ throw malformed2(file, [`not JSON: ${error.message}`]);
19965
+ }
19966
+ const problems = backlogProblems(value);
19967
+ if (problems.length)
19968
+ throw malformed2(file, problems);
19969
+ return value;
19970
+ }
19971
+ function malformed2(file, problems) {
19972
+ return new DecisionsError("invalid-request", `${file} is not a valid backlog (it was left as it is):
19973
+ ${problems.join("\n ")}`, { file, problems });
19974
+ }
19975
+ var LOCK_WAIT_MS = 5e3;
19976
+ var LOCK_STALE_MS = 3e4;
19977
+ function withBacklogLock(file, body) {
19978
+ mkdirSync2(dirname4(file), { recursive: true });
19979
+ const lock = `${file}.lock`;
19980
+ const deadline = Date.now() + LOCK_WAIT_MS;
19981
+ for (; ; ) {
19982
+ try {
19983
+ closeSync(openSync(lock, "wx"));
19984
+ break;
19985
+ } catch (error) {
19986
+ if (error.code !== "EEXIST")
19987
+ throw error;
19988
+ try {
19989
+ if (Date.now() - statSync4(lock).mtimeMs > LOCK_STALE_MS)
19990
+ rmSync(lock, { force: true });
19991
+ } catch {
19992
+ }
19993
+ if (Date.now() > deadline) {
19994
+ throw new DecisionsError("invalid-request", `${file} is locked by another \`decide opportunities add\` (${lock}). Retry, or delete the lock file if no add is running.`, { file, lock });
19995
+ }
19996
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
19997
+ }
19998
+ }
19999
+ try {
20000
+ return body();
20001
+ } finally {
20002
+ rmSync(lock, { force: true });
20003
+ }
20004
+ }
20005
+ function writeBacklog(file, backlog) {
20006
+ mkdirSync2(dirname4(file), { recursive: true });
20007
+ const tmp = `${file}.${process.pid}.tmp`;
20008
+ writeFileSync2(tmp, `${JSON.stringify(backlog, null, 2)}
20009
+ `);
20010
+ renameSync(tmp, file);
20011
+ }
20012
+ function mergeCandidates(backlog, candidates, now) {
20013
+ const problems = candidates.flatMap((c, i) => candidateProblems(c, `/candidates/${i}`));
20014
+ if (problems.length) {
20015
+ throw new DecisionsError("invalid-request", `Invalid candidates:
20016
+ ${problems.join("\n ")}`, {
20017
+ problems
20018
+ });
20019
+ }
20020
+ const byId = new Map(backlog.opportunities.map((o) => [o.id, o]));
20021
+ const existed = new Set(byId.keys());
20022
+ const seen = /* @__PURE__ */ new Set();
20023
+ for (const { statusReason: reason, ...c } of candidates) {
20024
+ const id = opportunityId(c.mode, c.evidence);
20025
+ seen.add(id);
20026
+ const before = byId.get(id);
20027
+ const statusReason = reason ?? (c.status ? void 0 : before?.statusReason);
20028
+ byId.set(id, {
20029
+ id,
20030
+ ...c,
20031
+ // Seeing the evidence again disproves "stale"; any other status stands.
20032
+ status: c.status ?? (before?.status === "stale" ? "new" : before?.status) ?? "new",
20033
+ ...statusReason ? { statusReason } : {},
20034
+ projected: { ...c.projected, basis: "projected", savingUsd: projectedSaving(c.projected) },
20035
+ firstSeenAt: before?.firstSeenAt ?? now,
20036
+ seenAt: now
20037
+ });
20038
+ }
20039
+ const added = [...seen].filter((id) => !existed.has(id));
20040
+ const updated = [...seen].filter((id) => existed.has(id));
20041
+ const key = (o) => `${o.mode}
20042
+ ${o.location.path}
20043
+ ${o.source.sweep}`;
20044
+ const covered = new Set(candidates.map(key));
20045
+ const staled = [];
20046
+ for (const o of byId.values()) {
20047
+ if (o.status === "new" && !seen.has(o.id) && covered.has(key(o))) {
20048
+ byId.set(o.id, { ...o, status: "stale" });
20049
+ staled.push(o.id);
20050
+ }
20051
+ }
20052
+ return {
20053
+ backlog: { version: BACKLOG_FORMAT, opportunities: [...byId.values()] },
20054
+ added,
20055
+ updated,
20056
+ staled
20057
+ };
20058
+ }
20059
+ var FIELDS = {
20060
+ id: {},
20061
+ mode: {},
20062
+ benefit: {},
20063
+ status: {},
20064
+ statusReason: {},
20065
+ shape: {},
20066
+ mechanism: {},
20067
+ evidence: {},
20068
+ next: {},
20069
+ firstSeenAt: {},
20070
+ seenAt: {},
20071
+ risk: { default: "level", subs: ["level", "note"] },
20072
+ projected: {
20073
+ default: "savingUsd",
20074
+ subs: [
20075
+ "savingUsd",
20076
+ "volume",
20077
+ "per",
20078
+ "currentCostPerItemUsd",
20079
+ "decisionCostPerItemUsd",
20080
+ "basis",
20081
+ "note"
20082
+ ]
20083
+ },
20084
+ location: { default: "path", subs: ["path", "lines.start", "lines.end"] },
20085
+ source: { default: "sweep", subs: ["sweep", "answers"] }
20086
+ };
20087
+ var ENTRY_FIELDS = [...Object.keys(OpportunitySchema.properties)];
20088
+ function parseRecordFilter(text) {
20089
+ const t = tokenizeFilter(text);
20090
+ if (!t) {
20091
+ throw new DecisionsError("invalid-request", `Cannot parse --keep "${text}". Use <field>[.<sub>]<op><value>, e.g. status=new or projected>=0.01`);
20092
+ }
20093
+ const field = recordField(t.name, t.path, text);
20094
+ if ((t.op === ">" || t.op === ">=" || t.op === "<" || t.op === "<=") && typeof t.value !== "number") {
20095
+ throw new DecisionsError("invalid-request", `--keep "${text}": ${t.op} needs a number`);
20096
+ }
20097
+ return { field, op: t.op, value: t.value, source: text };
20098
+ }
20099
+ function parseRecordSort(text) {
20100
+ const t = tokenizeSort(text);
20101
+ if (!t) {
20102
+ throw new DecisionsError("invalid-request", `Cannot parse --sort "${text}". Use <field>[.<sub>][:asc|desc]`);
20103
+ }
20104
+ return { field: recordField(t.name, t.path, text), direction: t.direction };
20105
+ }
20106
+ function recordField(name, path, text) {
20107
+ const spec = Object.hasOwn(FIELDS, name) ? FIELDS[name] : void 0;
20108
+ if (!spec) {
20109
+ throw new DecisionsError("invalid-request", `"${text}": no field "${name}". Fields: ${Object.keys(FIELDS).join(", ")}`);
20110
+ }
20111
+ if (path && !spec.subs?.includes(path)) {
20112
+ throw new DecisionsError("invalid-request", spec.subs ? `"${text}": ${name} has no "${path}". Use one of: ${spec.subs.join(", ")}` : `"${text}": ${name} has no sub-fields`);
20113
+ }
20114
+ const sub = path || spec.default;
20115
+ return sub ? `${name}.${sub}` : name;
20116
+ }
20117
+ function readField(o, field) {
20118
+ let value = o;
20119
+ for (const key of field.split(".")) {
20120
+ if (value === null || typeof value !== "object")
20121
+ return void 0;
20122
+ value = value[key];
20123
+ }
20124
+ return value;
20125
+ }
20126
+ function recordMatches(o, f) {
20127
+ const actual = readField(o, f.field);
20128
+ switch (f.op) {
20129
+ case "in":
20130
+ return f.value.includes(String(actual));
20131
+ case "=":
20132
+ return String(actual) === String(f.value);
20133
+ case "!=":
20134
+ return String(actual) !== String(f.value);
20135
+ default: {
20136
+ if (typeof actual !== "number")
20137
+ return false;
20138
+ const v = f.value;
20139
+ return f.op === ">=" ? actual >= v : f.op === ">" ? actual > v : f.op === "<=" ? actual <= v : actual < v;
20140
+ }
20141
+ }
20142
+ }
20143
+ function round(n) {
20144
+ return Math.round(n * 1e9) / 1e9;
20145
+ }
20146
+
19731
20147
  // ../core/dist/tools/schemas.js
19732
20148
  var Source = typebox_exports.Union([
19733
20149
  typebox_exports.Object({
@@ -19750,26 +20166,6 @@ var Source = typebox_exports.Union([
19750
20166
  paths: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String()))
19751
20167
  })
19752
20168
  ]);
19753
- var Question = typebox_exports.Union([
19754
- typebox_exports.Object({
19755
- type: typebox_exports.Literal("noul"),
19756
- instructions: typebox_exports.String({ minLength: 1 }),
19757
- /** Optional `true`/`false` criteria that sharpen an ambiguous statement. */
19758
- criteria: typebox_exports.Optional(typebox_exports.Record(typebox_exports.String(), typebox_exports.String()))
19759
- }, { additionalProperties: false }),
19760
- typebox_exports.Object({
19761
- type: typebox_exports.Literal("choice"),
19762
- instructions: typebox_exports.String({ minLength: 1 }),
19763
- /** Option key to when it applies. At least two, including a way out. */
19764
- criteria: typebox_exports.Record(typebox_exports.String(), typebox_exports.String(), { minProperties: 2 })
19765
- }, { additionalProperties: false }),
19766
- typebox_exports.Object({
19767
- type: typebox_exports.Literal("score"),
19768
- instructions: typebox_exports.String({ minLength: 1 }),
19769
- /** Levels, lowest first and 0-indexed. */
19770
- criteria: typebox_exports.Array(typebox_exports.String(), { minItems: 2 })
19771
- }, { additionalProperties: false })
19772
- ]);
19773
20169
  var Mode = typebox_exports.Union([
19774
20170
  typebox_exports.Literal("auto"),
19775
20171
  typebox_exports.Literal("live"),
@@ -19780,13 +20176,20 @@ var Common = {
19780
20176
  /** A saved spec: name (repo → user → bundled) or path. */
19781
20177
  spec: typebox_exports.Optional(typebox_exports.String()),
19782
20178
  /** An inline question set; mutually exclusive with `spec`. */
19783
- questions: typebox_exports.Optional(typebox_exports.Record(typebox_exports.String(), Question)),
20179
+ questions: typebox_exports.Optional(typebox_exports.Record(typebox_exports.String(), QuestionSchema)),
19784
20180
  /** Where the content comes from. Defaults to the spec's `source`. */
19785
20181
  sources: typebox_exports.Optional(typebox_exports.Array(Source)),
20182
+ /**
20183
+ * Leave out paths matching these globs (repo-relative, like `glob`). A per-call
20184
+ * choice, reported as `filtered`; egress excludes still apply on top.
20185
+ */
20186
+ exclude: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String({ minLength: 1 }))),
19786
20187
  /** `file` | `hunk` | `row` | `lines:N[/overlap]`. */
19787
20188
  split: typebox_exports.Optional(typebox_exports.String()),
19788
20189
  /** Filters, ANDed: `relevant>=0.7`, `kind in fix,feature`. */
19789
20190
  keep: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
20191
+ /** Kept when any of these matches (and every `keep` does): `a>=0.3`, `b>=0.3`. */
20192
+ keepAny: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
19790
20193
  mode: typebox_exports.Optional(Mode),
19791
20194
  model: typebox_exports.Optional(typebox_exports.String()),
19792
20195
  /**
@@ -19824,6 +20227,24 @@ var SpecCheckInput = typebox_exports.Object({
19824
20227
  /** Proceed past the spend guard. */
19825
20228
  confirm: typebox_exports.Optional(typebox_exports.Boolean())
19826
20229
  }, strict);
20230
+ var SpecLintInput = typebox_exports.Object({
20231
+ /** Spec name (repo → user → bundled) or path. Omitted: every spec in reach. */
20232
+ spec: typebox_exports.Optional(typebox_exports.String({ minLength: 1 }))
20233
+ }, strict);
20234
+ var OpportunitiesAddInput = typebox_exports.Object({
20235
+ /** Candidates to merge into `.system1/opportunities.json`. */
20236
+ candidates: typebox_exports.Array(CandidateSchema, { minItems: 1 })
20237
+ }, strict);
20238
+ var OpportunitiesListInput = typebox_exports.Object({
20239
+ /** Record filters, ANDed: `status=new`, `risk in medium,high`, `projected>=0.01`. */
20240
+ keep: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String())),
20241
+ /** `projected:desc` (the default), `seenAt:asc`, … */
20242
+ sort: typebox_exports.Optional(typebox_exports.String()),
20243
+ limit: typebox_exports.Optional(typebox_exports.Integer({ minimum: 0 })),
20244
+ /** Top-level fields to include per entry; `id` always is. */
20245
+ fields: typebox_exports.Optional(typebox_exports.Array(typebox_exports.String()))
20246
+ }, strict);
20247
+ var OpportunitiesCheckInput = typebox_exports.Object({}, strict);
19827
20248
  var RouteInput = typebox_exports.Object({
19828
20249
  /** The user's prompt, as the harness hook received it. */
19829
20250
  prompt: typebox_exports.String()
@@ -19833,11 +20254,15 @@ var TOOL_SCHEMAS = {
19833
20254
  many: ManyInput,
19834
20255
  usage: UsageInput,
19835
20256
  "spec-check": SpecCheckInput,
20257
+ "spec-lint": SpecLintInput,
20258
+ "opportunities-add": OpportunitiesAddInput,
20259
+ "opportunities-list": OpportunitiesListInput,
20260
+ "opportunities-check": OpportunitiesCheckInput,
19836
20261
  route: RouteInput
19837
20262
  };
19838
20263
 
19839
20264
  // ../core/dist/tools/context.js
19840
- import { isAbsolute as isAbsolute4, join as join4, relative as relative4 } from "node:path";
20265
+ import { isAbsolute as isAbsolute4, join as join5, relative as relative4 } from "node:path";
19841
20266
 
19842
20267
  // ../core/dist/egress/scrub.js
19843
20268
  var SECRET_NAME = "[A-Za-z0-9_.-]*(?:api[_-]?key|secret|token|passw(?:or)?d|pwd|credential|private[_-]?key|auth)[A-Za-z0-9_.-]*";
@@ -19857,6 +20282,13 @@ var RULES = [
19857
20282
  { kind: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
19858
20283
  { kind: "stripe-key", pattern: /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
19859
20284
  { kind: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
20285
+ // Service tokens with a known prefix and separator, whatever they are
20286
+ // assigned to (MCP server env blocks name them freely: NOTION_INTEGRATION,
20287
+ // HONEYCOMB_WRITEKEY).
20288
+ {
20289
+ kind: "service-token",
20290
+ pattern: /\b(?:secret_|ntn_|glpat-|npm_|pypi-|shpat_|shpss_|dop_v1_|hcaik_|hcxik_|sntrys_|lin_api_|figd_|xapp-)[A-Za-z0-9_]{20,}\b/g
20291
+ },
19860
20292
  { kind: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
19861
20293
  { kind: "url-credentials", pattern: /(?<=\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s@/]{3,}(?=@)/gi },
19862
20294
  // NAME = "literal": a quoted value of 8+ chars assigned to a secret-sounding name.
@@ -19870,6 +20302,15 @@ var RULES = [
19870
20302
  {
19871
20303
  kind: "assigned-secret",
19872
20304
  pattern: new RegExp(`(?<=\\b${SECRET_NAME}\\s*[:=]\\s*)(?!\\$\\{|<|\\[REDACTED)(?=[A-Za-z_\\-+/=~]*\\d)[A-Za-z0-9_\\-+/=~]{8,}(?![\\w(.])`, "gi")
20305
+ },
20306
+ // ENV_NAME: "value" in upper-case environment style, ending in a credential
20307
+ // word (API_WRITEKEY, SERVICE_PAT). Case-sensitive, and the value must look
20308
+ // like a credential (12+ characters, letters and digits, no dots or colons),
20309
+ // so UI constants (`PROBE_KEY = "pulse:probe"`) and code are left alone.
20310
+ // Last, so a value an earlier rule already caught keeps that rule's name.
20311
+ {
20312
+ kind: "env-secret",
20313
+ pattern: /(?<=\b[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PAT|CREDENTIALS?)["']?\s*[:=]\s*["']?)(?!\$\{|<|\[REDACTED)(?=[A-Za-z0-9_\-+/=~]*\d)(?=[A-Za-z0-9_\-+/=~]*[A-Za-z])[A-Za-z0-9_\-+/=~]{12,}(?=["'\s,}]|$)/gm
19873
20314
  }
19874
20315
  ];
19875
20316
  function scrubText(text, counts2 = {}) {
@@ -19927,13 +20368,13 @@ function assertStateFits(id, state, questions, profile) {
19927
20368
  }
19928
20369
 
19929
20370
  // ../core/dist/fixtures/store.js
19930
- import { createHash } from "node:crypto";
19931
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync2 } from "node:fs";
19932
- import { dirname as dirname4, join as join3 } from "node:path";
20371
+ import { createHash as createHash2 } from "node:crypto";
20372
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "node:fs";
20373
+ import { dirname as dirname5, join as join4 } from "node:path";
19933
20374
  var FIXTURE_VERSION = 1;
19934
20375
  function fixtureKey(request) {
19935
20376
  const { model, state, questions } = request;
19936
- return createHash("sha256").update(canonicalJson({ model, state, questions })).digest("hex");
20377
+ return createHash2("sha256").update(canonicalJson({ model, state, questions })).digest("hex");
19937
20378
  }
19938
20379
  function canonicalJson(value) {
19939
20380
  return JSON.stringify(sortKeys(value));
@@ -19956,13 +20397,13 @@ var FixtureStore = class {
19956
20397
  if (!NAMESPACE.test(namespace)) {
19957
20398
  throw new Error(`Invalid fixture namespace "${namespace}" (lowercase letters, digits, . _ -)`);
19958
20399
  }
19959
- return join3(this.dir, namespace, `${key}.json`);
20400
+ return join4(this.dir, namespace, `${key}.json`);
19960
20401
  }
19961
20402
  lookup(namespace, request) {
19962
20403
  const file = this.path(namespace, fixtureKey(request));
19963
- if (!existsSync5(file))
20404
+ if (!existsSync6(file))
19964
20405
  return void 0;
19965
- const record = JSON.parse(readFileSync5(file, "utf8"));
20406
+ const record = JSON.parse(readFileSync6(file, "utf8"));
19966
20407
  return record.v === FIXTURE_VERSION ? record : void 0;
19967
20408
  }
19968
20409
  record(namespace, request, response, now = /* @__PURE__ */ new Date()) {
@@ -19975,11 +20416,11 @@ var FixtureStore = class {
19975
20416
  response
19976
20417
  };
19977
20418
  const file = this.path(namespace, key);
19978
- mkdirSync2(dirname4(file), { recursive: true });
20419
+ mkdirSync3(dirname5(file), { recursive: true });
19979
20420
  const temp = `${file}.${process.pid}.tmp`;
19980
- writeFileSync2(temp, `${JSON.stringify(record, null, 2)}
20421
+ writeFileSync3(temp, `${JSON.stringify(record, null, 2)}
19981
20422
  `);
19982
- renameSync(temp, file);
20423
+ renameSync2(temp, file);
19983
20424
  return record;
19984
20425
  }
19985
20426
  };
@@ -20057,8 +20498,8 @@ function createDecider(options) {
20057
20498
  }
20058
20499
 
20059
20500
  // ../core/dist/run/spend.js
20060
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6 } from "node:fs";
20061
- import { dirname as dirname5 } from "node:path";
20501
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7 } from "node:fs";
20502
+ import { dirname as dirname6 } from "node:path";
20062
20503
  function sumUsage(usages) {
20063
20504
  const total = usages.reduce((sum, u) => ({
20064
20505
  input_tokens: sum.input_tokens + (u?.input_tokens ?? 0),
@@ -20072,15 +20513,15 @@ var SpendLedger = class {
20072
20513
  constructor(file) {
20073
20514
  this.file = file;
20074
20515
  }
20075
- append(entry) {
20076
- mkdirSync3(dirname5(this.file), { recursive: true });
20077
- appendFileSync(this.file, `${JSON.stringify(entry)}
20516
+ append(entry2) {
20517
+ mkdirSync4(dirname6(this.file), { recursive: true });
20518
+ appendFileSync(this.file, `${JSON.stringify(entry2)}
20078
20519
  `);
20079
20520
  }
20080
20521
  entries() {
20081
- if (!existsSync6(this.file))
20522
+ if (!existsSync7(this.file))
20082
20523
  return [];
20083
- return readFileSync6(this.file, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
20524
+ return readFileSync7(this.file, "utf8").split("\n").filter((line) => line.trim() !== "").flatMap((line) => {
20084
20525
  try {
20085
20526
  return [JSON.parse(line)];
20086
20527
  } catch {
@@ -20145,15 +20586,33 @@ function resolveRequest(ctx, input) {
20145
20586
  sources,
20146
20587
  split: parseSplit(input.split ?? spec?.source?.split ?? "file"),
20147
20588
  keep: keepText.map((k) => parseFilter(k, questions)),
20589
+ keepAny: (input.keepAny ?? spec?.keepAny ?? []).map((k) => parseFilter(k, questions)),
20148
20590
  ...sortText ? { sort: parseSort(sortText, questions) } : {},
20149
- profile
20591
+ profile,
20592
+ exclude: (input.exclude ?? []).map((p) => rebasePattern(p, ctx.cwd, ctx.config.repoRoot))
20150
20593
  };
20151
20594
  }
20595
+ function rebasePattern(pattern, cwd, repoRoot) {
20596
+ if (pattern.startsWith("!")) {
20597
+ throw new DecisionsError("invalid-request", `--exclude "${pattern}": negation isn't supported; name what to leave out`);
20598
+ }
20599
+ if (isAbsolute4(pattern)) {
20600
+ const rel = relative4(repoRoot, pattern);
20601
+ if (rel.startsWith("..") || isAbsolute4(rel)) {
20602
+ throw new DecisionsError("invalid-request", `--exclude "${pattern}" is outside the repo (${repoRoot}), so it would match nothing`);
20603
+ }
20604
+ return rel;
20605
+ }
20606
+ const prefix = relative4(repoRoot, cwd);
20607
+ if (prefix === "" || prefix.startsWith("..") || isAbsolute4(prefix) || pattern.startsWith("**/"))
20608
+ return pattern;
20609
+ return join5(prefix, pattern);
20610
+ }
20152
20611
  function rebase(source, cwd, repoRoot) {
20153
20612
  const prefix = relative4(repoRoot, cwd);
20154
20613
  if (prefix === "" || prefix.startsWith("..") || isAbsolute4(prefix))
20155
20614
  return source;
20156
- const move = (path) => isAbsolute4(path) ? path : join4(prefix, path);
20615
+ const move = (path) => isAbsolute4(path) ? path : join5(prefix, path);
20157
20616
  switch (source.kind) {
20158
20617
  case "file":
20159
20618
  return { ...source, path: move(source.path) };
@@ -20193,14 +20652,14 @@ function deciderFor(ctx, profile, mode = "auto") {
20193
20652
  ...ctx.fetch ? { fetch: ctx.fetch } : {}
20194
20653
  })
20195
20654
  } : {},
20196
- fixtures: new FixtureStore(join4(dir, "fixtures")),
20197
- ledger: new SpendLedger(join4(dir, "usage.jsonl")),
20655
+ fixtures: new FixtureStore(join5(dir, "fixtures")),
20656
+ ledger: new SpendLedger(join5(dir, "usage.jsonl")),
20198
20657
  mode: config.replay ? "replay" : mode,
20199
20658
  ...config.session ? { session: config.session } : {}
20200
20659
  });
20201
20660
  }
20202
20661
  function ledgerFor(ctx) {
20203
- return new SpendLedger(join4(stateDir(ctx.config.repoRoot), "usage.jsonl"));
20662
+ return new SpendLedger(join5(stateDir(ctx.config.repoRoot), "usage.jsonl"));
20204
20663
  }
20205
20664
 
20206
20665
  // ../core/dist/run/budget.js
@@ -20245,7 +20704,8 @@ async function prepare(input) {
20245
20704
  const read2 = await readSources(input.sources, {
20246
20705
  cwd: input.cwd,
20247
20706
  ...input.maxFileBytes ? { maxFileBytes: input.maxFileBytes } : {},
20248
- ...input.allowOutside ? { allowOutside: true } : {}
20707
+ ...input.allowOutside ? { allowOutside: true } : {},
20708
+ ...input.filter?.length ? { filter: input.filter, withhold: input.exclude ?? [] } : {}
20249
20709
  });
20250
20710
  const byKind = {};
20251
20711
  let redactedSources = 0;
@@ -20256,7 +20716,8 @@ async function prepare(input) {
20256
20716
  return found2;
20257
20717
  };
20258
20718
  const allowed = applyExcludes(read2.documents, input.exclude);
20259
- const documents = allowed.items.map((doc) => {
20719
+ const chosen = applyFilter(allowed.items, input.filter);
20720
+ const documents = chosen.items.map((doc) => {
20260
20721
  const counts2 = {};
20261
20722
  const scrubbed = {
20262
20723
  ...doc,
@@ -20270,17 +20731,36 @@ async function prepare(input) {
20270
20731
  const filtered = applyExcludes(split(documents, input.split), input.exclude);
20271
20732
  const kept = input.split.kind === "join" ? joinItems(filtered.items) : filtered.items;
20272
20733
  const tokens = [];
20273
- const items = kept.map((item) => {
20734
+ const oversize = [];
20735
+ const items = kept.flatMap((item) => {
20274
20736
  const counts2 = {};
20275
20737
  const state = scrubState(item.state, counts2);
20276
20738
  if (count(counts2) > 0)
20277
20739
  redactedSources += 1;
20278
- tokens.push(assertStateFits(item.id, state, input.questions, input.profile));
20279
- return { ...item, state };
20740
+ try {
20741
+ tokens.push(assertStateFits(item.id, state, input.questions, input.profile));
20742
+ } catch (error) {
20743
+ if (input.oversize !== "skip" || !isDecisionsError(error))
20744
+ throw error;
20745
+ const estimated = error.details.estimatedTokens;
20746
+ oversize.push({
20747
+ path: item.path ?? item.id,
20748
+ reason: "too-large",
20749
+ detail: `about ${estimated} tokens with its questions (limit ${input.profile.maxStateTokens}); re-run it with --split lines:400/40`
20750
+ });
20751
+ return [];
20752
+ }
20753
+ return [{ ...item, state }];
20280
20754
  });
20281
20755
  return {
20282
20756
  items,
20283
- skipped: [...read2.skipped, ...allowed.excluded, ...filtered.excluded],
20757
+ skipped: unique([
20758
+ ...read2.skipped,
20759
+ ...allowed.excluded,
20760
+ ...chosen.filtered,
20761
+ ...filtered.excluded,
20762
+ ...oversize
20763
+ ]),
20284
20764
  redactions: {
20285
20765
  total: Object.values(byKind).reduce((a, b) => a + b, 0),
20286
20766
  byKind,
@@ -20299,6 +20779,16 @@ function assertChoicesFit(questions, profile) {
20299
20779
  }
20300
20780
  }
20301
20781
  }
20782
+ function unique(skipped) {
20783
+ const seen = /* @__PURE__ */ new Set();
20784
+ return skipped.filter((s) => {
20785
+ const key = `${s.path}\0${s.reason}`;
20786
+ if (seen.has(key))
20787
+ return false;
20788
+ seen.add(key);
20789
+ return true;
20790
+ });
20791
+ }
20302
20792
 
20303
20793
  // ../core/dist/run/pool.js
20304
20794
  async function mapWithConcurrency(items, limit, worker, onProgress) {
@@ -20339,6 +20829,8 @@ async function runMany(ctx, rawInput) {
20339
20829
  profile: req.profile,
20340
20830
  cwd: ctx.config.repoRoot,
20341
20831
  exclude: ctx.config.egress.exclude,
20832
+ filter: req.exclude,
20833
+ oversize: "skip",
20342
20834
  ...input.allowOutside ? { allowOutside: true } : {}
20343
20835
  });
20344
20836
  const base2 = {
@@ -20390,6 +20882,7 @@ async function runMany(ctx, rawInput) {
20390
20882
  const projected = project({
20391
20883
  rows,
20392
20884
  keep: req.keep,
20885
+ keepAny: req.keepAny,
20393
20886
  ...req.sort ? { sort: req.sort } : {},
20394
20887
  ...input.limit !== void 0 ? { limit: input.limit } : {},
20395
20888
  ...input.fields ? { fields: input.fields } : {},
@@ -20455,11 +20948,14 @@ async function runAsk(ctx, rawInput) {
20455
20948
  profile: req.profile,
20456
20949
  cwd: ctx.config.repoRoot,
20457
20950
  exclude: ctx.config.egress.exclude,
20951
+ filter: req.exclude,
20458
20952
  ...input.allowOutside ? { allowOutside: true } : {}
20459
20953
  });
20460
20954
  const [item, ...rest] = prepared.items;
20461
20955
  if (!item || rest.length > 0) {
20462
- const withheld = prepared.skipped.length ? ` (${prepared.skipped.length} withheld: ${prepared.skipped.map((s) => `${s.path} ${s.reason}`).join(", ")})` : "";
20956
+ const held = prepared.skipped.filter((s) => s.reason !== "filtered");
20957
+ const left = prepared.skipped.length - held.length;
20958
+ const withheld = (held.length ? ` (${held.length} withheld: ${held.map((s) => `${s.path} ${s.reason}`).join(", ")})` : "") + (left ? ` (${left} left out by --exclude)` : "");
20463
20959
  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 });
20464
20960
  }
20465
20961
  const result = await deciderFor(ctx, req.profile, input.mode ?? "auto").decide({
@@ -20467,11 +20963,7 @@ async function runAsk(ctx, rawInput) {
20467
20963
  questions: req.questions,
20468
20964
  namespace: req.namespace
20469
20965
  });
20470
- let verdict;
20471
- if (req.keep.length > 0) {
20472
- const referenced = new Set(req.keep.map((f) => f.question));
20473
- verdict = result.undecided.some((q) => referenced.has(q)) ? "undecided" : req.keep.every((f) => matches(result.answers, f)) ? "kept" : "dropped";
20474
- }
20966
+ const verdict = req.keep.length > 0 || req.keepAny.length > 0 ? verdictOf(result.answers, result.undecided, req.keep, req.keepAny).verdict : void 0;
20475
20967
  return {
20476
20968
  ...req.spec ? { spec: req.spec.name } : {},
20477
20969
  id: item.id,
@@ -20491,8 +20983,8 @@ async function runAsk(ctx, rawInput) {
20491
20983
  }
20492
20984
 
20493
20985
  // ../core/dist/tools/doctor.js
20494
- import { accessSync, constants, statSync as statSync4 } from "node:fs";
20495
- import { delimiter, join as join5 } from "node:path";
20986
+ import { accessSync, constants, existsSync as existsSync8, statSync as statSync5 } from "node:fs";
20987
+ import { delimiter, join as join6 } from "node:path";
20496
20988
  function routeCheck(config) {
20497
20989
  try {
20498
20990
  route("", config);
@@ -20518,6 +21010,32 @@ function routeCheck(config) {
20518
21010
  };
20519
21011
  }
20520
21012
  }
21013
+ function backlogProblem(error) {
21014
+ const problems = isDecisionsError(error) ? error.details?.problems : void 0;
21015
+ const first = Array.isArray(problems) ? problems[0] : void 0;
21016
+ const count = Array.isArray(problems) && problems.length > 1 ? ` (+${problems.length - 1} more)` : "";
21017
+ return `the backlog doesn't validate: ${first ?? (error instanceof Error ? error.message : String(error))}${count}`;
21018
+ }
21019
+ function backlogCheck(repoRoot) {
21020
+ const file = backlogPath(repoRoot);
21021
+ if (!existsSync8(file))
21022
+ return { name: "backlog", status: "ok", detail: "no scout backlog yet" };
21023
+ try {
21024
+ const { opportunities } = readBacklog(file);
21025
+ return {
21026
+ name: "backlog",
21027
+ status: "ok",
21028
+ detail: `${opportunities.length} opportunit${opportunities.length === 1 ? "y" : "ies"} in ${file}`
21029
+ };
21030
+ } catch (error) {
21031
+ return {
21032
+ name: "backlog",
21033
+ status: "warn",
21034
+ detail: backlogProblem(error),
21035
+ fix: "`decide opportunities check` lists every problem; correct the file by hand or move it aside (decide never repairs it)"
21036
+ };
21037
+ }
21038
+ }
20521
21039
  var CODEX_RULE = 'prefix_rule(pattern = ["decide"], decision = "allow")';
20522
21040
  async function runDoctor(options) {
20523
21041
  const { env } = options;
@@ -20569,7 +21087,7 @@ async function runDoctor(options) {
20569
21087
  status: "warn",
20570
21088
  detail: "no API key: only replay works",
20571
21089
  // The file this shell would read: XDG_CONFIG_HOME moves it.
20572
- fix: `set OPENROUTER_API_KEY, or write \`openrouter_api_key: <key>\` to ${join5(userConfigDir(env, options.home), "credentials")} (create its directory first; chmod 600)`
21090
+ fix: `set OPENROUTER_API_KEY, or write \`openrouter_api_key: <key>\` to ${join6(userConfigDir(env, options.home), "credentials")} (create its directory first; chmod 600)`
20573
21091
  });
20574
21092
  checks.push(config.egress.consent.granted ? { name: "consent", status: "ok", detail: `egress consent granted for ${config.repoRoot}` } : {
20575
21093
  name: "consent",
@@ -20578,6 +21096,7 @@ async function runDoctor(options) {
20578
21096
  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"
20579
21097
  });
20580
21098
  checks.push(routeCheck(config.route));
21099
+ checks.push(backlogCheck(config.repoRoot));
20581
21100
  let reach;
20582
21101
  try {
20583
21102
  reach = await ping({
@@ -20687,10 +21206,10 @@ function which(name, path) {
20687
21206
  for (const dir of path.split(delimiter)) {
20688
21207
  if (!dir)
20689
21208
  continue;
20690
- const candidate = join5(dir, name);
21209
+ const candidate = join6(dir, name);
20691
21210
  try {
20692
21211
  accessSync(candidate, constants.X_OK);
20693
- if (statSync4(candidate).isFile())
21212
+ if (statSync5(candidate).isFile())
20694
21213
  return candidate;
20695
21214
  } catch {
20696
21215
  }
@@ -20698,6 +21217,72 @@ function which(name, path) {
20698
21217
  return void 0;
20699
21218
  }
20700
21219
 
21220
+ // ../core/dist/tools/opportunities.js
21221
+ import { existsSync as existsSync9 } from "node:fs";
21222
+ function runOpportunitiesAdd(ctx, rawInput, now = () => /* @__PURE__ */ new Date()) {
21223
+ const input = checkInput("opportunities-add", rawInput);
21224
+ const file = backlogPath(ctx.config.repoRoot);
21225
+ const merged2 = withBacklogLock(file, () => {
21226
+ const result = mergeCandidates(readBacklog(file), input.candidates, now().toISOString());
21227
+ writeBacklog(file, result.backlog);
21228
+ return result;
21229
+ });
21230
+ return {
21231
+ file,
21232
+ added: merged2.added,
21233
+ updated: merged2.updated,
21234
+ staled: merged2.staled,
21235
+ total: merged2.backlog.opportunities.length
21236
+ };
21237
+ }
21238
+ function runOpportunitiesList(ctx, rawInput = {}) {
21239
+ const input = checkInput("opportunities-list", rawInput);
21240
+ const filters = (input.keep ?? []).map(parseRecordFilter);
21241
+ const sort = parseRecordSort(input.sort ?? "projected:desc");
21242
+ assertFields(input.fields);
21243
+ const file = backlogPath(ctx.config.repoRoot);
21244
+ const { opportunities } = readBacklog(file);
21245
+ 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));
21246
+ const shown = input.limit === void 0 ? matched : matched.slice(0, input.limit);
21247
+ return {
21248
+ file,
21249
+ total: opportunities.length,
21250
+ matched: matched.length,
21251
+ basis: "projected",
21252
+ opportunities: shown.map((o) => pick2(o, input.fields))
21253
+ };
21254
+ }
21255
+ function runOpportunitiesCheck(ctx, rawInput = {}) {
21256
+ checkInput("opportunities-check", rawInput);
21257
+ const file = backlogPath(ctx.config.repoRoot);
21258
+ const backlog = readBacklog(file);
21259
+ const byStatus = {};
21260
+ for (const o of backlog.opportunities)
21261
+ byStatus[o.status] = (byStatus[o.status] ?? 0) + 1;
21262
+ return { file, exists: existsSync9(file), entries: backlog.opportunities.length, byStatus };
21263
+ }
21264
+ function compare(a, b, direction) {
21265
+ if (a === void 0 || b === void 0)
21266
+ return a === b ? 0 : a === void 0 ? 1 : -1;
21267
+ const order = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b));
21268
+ return direction === "asc" ? order : -order;
21269
+ }
21270
+ function pick2(o, fields) {
21271
+ if (!fields)
21272
+ return o;
21273
+ const out = { id: o.id };
21274
+ for (const f of fields)
21275
+ if (Object.hasOwn(o, f))
21276
+ out[f] = o[f];
21277
+ return out;
21278
+ }
21279
+ function assertFields(fields = []) {
21280
+ const unknown = fields.filter((f) => !ENTRY_FIELDS.includes(f));
21281
+ if (unknown.length) {
21282
+ throw new DecisionsError("invalid-request", `--fields: no field ${unknown.join(", ")}. Fields: ${ENTRY_FIELDS.join(", ")}`);
21283
+ }
21284
+ }
21285
+
20701
21286
  // ../core/dist/tools/route.js
20702
21287
  function runRoute(options, rawInput) {
20703
21288
  const problems = checkRouteInput(rawInput);
@@ -20725,10 +21310,133 @@ function checkRouteInput(input) {
20725
21310
  ];
20726
21311
  }
20727
21312
 
21313
+ // ../core/dist/spec/lint.js
21314
+ var LINT_SEVERITY = {
21315
+ "no-way-out": "warning",
21316
+ "abstract-levels": "warning",
21317
+ "unsupported-task": "warning",
21318
+ "merged-question": "warning",
21319
+ "unjustified-threshold": "warning",
21320
+ "unknown-threshold": "error"
21321
+ };
21322
+ var WAY_OUT_KEY = /^(none|other|others|unclear|unknown|neither|na|nomatch|unsure|mixed|else|nothing|irrelevant|unrelated)$/i;
21323
+ var WAY_OUT_TEXT = /\b(none of|any other|anything else|something else|neither|unclear|not (?:clear|sure|applicable|enough)|no match|does(?:n't| not) (?:fit|apply)|other than (?:these|the above))\b/i;
21324
+ var UNSUPPORTED = [
21325
+ {
21326
+ kind: "counting or arithmetic",
21327
+ pattern: /(^\s*count\b|\bhow many\b|\bthe number of\b|\bcount (?:all|every|each|how|the number)\b|\b(?:more|fewer|less|greater|longer|shorter|larger|smaller|higher|lower) than \d|\b(?:over|under|above|below|at least|at most|exactly|up to) \d|\b\d+(?:\.\d+)?\s?(?:%|percent)|\bsum of\b|\btotal of\b|\baverage of\b|\bon average\b)/i
21328
+ },
21329
+ {
21330
+ kind: "dates",
21331
+ pattern: /(\b(?:older|newer|younger) than \d+ (?:days?|weeks?|months?|years?)\b|\b\d+ (?:days?|weeks?|months?|years?) (?:ago|old)\b|\b(?:within|in) the (?:last|past|next) \d+ (?:days?|weeks?|months?|years?)\b|\b(?:yesterday|tomorrow)\b|\b(?:this|last|next) (?:week|month|quarter|year)\b|\b(?:before|after|since|until|by) (?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|january|february|march|april|may|june|july|august|september|october|november|december|(?:19|20)\d\d)\b|\b(?:has|is|was) (?:expired|overdue)\b)/i
21332
+ },
21333
+ {
21334
+ kind: "images",
21335
+ pattern: /(\bscreenshot|\bphoto(?:graph)?s?\b|\b(?:the|this|an?) (?:image|picture) (?:shows?|contains?|depicts?)\b|\b(?:visible|shown|appears?|seen) in (?:the|this|an?) (?:image|picture)\b)/i
21336
+ },
21337
+ {
21338
+ kind: "an exact fact to read or run, not judge",
21339
+ pattern: /(\b(?:all|every|the) tests? (?:pass(?:es|ed)?|fail(?:s|ed)?|succeed(?:s|ed)?)\b|\btests? (?:pass(?:es|ed)?|are (?:passing|green))\b|\b(?:the )?(?:build|ci|pipeline) (?:passes|passed|succeeds|succeeded|is green)\b|\b(?:it )?compiles without errors\b)/i
21340
+ }
21341
+ ];
21342
+ function lintQuestions(questions) {
21343
+ const out = [];
21344
+ for (const [name, q] of Object.entries(questions)) {
21345
+ out.push(...lintQuestion(name, q));
21346
+ }
21347
+ return out;
21348
+ }
21349
+ function lintSpec(spec) {
21350
+ const out = lintQuestions(spec.questions);
21351
+ const thresholds = spec.policy?.thresholds ?? {};
21352
+ for (const name of Object.keys(thresholds)) {
21353
+ if (!(name in spec.questions)) {
21354
+ out.push(finding("unknown-threshold", `policy.thresholds.${name} names no question in this spec`, `Rename it to one of: ${Object.keys(spec.questions).join(", ")}, or remove it.`));
21355
+ }
21356
+ }
21357
+ const justified = new Set(Object.keys(thresholds));
21358
+ const reported = /* @__PURE__ */ new Set();
21359
+ const filters = [
21360
+ ...(spec.keep ?? []).map((text) => ({ text, list: "keep" })),
21361
+ ...(spec.keepAny ?? []).map((text) => ({ text, list: "keepAny" }))
21362
+ ];
21363
+ for (const { text, list } of filters) {
21364
+ const { question, op } = parseFilter(text, spec.questions);
21365
+ if (!NUMERIC.has(op))
21366
+ continue;
21367
+ if (justified.has(question) || reported.has(question))
21368
+ continue;
21369
+ reported.add(question);
21370
+ out.push(finding("unjustified-threshold", `${list} "${text}" has no policy.thresholds.${question} saying why`, `Add policy.thresholds.${question}: {value, why}, with the cost of a wrong keep and a wrong drop (see thresholds.md).`, question));
21371
+ }
21372
+ return out;
21373
+ }
21374
+ var NUMERIC = /* @__PURE__ */ new Set([">=", ">", "<=", "<"]);
21375
+ function lintQuestion(name, q) {
21376
+ const out = [];
21377
+ const texts = [q.instructions, ...criteriaTexts(q)];
21378
+ if (q.type === "choice") {
21379
+ const options = Object.entries(q.criteria);
21380
+ const wayOut = ([key, desc]) => key.split(/[_\-\s/]+/).some((t) => WAY_OUT_KEY.test(t)) || WAY_OUT_TEXT.test(desc);
21381
+ if (!options.some(wayOut)) {
21382
+ out.push(finding("no-way-out", `choice "${name}" has no none/other/unclear option`, "Add an option such as `none: None of these apply`, so text that fits nothing isn't forced onto an option.", name));
21383
+ }
21384
+ }
21385
+ if (q.type === "score" && q.criteria.every((level) => words(level) <= 2)) {
21386
+ out.push(finding("abstract-levels", `score "${name}" has one- or two-word levels (${q.criteria.join(" | ")})`, "Describe each level as a concrete situation someone could spot in the text, lowest first.", name));
21387
+ }
21388
+ out.push(...unsupported([q.instructions, ...criteriaTexts(q).map(unquoted)].join("\n"), name));
21389
+ if (merged(q.instructions)) {
21390
+ out.push(finding("merged-question", `"${name}" looks like two questions in one`, "Split it into one question per idea and combine the answers with --keep.", name));
21391
+ }
21392
+ return dedupe(out);
21393
+ }
21394
+ function unsupported(text, question) {
21395
+ return UNSUPPORTED.filter(({ pattern }) => pattern.test(text)).map(({ kind, pattern }) => finding("unsupported-task", `${question ? `"${question}" ` : ""}asks for ${kind} ("${pattern.exec(text)?.[0]}")`, "Do that part in code (grep, a date check, a count) and ask the model only about what's left.", question));
21396
+ }
21397
+ function merged(instructions) {
21398
+ const claims = instructions.replace(/\bhttps?:\/\/\S+/gi, "URL").replace(/\b(e\.g|i\.e|etc|vs|cf|mr|mrs|ms|dr|st|no|approx)\./gi, "$1").split(/[.?!](?:\s+|$)/).map((s) => s.trim()).filter((s) => words(s) >= 3 && !FRAMING.test(s));
21399
+ return claims.length > 1 || /\band\/or\b/i.test(instructions) || /\beither\b.+\bor\b/i.test(instructions);
21400
+ }
21401
+ var FRAMING = /^(consider|only|ignore|answer|note|treat|assume|look|focus|read|judge|use|do not|don't|exclude|include|given|for example|e\.g|if|when|here|this question|count only|disregard)\b/i;
21402
+ function unquoted(text) {
21403
+ return text.replace(/"[^"\n]*"|“[^”\n]*”/g, '""');
21404
+ }
21405
+ function criteriaTexts(q) {
21406
+ if (q.type === "noul")
21407
+ return q.criteria ? Object.values(q.criteria) : [];
21408
+ if (q.type === "choice")
21409
+ return Object.values(q.criteria);
21410
+ return q.criteria;
21411
+ }
21412
+ function words(text) {
21413
+ return text.trim().split(/\s+/).filter(Boolean).length;
21414
+ }
21415
+ function finding(check, message, fix, question) {
21416
+ return {
21417
+ check,
21418
+ severity: LINT_SEVERITY[check],
21419
+ ...question ? { question } : {},
21420
+ message,
21421
+ fix
21422
+ };
21423
+ }
21424
+ function dedupe(findings) {
21425
+ const seen = /* @__PURE__ */ new Set();
21426
+ return findings.filter((f) => {
21427
+ const key = `${f.check}\0${f.question ?? ""}\0${f.message}`;
21428
+ if (seen.has(key))
21429
+ return false;
21430
+ seen.add(key);
21431
+ return true;
21432
+ });
21433
+ }
21434
+
20728
21435
  // ../core/dist/tools/spec-check.js
20729
21436
  async function runSpecCheck(ctx, rawInput) {
20730
21437
  const input = checkInput("spec-check", rawInput);
20731
21438
  const spec = loadSpec(input.spec, ctx.specDirs, ctx.cwd);
21439
+ const lint = lintSpec(spec);
20732
21440
  const examples = spec.examples ?? [];
20733
21441
  if (examples.length === 0) {
20734
21442
  throw new DecisionsError("invalid-request", `Spec "${spec.name}" has no examples to check. Add some under examples: (id, state or file, expect).`);
@@ -20826,7 +21534,8 @@ async function runSpecCheck(ctx, rawInput) {
20826
21534
  counts: counts2,
20827
21535
  examples: results,
20828
21536
  usage: sumUsage(usages),
20829
- skipped: summariseSkipped(prepared.flatMap((p) => p.prep?.skipped ?? []))
21537
+ skipped: summariseSkipped(prepared.flatMap((p) => p.prep?.skipped ?? [])),
21538
+ lint
20830
21539
  };
20831
21540
  }
20832
21541
  function judge(id, answers, flat, expect) {
@@ -20858,6 +21567,43 @@ function judge(id, answers, flat, expect) {
20858
21567
  };
20859
21568
  }
20860
21569
 
21570
+ // ../core/dist/tools/spec-lint.js
21571
+ import { basename as basename3, extname as extname2 } from "node:path";
21572
+ function runSpecLint(ctx, rawInput = {}) {
21573
+ const input = checkInput("spec-lint", rawInput);
21574
+ 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 } : (
21575
+ // Loaded by path, so keep the layer it was listed from.
21576
+ { ...entry(loadSpec(s.file, ctx.specDirs, ctx.cwd)), origin: s.origin }
21577
+ ));
21578
+ const all = specs.flatMap((s) => s.findings);
21579
+ const counts2 = {
21580
+ specs: specs.length,
21581
+ errors: all.filter((f) => f.severity === "error").length,
21582
+ warnings: all.filter((f) => f.severity === "warning").length,
21583
+ invalid: specs.filter((s) => s.invalid).length
21584
+ };
21585
+ return { specs, counts: counts2, passed: counts2.errors === 0 && counts2.invalid === 0 };
21586
+ }
21587
+ function named(ctx, ref) {
21588
+ try {
21589
+ return entry(loadSpec(ref, ctx.specDirs, ctx.cwd));
21590
+ } catch (error) {
21591
+ const file = isDecisionsError(error) ? error.details?.file : void 0;
21592
+ if (typeof file !== "string")
21593
+ throw error;
21594
+ return {
21595
+ name: basename3(file, extname2(file)),
21596
+ file,
21597
+ origin: "path",
21598
+ findings: [],
21599
+ invalid: error.message
21600
+ };
21601
+ }
21602
+ }
21603
+ function entry(spec) {
21604
+ return { name: spec.name, file: spec.file, origin: spec.origin, findings: lintSpec(spec) };
21605
+ }
21606
+
20861
21607
  // ../core/dist/tools/usage.js
20862
21608
  function runUsage(ctx, rawInput = {}) {
20863
21609
  const input = checkInput("usage", rawInput);
@@ -20918,8 +21664,12 @@ export {
20918
21664
  runMany,
20919
21665
  runAsk,
20920
21666
  runDoctor,
21667
+ runOpportunitiesAdd,
21668
+ runOpportunitiesList,
21669
+ runOpportunitiesCheck,
20921
21670
  runRoute,
20922
21671
  runSpecCheck,
21672
+ runSpecLint,
20923
21673
  runUsage,
20924
21674
  resolveConnection
20925
21675
  };