finitemonkey 0.1.0-next.24 → 0.1.0-next.25

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.
Files changed (2) hide show
  1. package/dist/finitemonkey.mjs +246 -82
  2. package/package.json +1 -1
@@ -3239,6 +3239,36 @@ var deadlineFor = (event, raw) => {
3239
3239
  return EVENT_DEADLINES[event] ?? DEFAULT_HOOK_DEADLINE_MS;
3240
3240
  };
3241
3241
 
3242
+ // src/gate/hooks/common.ts
3243
+ import { dirname as dirname5, join as join11 } from "node:path";
3244
+ var DEFAULT_GATE_BUDGET_MS = 3e5;
3245
+ var sessionCwd = (input) => input.payload.cwd === void 0 ? input.cwd : toAbsolutePath(input.payload.cwd, input.cwd);
3246
+ var MAX_DEPTH = 64;
3247
+ var findRepoRoot = (runtime, start) => {
3248
+ let current2 = start;
3249
+ for (let depth = 0; depth < MAX_DEPTH; depth += 1) {
3250
+ if (runtime.stat(toAbsolutePath(join11(current2, ".git"))) !== void 0) return current2;
3251
+ const parent = toAbsolutePath(dirname5(current2));
3252
+ if (parent === current2) return void 0;
3253
+ current2 = parent;
3254
+ }
3255
+ return void 0;
3256
+ };
3257
+ var projectPaths = (input) => {
3258
+ const root = findRepoRoot(input.runtime, sessionCwd(input));
3259
+ return root === void 0 ? void 0 : fmPaths(root);
3260
+ };
3261
+ var gateBudget = (input) => {
3262
+ const parsed = Number.parseInt(input.env["FINITEMONKEY_GATE_BUDGET_MS"] ?? "", 10);
3263
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GATE_BUDGET_MS;
3264
+ };
3265
+ var allowWith = (output) => ({ kind: "allow", output });
3266
+ var contextOutput = (event, additionalContext, systemMessage) => ({
3267
+ hookSpecificOutput: { hookEventName: event, additionalContext },
3268
+ ...systemMessage === void 0 ? {} : { systemMessage }
3269
+ });
3270
+ var human = (text8) => `finitemonkey: ${text8}`;
3271
+
3242
3272
  // src/hooks/fields.ts
3243
3273
  var textField = (record3, ...names) => {
3244
3274
  for (const name of names) {
@@ -4005,7 +4035,7 @@ var unwrap = (initial, stdin) => {
4005
4035
  };
4006
4036
 
4007
4037
  // src/shell/normalize.ts
4008
- var MAX_DEPTH = 8;
4038
+ var MAX_DEPTH2 = 8;
4009
4039
  var ASSIGNMENT2 = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s;
4010
4040
  var PS_ENV = /^\$env:([A-Za-z_][A-Za-z0-9_]*)=?(.*)$/is;
4011
4041
  var CHANGES_DIR = /* @__PURE__ */ new Set(["cd", "pushd", "set-location", "push-location"]);
@@ -4081,7 +4111,7 @@ var visit = (raw, language, context, depth, collector) => {
4081
4111
  for (const extra of result.extra) emit(extra, raw, language, context, collector);
4082
4112
  };
4083
4113
  var collect = (text8, language, context, depth, collector) => {
4084
- if (depth > MAX_DEPTH) {
4114
+ if (depth > MAX_DEPTH2) {
4085
4115
  collector.incomplete = true;
4086
4116
  return;
4087
4117
  }
@@ -4310,7 +4340,7 @@ var selfGuard = (input) => {
4310
4340
  };
4311
4341
 
4312
4342
  // src/guards/tests/hook.ts
4313
- import { join as join11, relative as relative3 } from "node:path";
4343
+ import { join as join12, relative as relative3 } from "node:path";
4314
4344
 
4315
4345
  // src/guards/tests/patterns.ts
4316
4346
  var JS_TEST = /(^|\/)(__tests__|__test__)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)tests?\/.*\.[cm]?[jt]sx?$/;
@@ -4500,7 +4530,7 @@ var shellViolations = (input, root) => {
4500
4530
  return parseShell(command).flatMap(
4501
4531
  (simple) => writtenPaths(simple).flatMap((path) => {
4502
4532
  const local = localPath(root, path);
4503
- const exists = input.runtime.stat(toAbsolutePath(join11(root, local))) !== void 0;
4533
+ const exists = input.runtime.stat(toAbsolutePath(join12(root, local))) !== void 0;
4504
4534
  if (!exists) return [];
4505
4535
  if (isProtectedConfig(local)) return [{ kind: "config_changed", path: local }];
4506
4536
  return isTestFile(local) ? [{ kind: "test_removed", path: local }] : [];
@@ -4521,38 +4551,6 @@ var testsGuard = (input) => {
4521
4551
 
4522
4552
  // src/look/reads.ts
4523
4553
  import { relative as relative4 } from "node:path";
4524
-
4525
- // src/gate/hooks/common.ts
4526
- import { dirname as dirname5, join as join12 } from "node:path";
4527
- var DEFAULT_GATE_BUDGET_MS = 3e5;
4528
- var sessionCwd = (input) => input.payload.cwd === void 0 ? input.cwd : toAbsolutePath(input.payload.cwd, input.cwd);
4529
- var MAX_DEPTH2 = 64;
4530
- var findRepoRoot = (runtime, start) => {
4531
- let current2 = start;
4532
- for (let depth = 0; depth < MAX_DEPTH2; depth += 1) {
4533
- if (runtime.stat(toAbsolutePath(join12(current2, ".git"))) !== void 0) return current2;
4534
- const parent = toAbsolutePath(dirname5(current2));
4535
- if (parent === current2) return void 0;
4536
- current2 = parent;
4537
- }
4538
- return void 0;
4539
- };
4540
- var projectPaths = (input) => {
4541
- const root = findRepoRoot(input.runtime, sessionCwd(input));
4542
- return root === void 0 ? void 0 : fmPaths(root);
4543
- };
4544
- var gateBudget = (input) => {
4545
- const parsed = Number.parseInt(input.env["FINITEMONKEY_GATE_BUDGET_MS"] ?? "", 10);
4546
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GATE_BUDGET_MS;
4547
- };
4548
- var allowWith = (output) => ({ kind: "allow", output });
4549
- var contextOutput = (event, additionalContext, systemMessage) => ({
4550
- hookSpecificOutput: { hookEventName: event, additionalContext },
4551
- ...systemMessage === void 0 ? {} : { systemMessage }
4552
- });
4553
- var human = (text8) => `finitemonkey: ${text8}`;
4554
-
4555
- // src/look/reads.ts
4556
4554
  var READS_FILE = "look-reads.json";
4557
4555
  var KEEP = 500;
4558
4556
  var readLookReads = (runtime, paths) => {
@@ -6585,7 +6583,19 @@ var en_default = {
6585
6583
  "statusline.gate.red": "gate red",
6586
6584
  "statusline.gate.incomplete": "gate incomplete",
6587
6585
  "statusline.gate.did_not_run": "gate did not run",
6588
- "statusline.context": "ctx {percent}%"
6586
+ "statusline.context": "ctx {percent}%",
6587
+ "command.gain.summary": "What the guards did: saves next to false alarms and gate failures",
6588
+ "gain.title": "Finite Monkey, last {days, plural, one {# day} other {# days}} (from .finitemonkey/log.jsonl and the evidence ledgers):",
6589
+ "gain.row.blocks": "{count, plural, one {# block} other {# blocks}} by the guards",
6590
+ "gain.row.saves": "{count, plural, one {# block} other {# blocks}} followed by a green gate within a day",
6591
+ "gain.row.false_positives": "{count, plural, one {# false alarm} other {# false alarms}} (releases typed by the person, and false positives recorded)",
6592
+ "gain.row.gate_runs": "{count, plural, one {# gate run} other {# gate runs}}",
6593
+ "gain.row.gate_failed": "{count, plural, one {# gate run} other {# gate runs}} failed",
6594
+ "gain.row.gate_unverified": "{count, plural, one {# gate run} other {# gate runs}} ended not verified",
6595
+ "gain.row.gate_time": "{minutes, plural, one {# minute} other {# minutes}} spent in the gate",
6596
+ "gain.up": "up {delta} on the window before",
6597
+ "gain.down": "down {delta} on the window before",
6598
+ "gain.footer": "Counts come from local files only; a save is a block followed by a passing gate, not proof that the block caused it."
6589
6599
  };
6590
6600
 
6591
6601
  // src/i18n/es.json
@@ -7243,7 +7253,19 @@ var es_default = {
7243
7253
  "statusline.gate.red": "compuerta roja",
7244
7254
  "statusline.gate.incomplete": "compuerta incompleta",
7245
7255
  "statusline.gate.did_not_run": "la compuerta no se ejecutó",
7246
- "statusline.context": "ctx {percent}%"
7256
+ "statusline.context": "ctx {percent}%",
7257
+ "command.gain.summary": "Lo que hicieron los bloqueos: aciertos junto a falsas alarmas y fallos de la compuerta",
7258
+ "gain.title": "Finite Monkey, {days, plural, one {último # día} other {últimos # días}} (de .finitemonkey/log.jsonl y los ledgers de evidencia):",
7259
+ "gain.row.blocks": "{count, plural, one {# bloqueo} other {# bloqueos}} de los guards",
7260
+ "gain.row.saves": "{count, plural, one {# bloqueo seguido} other {# bloqueos seguidos}} de una compuerta en verde en menos de un día",
7261
+ "gain.row.false_positives": "{count, plural, one {# falsa alarma} other {# falsas alarmas}} (liberaciones escritas por la persona y falsos positivos registrados)",
7262
+ "gain.row.gate_runs": "{count, plural, one {# ejecución de la compuerta} other {# ejecuciones de la compuerta}}",
7263
+ "gain.row.gate_failed": "{count, plural, one {# ejecución de la compuerta falló} other {# ejecuciones de la compuerta fallaron}}",
7264
+ "gain.row.gate_unverified": "{count, plural, one {# ejecución de la compuerta terminó} other {# ejecuciones de la compuerta terminaron}} sin verificar",
7265
+ "gain.row.gate_time": "{minutes, plural, one {# minuto} other {# minutos}} en la compuerta",
7266
+ "gain.up": "+{delta} respecto a la ventana anterior",
7267
+ "gain.down": "-{delta} respecto a la ventana anterior",
7268
+ "gain.footer": "Los conteos salen solo de archivos locales; un acierto es un bloqueo seguido de una compuerta en verde, no prueba de que el bloqueo lo causara."
7247
7269
  };
7248
7270
 
7249
7271
  // src/i18n/pt-BR.json
@@ -7901,7 +7923,19 @@ var pt_BR_default = {
7901
7923
  "statusline.gate.red": "portão vermelho",
7902
7924
  "statusline.gate.incomplete": "portão incompleto",
7903
7925
  "statusline.gate.did_not_run": "portão não rodou",
7904
- "statusline.context": "ctx {percent}%"
7926
+ "statusline.context": "ctx {percent}%",
7927
+ "command.gain.summary": "O que as travas fizeram: acertos ao lado dos alarmes falsos e das falhas do portão",
7928
+ "gain.title": "Finite Monkey, últimos {days, plural, one {# dia} other {# dias}} (de .finitemonkey/log.jsonl e dos ledgers de evidência):",
7929
+ "gain.row.blocks": "{count, plural, one {# bloqueio} other {# bloqueios}} das travas",
7930
+ "gain.row.saves": "{count, plural, one {# bloqueio seguido} other {# bloqueios seguidos}} de portão verde em até um dia",
7931
+ "gain.row.false_positives": "{count, plural, one {# alarme falso} other {# alarmes falsos}} (liberações digitadas pela pessoa e falsos positivos registrados)",
7932
+ "gain.row.gate_runs": "{count, plural, one {# execução do portão} other {# execuções do portão}}",
7933
+ "gain.row.gate_failed": "{count, plural, one {# execução do portão reprovou} other {# execuções do portão reprovaram}}",
7934
+ "gain.row.gate_unverified": "{count, plural, one {# execução do portão terminou} other {# execuções do portão terminaram}} sem verificar",
7935
+ "gain.row.gate_time": "{minutes, plural, one {# minuto} other {# minutos}} no portão",
7936
+ "gain.up": "+{delta} em relação à janela anterior",
7937
+ "gain.down": "-{delta} em relação à janela anterior",
7938
+ "gain.footer": "As contagens vêm só de arquivos locais; um acerto é um bloqueio seguido de portão verde, não prova de que o bloqueio causou isso."
7905
7939
  };
7906
7940
 
7907
7941
  // src/i18n/review.json
@@ -8560,7 +8594,19 @@ var review_default = {
8560
8594
  "statusline.gate.red": "machine",
8561
8595
  "statusline.gate.incomplete": "machine",
8562
8596
  "statusline.gate.did_not_run": "machine",
8563
- "statusline.context": "machine"
8597
+ "statusline.context": "machine",
8598
+ "command.gain.summary": "machine",
8599
+ "gain.title": "machine",
8600
+ "gain.row.blocks": "machine",
8601
+ "gain.row.saves": "machine",
8602
+ "gain.row.false_positives": "machine",
8603
+ "gain.row.gate_runs": "machine",
8604
+ "gain.row.gate_failed": "machine",
8605
+ "gain.row.gate_unverified": "machine",
8606
+ "gain.row.gate_time": "machine",
8607
+ "gain.up": "machine",
8608
+ "gain.down": "machine",
8609
+ "gain.footer": "machine"
8564
8610
  },
8565
8611
  es: {
8566
8612
  "command.init.summary": "machine",
@@ -9216,7 +9262,19 @@ var review_default = {
9216
9262
  "statusline.gate.red": "machine",
9217
9263
  "statusline.gate.incomplete": "machine",
9218
9264
  "statusline.gate.did_not_run": "machine",
9219
- "statusline.context": "machine"
9265
+ "statusline.context": "machine",
9266
+ "command.gain.summary": "machine",
9267
+ "gain.title": "machine",
9268
+ "gain.row.blocks": "machine",
9269
+ "gain.row.saves": "machine",
9270
+ "gain.row.false_positives": "machine",
9271
+ "gain.row.gate_runs": "machine",
9272
+ "gain.row.gate_failed": "machine",
9273
+ "gain.row.gate_unverified": "machine",
9274
+ "gain.row.gate_time": "machine",
9275
+ "gain.up": "machine",
9276
+ "gain.down": "machine",
9277
+ "gain.footer": "machine"
9220
9278
  }
9221
9279
  };
9222
9280
 
@@ -14958,7 +15016,18 @@ var mergeOutputs = (left, right) => {
14958
15016
  if (specific !== void 0) merged2["hookSpecificOutput"] = specific;
14959
15017
  return merged2;
14960
15018
  };
15019
+ var recordBlock = (input, decision) => {
15020
+ const paths = projectPaths(input);
15021
+ if (paths === void 0) return;
15022
+ logEvent(input.runtime, paths, "guard_block", {
15023
+ hook: input.event,
15024
+ tool: toolName2(input.payload),
15025
+ sessionId: input.payload.sessionId ?? null,
15026
+ claim: (decision.reason.split("\n")[0] ?? "").slice(0, 200)
15027
+ });
15028
+ };
14961
15029
  var shaped = (input, decision) => {
15030
+ recordBlock(input, decision);
14962
15031
  if (input.event !== "PreToolUse" || decision.output !== void 0) return decision;
14963
15032
  const reason = capReason(decision.reason);
14964
15033
  return {
@@ -15914,6 +15983,100 @@ var doctorCommand = {
15914
15983
  run: (ctx, args2) => Promise.resolve(runDoctor(ctx, args2.values["fix"] === true, args2.values["report"] === true))
15915
15984
  };
15916
15985
 
15986
+ // src/gain/gain.ts
15987
+ import { join as join39 } from "node:path";
15988
+ var GAIN_SCHEMA = "finitemonkey/gain@1";
15989
+ var DAY_MS2 = 864e5;
15990
+ var SAVE_WINDOW_MS = DAY_MS2;
15991
+ var readLog = (runtime, paths) => {
15992
+ const text8 = runtime.readText(paths.log);
15993
+ if (!text8.ok) return [];
15994
+ return text8.value.split("\n").flatMap((line2) => {
15995
+ const parsed = parseJsonObject(line2);
15996
+ if (!parsed.ok || !isRecord(parsed.value)) return [];
15997
+ const { at, event } = parsed.value;
15998
+ const time = typeof at === "string" ? Date.parse(at) : Number.NaN;
15999
+ return typeof event === "string" && Number.isFinite(time) ? [{ at: time, event }] : [];
16000
+ });
16001
+ };
16002
+ var readGates = (runtime, paths) => {
16003
+ const listed4 = runtime.listDir(toAbsolutePath(join39(paths.dir, "evidence")));
16004
+ const files = listed4.ok ? listed4.value.filter((name) => name.endsWith(".jsonl")) : [];
16005
+ return files.flatMap((name) => readLedger(runtime, paths.evidence(name.replace(/\.jsonl$/, "")))).filter((entry2) => entry2.check === GATE_CHECK);
16006
+ };
16007
+ var countsIn = (log, gates, from, to) => {
16008
+ const inWindow = (at) => at >= from && at < to;
16009
+ const lines3 = log.filter((line2) => inWindow(line2.at));
16010
+ const runs = gates.filter((entry2) => inWindow(Date.parse(entry2.at)));
16011
+ const passedAt = gates.filter((entry2) => entry2.status === "passed").map((entry2) => Date.parse(entry2.at));
16012
+ const blocks = lines3.filter((line2) => line2.event === "guard_block");
16013
+ return {
16014
+ blocks: blocks.length,
16015
+ saves: blocks.filter((block4) => passedAt.some((at) => at > block4.at && at - block4.at <= SAVE_WINDOW_MS)).length,
16016
+ releases: lines3.filter((line2) => line2.event === "person.allow").length,
16017
+ falsePositives: lines3.filter((line2) => line2.event === "person.allow").length + lines3.filter((line2) => line2.event === "guard_false_positive").length,
16018
+ gateRuns: runs.length,
16019
+ gateFailed: runs.filter((entry2) => entry2.status === "failed").length,
16020
+ gateUnverified: runs.filter((entry2) => entry2.status === "unverified").length,
16021
+ gateMs: runs.reduce((sum, entry2) => sum + (entry2.durationMs ?? 0), 0)
16022
+ };
16023
+ };
16024
+ var gainReport = (runtime, paths, days) => {
16025
+ const to = Date.parse(runtime.now());
16026
+ const from = to - days * DAY_MS2;
16027
+ const log = readLog(runtime, paths);
16028
+ const gates = readGates(runtime, paths);
16029
+ return {
16030
+ schema: GAIN_SCHEMA,
16031
+ days,
16032
+ from: new Date(from).toISOString(),
16033
+ to: new Date(to).toISOString(),
16034
+ current: countsIn(log, gates, from, to + 1),
16035
+ previous: countsIn(log, gates, from - days * DAY_MS2, from)
16036
+ };
16037
+ };
16038
+
16039
+ // src/commands/gain.ts
16040
+ var DEFAULT_DAYS = 30;
16041
+ var trend = (ctx, now, before) => now === before ? "" : ` (${ctx.t(now > before ? "gain.up" : "gain.down", { delta: Math.abs(now - before) })})`;
16042
+ var ROWS = [
16043
+ { key: "gain.row.blocks", pick: (counts) => counts.blocks },
16044
+ { key: "gain.row.saves", pick: (counts) => counts.saves },
16045
+ { key: "gain.row.false_positives", pick: (counts) => counts.falsePositives },
16046
+ { key: "gain.row.gate_runs", pick: (counts) => counts.gateRuns },
16047
+ { key: "gain.row.gate_failed", pick: (counts) => counts.gateFailed },
16048
+ { key: "gain.row.gate_unverified", pick: (counts) => counts.gateUnverified }
16049
+ ];
16050
+ var linesOf = (ctx, report2) => [
16051
+ ctx.t("gain.title", { days: report2.days }),
16052
+ ...ROWS.map(
16053
+ ({ key, pick: pick4 }) => ` ${ctx.t(key, { count: pick4(report2.current) })}${trend(ctx, pick4(report2.current), pick4(report2.previous))}`
16054
+ ),
16055
+ ` ${ctx.t("gain.row.gate_time", { minutes: Math.round(report2.current.gateMs / 6e4) })}`,
16056
+ ctx.t("gain.footer")
16057
+ ];
16058
+ var runGain = (ctx, rawDays) => {
16059
+ const days = rawDays === void 0 ? DEFAULT_DAYS : Number.parseInt(rawDays, 10);
16060
+ if (!Number.isInteger(days) || days < 1 || days > 365) {
16061
+ return failureFrom(
16062
+ ctx.t,
16063
+ usageProblem("invalid_arguments", "error.invalid_arguments", "finitemonkey gain --days 30", {
16064
+ detail: "--days 1-365"
16065
+ })
16066
+ );
16067
+ }
16068
+ const report2 = gainReport(ctx.runtime, fmPaths(findRepoRoot(ctx.runtime, ctx.cwd) ?? ctx.cwd), days);
16069
+ return success(report2, linesOf(ctx, report2));
16070
+ };
16071
+ var gainCommand = {
16072
+ path: ["gain"],
16073
+ summaryKey: "command.gain.summary",
16074
+ usage: "finitemonkey gain [--days 1-365]",
16075
+ status: "ready",
16076
+ options: { days: { type: "string" } },
16077
+ run: (ctx, args2) => Promise.resolve(runGain(ctx, stringOption(args2.values, "days")))
16078
+ };
16079
+
15917
16080
  // src/gate/baseline-run.ts
15918
16081
  var runBaseline = (options) => {
15919
16082
  const ready = prepareGate({ ...options, noSkip: true });
@@ -16355,11 +16518,11 @@ var mergeVerifyCommand = {
16355
16518
  var REFLOG_LIMIT = 300;
16356
16519
  var LIST_LIMIT = 50;
16357
16520
  var SHA = /^[0-9a-f]{40,64}$/;
16358
- var linesOf = (text8) => text8.split("\n").filter((line2) => line2.trim() !== "");
16521
+ var linesOf2 = (text8) => text8.split("\n").filter((line2) => line2.trim() !== "");
16359
16522
  var reflogLines = (runtime, repo) => {
16360
16523
  const result = runtime.git(["reflog", "show", "--all", `-n${String(REFLOG_LIMIT)}`, "--format=%H %gD"], repo.root);
16361
16524
  if (!result.ok) return [];
16362
- return linesOf(result.stdout).flatMap((line2) => {
16525
+ return linesOf2(result.stdout).flatMap((line2) => {
16363
16526
  const [sha, ref] = line2.split(" ");
16364
16527
  return sha === void 0 || ref === void 0 || !SHA.test(sha) || ref.startsWith("refs/finitemonkey/") ? [] : [{ sha, ref }];
16365
16528
  });
@@ -16367,11 +16530,11 @@ var reflogLines = (runtime, repo) => {
16367
16530
  var unreachable = (runtime, repo, shas) => {
16368
16531
  if (shas.length === 0) return /* @__PURE__ */ new Set();
16369
16532
  const result = runtime.git(["rev-list", ...shas, "--not", "--branches", "--tags", "--remotes"], repo.root);
16370
- return new Set(result.ok ? linesOf(result.stdout) : []);
16533
+ return new Set(result.ok ? linesOf2(result.stdout) : []);
16371
16534
  };
16372
16535
  var fsckDangling = (runtime, repo) => {
16373
16536
  const result = runtime.git(["fsck", "--no-reflogs", "--no-progress", "--dangling"], repo.root);
16374
- const lines3 = linesOf(result.stdout);
16537
+ const lines3 = linesOf2(result.stdout);
16375
16538
  const pick4 = (kind) => lines3.flatMap((line2) => {
16376
16539
  const [label2, type, sha] = line2.split(" ");
16377
16540
  return label2 === "dangling" && type === kind && sha !== void 0 ? [sha] : [];
@@ -16382,7 +16545,7 @@ var describe = (runtime, repo, shas) => {
16382
16545
  if (shas.length === 0) return /* @__PURE__ */ new Map();
16383
16546
  const result = runtime.git(["log", "--no-walk=unsorted", "--format=%H%x09%cr%x09%s", ...shas], repo.root);
16384
16547
  return new Map(
16385
- linesOf(result.stdout).flatMap((line2) => {
16548
+ linesOf2(result.stdout).flatMap((line2) => {
16386
16549
  const [sha, when, ...subject2] = line2.split(" ");
16387
16550
  return sha === void 0 ? [] : [[sha, [when ?? "", subject2.join(" ")]]];
16388
16551
  })
@@ -16476,7 +16639,7 @@ var rescueCommand = {
16476
16639
  };
16477
16640
 
16478
16641
  // src/git/undo.ts
16479
- import { basename as basename3, dirname as dirname8, join as join39 } from "node:path";
16642
+ import { basename as basename3, dirname as dirname8, join as join40 } from "node:path";
16480
16643
  var INDEX_SUBJECT = "finitemonkey index:";
16481
16644
  var gitLine = (runtime, repo, args2) => {
16482
16645
  const result = runtime.git(args2, repo.root);
@@ -16519,7 +16682,7 @@ var findSnapshot = (views, wanted) => {
16519
16682
  };
16520
16683
  var stampOf2 = (iso) => iso.replace(/[:.]/g, "-");
16521
16684
  var worktreePath = (runtime, repo) => {
16522
- const base = join39(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
16685
+ const base = join40(dirname8(repo.root), `${basename3(repo.root)}-undo-${stampOf2(runtime.now())}`);
16523
16686
  return toAbsolutePath(runtime.stat(toAbsolutePath(base)) === void 0 ? base : `${base}-2`);
16524
16687
  };
16525
16688
  var intoWorktree = (runtime, repo, view2) => {
@@ -16754,7 +16917,7 @@ var hookCommand = {
16754
16917
  };
16755
16918
 
16756
16919
  // src/commands/i18n-check.ts
16757
- import { join as join40 } from "node:path";
16920
+ import { join as join41 } from "node:path";
16758
16921
 
16759
16922
  // src/i18n/message.ts
16760
16923
  var ARG = /^\s*([a-zA-Z][a-zA-Z0-9_]*)\s*(?:,\s*(plural|select)\s*,)?/;
@@ -16980,7 +17143,7 @@ var unreadable = (dir, file, detail) => ({
16980
17143
  exitCode: ExitCode.NotConfigured
16981
17144
  });
16982
17145
  var readCatalog = (ctx, dir, lang) => {
16983
- const file = toAbsolutePath(join40(dir, `${lang}.json`));
17146
+ const file = toAbsolutePath(join41(dir, `${lang}.json`));
16984
17147
  const text8 = ctx.runtime.readText(file);
16985
17148
  if (!text8.ok) return reject(unreadable(dir, file, text8.error));
16986
17149
  const parsed = parseJsonObject(text8.value);
@@ -17003,7 +17166,7 @@ var describeProblem = (ctx, problem4) => {
17003
17166
  return ` ${problem4.lang} ${problem4.key} ${kind}`;
17004
17167
  };
17005
17168
  var loadReview = (ctx, dir) => {
17006
- const text8 = ctx.runtime.readText(toAbsolutePath(join40(dir, "review.json")));
17169
+ const text8 = ctx.runtime.readText(toAbsolutePath(join41(dir, "review.json")));
17007
17170
  const parsed = text8.ok ? parseJsonObject(text8.value) : void 0;
17008
17171
  if (parsed?.ok !== true) return void 0;
17009
17172
  const statuses = (lang) => {
@@ -17044,7 +17207,7 @@ var i18nCheckCommand = {
17044
17207
  };
17045
17208
 
17046
17209
  // src/history/look-back.ts
17047
- import { join as join41 } from "node:path";
17210
+ import { join as join42 } from "node:path";
17048
17211
 
17049
17212
  // src/history/patterns.ts
17050
17213
  import { posix as posix5 } from "node:path";
@@ -17180,7 +17343,7 @@ var parseTranscript = (jsonl) => {
17180
17343
  // src/history/look-back.ts
17181
17344
  var SAMPLES = 3;
17182
17345
  var projectLogKey = (root) => root.replace(/[^A-Za-z0-9-]/g, "-");
17183
- var projectLogDir = (runtime, root, env) => join41(env["CLAUDE_CONFIG_DIR"] ?? join41(runtime.homeDir(), ".claude"), "projects", projectLogKey(root));
17346
+ var projectLogDir = (runtime, root, env) => join42(env["CLAUDE_CONFIG_DIR"] ?? join42(runtime.homeDir(), ".claude"), "projects", projectLogKey(root));
17184
17347
  var lookBack = (runtime, root, env) => {
17185
17348
  const dir = projectLogDir(runtime, root, env);
17186
17349
  const listed4 = runtime.listDir(toAbsolutePath(dir));
@@ -17201,7 +17364,7 @@ var lookBack = (runtime, root, env) => {
17201
17364
  };
17202
17365
  let turns = 0;
17203
17366
  for (const name of files) {
17204
- const text8 = runtime.readText(toAbsolutePath(join41(dir, name)));
17367
+ const text8 = runtime.readText(toAbsolutePath(join42(dir, name)));
17205
17368
  if (!text8.ok) continue;
17206
17369
  for (const turn of parseTranscript(text8.value)) {
17207
17370
  turns += 1;
@@ -17453,7 +17616,7 @@ var applyAllows = (findings, allows, decided) => {
17453
17616
 
17454
17617
  // src/look/node-capture.ts
17455
17618
  import { mkdirSync as mkdirSync3 } from "node:fs";
17456
- import { join as join42 } from "node:path";
17619
+ import { join as join43 } from "node:path";
17457
17620
 
17458
17621
  // src/look/measure.ts
17459
17622
  var measurePage = (doc) => {
@@ -17529,7 +17692,7 @@ var captureOne = async (browser, request, route, viewport) => {
17529
17692
  const context = await browser.newContext({ viewport: { width: viewport, height: HEIGHT }, reducedMotion: "reduce" });
17530
17693
  const page = await context.newPage();
17531
17694
  const events = watch(page);
17532
- const path = join42(request.dir, `${slugOf(route)}-${String(viewport)}.png`);
17695
+ const path = join43(request.dir, `${slugOf(route)}-${String(viewport)}.png`);
17533
17696
  const loadError = await loadAndShoot(page, `${request.baseUrl}${route}`, path, request.timeoutMs);
17534
17697
  const measure = loadError === void 0 ? parseMeasure(await page.evaluate(MEASURE_SCRIPT)) : void 0;
17535
17698
  await context.close();
@@ -17546,7 +17709,7 @@ var captureAll = async (browser, request) => {
17546
17709
 
17547
17710
  // src/look/node-playwright.ts
17548
17711
  import { createRequire } from "node:module";
17549
- import { join as join43 } from "node:path";
17712
+ import { join as join44 } from "node:path";
17550
17713
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
17551
17714
  var NAMES = ["playwright", "playwright-core"];
17552
17715
  var isPlaywright = (value) => isRecord(value) && isRecord(value["chromium"]) && typeof value["chromium"]["launch"] === "function";
@@ -17561,7 +17724,7 @@ var tryLoad = async (base, name) => {
17561
17724
  }
17562
17725
  };
17563
17726
  var loadPlaywright = async (root) => {
17564
- for (const base of [join43(root, "package.json"), fileURLToPath3(import.meta.url)]) {
17727
+ for (const base of [join44(root, "package.json"), fileURLToPath3(import.meta.url)]) {
17565
17728
  for (const name of NAMES) {
17566
17729
  const found = await tryLoad(base, name);
17567
17730
  if (found !== void 0) return found;
@@ -17582,7 +17745,7 @@ var launchBrowser = async (playwright) => {
17582
17745
 
17583
17746
  // src/look/node-sheet.ts
17584
17747
  import { readFileSync as readFileSync2 } from "node:fs";
17585
- import { join as join44 } from "node:path";
17748
+ import { join as join45 } from "node:path";
17586
17749
  var escapeHtml = (text8) => text8.replace(/[&<>"]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[char] ?? char);
17587
17750
  var card = (result) => {
17588
17751
  const image = (() => {
@@ -17605,7 +17768,7 @@ var contactSheet = async (browser, results, dir) => {
17605
17768
  if (shown3.length === 0) return void 0;
17606
17769
  const page = await browser.newPage({ viewport: { width: 1600, height: 900 } });
17607
17770
  await page.setContent(`<!doctype html><style>${STYLE}</style>${shown3.map(card).join("")}`);
17608
- const path = join44(dir, "contact-sheet.png");
17771
+ const path = join45(dir, "contact-sheet.png");
17609
17772
  await page.screenshot({ path, fullPage: true });
17610
17773
  await page.close();
17611
17774
  return path;
@@ -17945,12 +18108,12 @@ var proseCheckCommand = {
17945
18108
  };
17946
18109
 
17947
18110
  // src/review/packet.ts
17948
- import { join as join45 } from "node:path";
18111
+ import { join as join46 } from "node:path";
17949
18112
  var REVIEW_STATE_FILE = ".finitemonkey/review/state.json";
17950
18113
  var FULL_ROUNDS = 3;
17951
18114
  var REVIEW_FILE_MAX_LINES = 200;
17952
18115
  var LEDGER_MAX_LINES = 100;
17953
- var statePath = (root) => toAbsolutePath(join45(root, REVIEW_STATE_FILE));
18116
+ var statePath = (root) => toAbsolutePath(join46(root, REVIEW_STATE_FILE));
17954
18117
  var readReviewState = (runtime, root) => {
17955
18118
  const raw = runtime.readText(statePath(root));
17956
18119
  const parsed = raw.ok ? parseJsonObject(raw.value) : void 0;
@@ -17972,7 +18135,7 @@ var taskSections = (runtime, root) => {
17972
18135
  const active = readState(runtime, paths).active;
17973
18136
  if (active === void 0)
17974
18137
  return [section("Contract", void 0, "(no active task)"), section("Evidence ledger", void 0, "(none)")];
17975
- const contract = runtime.readText(toAbsolutePath(join45(root, taskRelative(active))));
18138
+ const contract = runtime.readText(toAbsolutePath(join46(root, taskRelative(active))));
17976
18139
  const ledger = runtime.readText(paths.evidence(active));
17977
18140
  return [
17978
18141
  section("Contract", contract.ok ? contract.value : void 0, "(contract file missing)"),
@@ -18002,7 +18165,7 @@ var buildPacket = (runtime, root, base, head) => {
18002
18165
  ...taskSections(runtime, root),
18003
18166
  ...owners === void 0 ? [] : [section("REVIEW.md (base branch)", owners, "")]
18004
18167
  ].join("\n");
18005
- runtime.mkdir(toAbsolutePath(join45(root, ".finitemonkey", "review")));
18168
+ runtime.mkdir(toAbsolutePath(join46(root, ".finitemonkey", "review")));
18006
18169
  runtime.writeAtomic(statePath(root), `${JSON.stringify(state, null, 2)}
18007
18170
  `);
18008
18171
  return { round: state.rounds, base, head, range, text: text8 };
@@ -18058,14 +18221,14 @@ var gitUnavailable = (detail) => ({
18058
18221
  });
18059
18222
 
18060
18223
  // src/commands/secrets-accept.ts
18061
- var DAY_MS2 = 864e5;
18224
+ var DAY_MS3 = 864e5;
18062
18225
  var daysOf = (values) => {
18063
18226
  const raw = stringOption(values, "days");
18064
18227
  if (raw === void 0) return EXCEPTION_MAX_DAYS;
18065
18228
  const days = Number.parseInt(raw, 10);
18066
18229
  return Number.isInteger(days) && days > 0 && days <= EXCEPTION_MAX_DAYS ? days : void 0;
18067
18230
  };
18068
- var plusDays = (iso, days) => new Date(Date.parse(iso) + days * DAY_MS2).toISOString();
18231
+ var plusDays = (iso, days) => new Date(Date.parse(iso) + days * DAY_MS3).toISOString();
18069
18232
  var failure3 = (ctx, problem4) => failureFrom(ctx.t, problem4);
18070
18233
  var inputProblem = (ctx, id, reason, days) => {
18071
18234
  const fix = "finitemonkey secrets accept <id> --reason <why> [--days 1-90] [--false-positive]";
@@ -18157,7 +18320,7 @@ var secretsAcceptCommand = {
18157
18320
  };
18158
18321
 
18159
18322
  // src/secrets/runbooks.ts
18160
- import { dirname as dirname9, join as join46 } from "node:path";
18323
+ import { dirname as dirname9, join as join47 } from "node:path";
18161
18324
  var PROVIDER_OF_RULE = {
18162
18325
  "aws-access-token": "aws",
18163
18326
  "aws-secret-access-key": "aws",
@@ -18179,7 +18342,7 @@ var RUNBOOKS_URL = "https://github.com/gabrielvieiraf/finitemonkey/blob/main/run
18179
18342
  var runbookProvider = (rule) => PROVIDER_OF_RULE[rule] ?? "generic";
18180
18343
  var runbookLink = (runtime, rule) => {
18181
18344
  const file = `${runbookProvider(rule)}.md`;
18182
- const local = toAbsolutePath(join46(dirname9(dirname9(runtime.selfPath)), "runbooks", file));
18345
+ const local = toAbsolutePath(join47(dirname9(dirname9(runtime.selfPath)), "runbooks", file));
18183
18346
  return runtime.stat(local)?.kind === "file" ? local : `${RUNBOOKS_URL}/${file}`;
18184
18347
  };
18185
18348
 
@@ -18739,7 +18902,7 @@ var statuslineCommand = {
18739
18902
  };
18740
18903
 
18741
18904
  // src/task/create.ts
18742
- import { join as join47 } from "node:path";
18905
+ import { join as join48 } from "node:path";
18743
18906
 
18744
18907
  // src/core/yaml/write.ts
18745
18908
  var PLAIN_SAFE = /^[A-Za-z_./][A-Za-z0-9_./ ()-]*$/;
@@ -18828,10 +18991,10 @@ var explicitChecks = (draft) => [
18828
18991
  var draftChecks = (runtime, paths, draft) => {
18829
18992
  const disk = {
18830
18993
  read: (relative11) => {
18831
- const text8 = runtime.readText(toAbsolutePath(join47(paths.root, relative11)));
18994
+ const text8 = runtime.readText(toAbsolutePath(join48(paths.root, relative11)));
18832
18995
  return text8.ok ? text8.value : void 0;
18833
18996
  },
18834
- exists: (relative11) => runtime.stat(toAbsolutePath(join47(paths.root, relative11))) !== void 0
18997
+ exists: (relative11) => runtime.stat(toAbsolutePath(join48(paths.root, relative11))) !== void 0
18835
18998
  };
18836
18999
  const detected = detectChecks(disk);
18837
19000
  const base = draft.runs.length > 0 || draft.tests !== void 0 ? explicitChecks(draft) : detected;
@@ -18846,7 +19009,7 @@ var draftChecks = (runtime, paths, draft) => {
18846
19009
  { id: "merge", kind: "merge" },
18847
19010
  { id: "callers", kind: "callers" },
18848
19011
  ...draft.paths.length > 0 || draft.budget !== void 0 ? [{ id: "diff", kind: "diff" }] : [],
18849
- ...runtime.stat(toAbsolutePath(join47(paths.root, LOOK_CONFIG))) === void 0 ? [] : [{ id: "look", kind: "look" }],
19012
+ ...runtime.stat(toAbsolutePath(join48(paths.root, LOOK_CONFIG))) === void 0 ? [] : [{ id: "look", kind: "look" }],
18850
19013
  ...draft.git.length > 0 ? [{ id: "git", kind: "git", require: draft.git }] : [],
18851
19014
  { id: "pr", kind: "pr", requireOpen: draft.git.includes("pr_open") },
18852
19015
  ...draft.manual.map((description, index) => ({
@@ -19126,7 +19289,7 @@ var dataOf3 = (root, plan, applied) => ({
19126
19289
  kept: plan.items.filter((item2) => item2.action.startsWith("keep")).map(({ path }) => path),
19127
19290
  outside: OUTSIDE.map(({ command }) => command)
19128
19291
  });
19129
- var linesOf2 = (ctx, data) => [
19292
+ var linesOf3 = (ctx, data) => [
19130
19293
  ctx.t("install.uninstall.title", { root: data.root }),
19131
19294
  ...data.actions.map(({ path, action }) => ` ${action.padEnd(15)}${path}`),
19132
19295
  ...data.purge ? [ctx.t("install.uninstall.purge")] : [ctx.t("install.uninstall.keeps_state")],
@@ -19147,16 +19310,16 @@ var runUninstall = (ctx, args2) => {
19147
19310
  const options = { purge: args2.values["purge"] === true, keepGenerated: args2.values["keep-generated"] === true };
19148
19311
  const plan = planRemoval(project, lock.value, options);
19149
19312
  const preview = dataOf3(project.root, plan, false);
19150
- if (ctx.dryRun) return success(preview, [...linesOf2(ctx, preview), ctx.t("install.dry_run")]);
19313
+ if (ctx.dryRun) return success(preview, [...linesOf3(ctx, preview), ctx.t("install.dry_run")]);
19151
19314
  if (!ctx.yes) {
19152
19315
  const next2 = fixFor(args2);
19153
19316
  const problem4 = confirmProblem("uninstall", ctx.t("install.uninstall.question"), next2);
19154
- return { ...failureFrom(ctx.t, { ...problem4, data: preview }, linesOf2(ctx, preview)), next: next2 };
19317
+ return { ...failureFrom(ctx.t, { ...problem4, data: preview }, linesOf3(ctx, preview)), next: next2 };
19155
19318
  }
19156
19319
  const removed = applyRemoval(project, plan);
19157
19320
  if (!removed.ok) return failureFrom(ctx.t, applyProblem(removed.error));
19158
19321
  const data = dataOf3(project.root, plan, true);
19159
- return success(data, [...linesOf2(ctx, data), ctx.t("install.uninstall.done")]);
19322
+ return success(data, [...linesOf3(ctx, data), ctx.t("install.uninstall.done")]);
19160
19323
  };
19161
19324
  var uninstallCommand = {
19162
19325
  path: ["uninstall"],
@@ -19236,6 +19399,7 @@ var REGISTRY = [
19236
19399
  policyFalsePositiveCommand,
19237
19400
  offCommand,
19238
19401
  onCommand,
19402
+ gainCommand,
19239
19403
  secretsIncidentCommand,
19240
19404
  secretsLiveCommand,
19241
19405
  proseCheckCommand,
@@ -19255,7 +19419,7 @@ var REGISTRY = [
19255
19419
  ];
19256
19420
 
19257
19421
  // src/i18n/lang-settings.ts
19258
- import { join as join48 } from "node:path";
19422
+ import { join as join49 } from "node:path";
19259
19423
  var read3 = (runtime, path) => {
19260
19424
  const text8 = runtime.readText(toAbsolutePath(path));
19261
19425
  return text8.ok ? text8.value : void 0;
@@ -19270,14 +19434,14 @@ var languageIn = (text8) => {
19270
19434
  const value = parsed.ok && isRecord(parsed.value) ? parsed.value["language"] : void 0;
19271
19435
  return typeof value === "string" && value.trim() !== "" ? value : void 0;
19272
19436
  };
19273
- var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join48(runtime.homeDir(), ".claude");
19437
+ var claudeDir = (runtime, env) => env["CLAUDE_CONFIG_DIR"] ?? join49(runtime.homeDir(), ".claude");
19274
19438
  var langSettings = (runtime, cwd, env, platform) => {
19275
19439
  const root = findGitRoot(cwd, runtime) ?? cwd;
19276
- const config = parseConfigYaml(read3(runtime, join48(root, CONFIG_FILE)) ?? "");
19440
+ const config = parseConfigYaml(read3(runtime, join49(root, CONFIG_FILE)) ?? "");
19277
19441
  const claudeFiles = [
19278
- join48(root, ".claude", "settings.local.json"),
19279
- join48(root, ".claude", "settings.json"),
19280
- join48(claudeDir(runtime, env), "settings.json")
19442
+ join49(root, ".claude", "settings.local.json"),
19443
+ join49(root, ".claude", "settings.json"),
19444
+ join49(claudeDir(runtime, env), "settings.json")
19281
19445
  ];
19282
19446
  return {
19283
19447
  config: configText(config, "lang"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finitemonkey",
3
- "version": "0.1.0-next.24",
3
+ "version": "0.1.0-next.25",
4
4
  "description": "Claude Code hooks that stop a coding agent from saying done before the tests pass: a machine-checked definition of done, plus guardrails for secrets, git safety and production. Pre-release.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",