finitemonkey 0.1.0-next.7 → 0.1.0-next.9

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 +129 -24
  2. package/package.json +1 -1
@@ -4441,11 +4441,11 @@ var requirement2 = (want, ctx) => {
4441
4441
  };
4442
4442
  var gitCheck = (check2, ctx) => {
4443
4443
  const wants = check2.require ?? ["committed"];
4444
- const missing = wants.flatMap((want) => {
4444
+ const missing2 = wants.flatMap((want) => {
4445
4445
  const problem4 = requirement2(want, ctx);
4446
4446
  return problem4 === void 0 ? [] : [problem4];
4447
4447
  });
4448
- return missing.length > 0 ? failed(missing) : passed([note("gate.git.ok", { require: wants.join(", ") })]);
4448
+ return missing2.length > 0 ? failed(missing2) : passed([note("gate.git.ok", { require: wants.join(", ") })]);
4449
4449
  };
4450
4450
 
4451
4451
  // src/guards/release/grants.ts
@@ -4611,6 +4611,7 @@ var LOCK_FILE = ".finitemonkey/lock.json";
4611
4611
  var BACKUPS_DIR = ".finitemonkey/backups";
4612
4612
  var LOCKS_DIR = ".finitemonkey/locks";
4613
4613
  var INSTALL_LOCK = ".finitemonkey/locks/install.lock";
4614
+ var VERIFY_SKILL = ".claude/skills/verify/SKILL.md";
4614
4615
  var LOCAL_BIN = "node_modules/finitemonkey/dist/finitemonkey.mjs";
4615
4616
  var VENDOR_BIN = ".finitemonkey/runtime/dist/finitemonkey.mjs";
4616
4617
  var VENDOR_PACKAGE = ".finitemonkey/runtime/package.json";
@@ -6506,10 +6507,10 @@ var symbolFindings = (context, pair, path, text6) => {
6506
6507
  var verifyFile = (context, pair) => {
6507
6508
  const path = resultPathOf(context, pair);
6508
6509
  if (pair.ours?.binary === true || pair.theirs?.binary === true) return binaryFindings(context, pair, path);
6509
- const missing = ["ours", "theirs"].flatMap(
6510
+ const missing2 = ["ours", "theirs"].flatMap(
6510
6511
  (side) => created2(pair[side]) && path === void 0 ? [finding("missing-file", pair.key, side, pair.key)] : []
6511
6512
  );
6512
- if (missing.length > 0) return missing;
6513
+ if (missing2.length > 0) return missing2;
6513
6514
  const copied = [pair.ours, pair.theirs].find((diff) => diff?.status === "copied");
6514
6515
  const baseSource = copied?.oldPath ?? (created2(pair.ours) || created2(pair.theirs) ? void 0 : pair.key);
6515
6516
  const baseText = baseSource === void 0 ? void 0 : blobText(context.runtime, context.repo, context.inputs.base, baseSource);
@@ -10816,34 +10817,49 @@ var mergeHooks = (hooks, isOwn, wanted) => {
10816
10817
  var withHooks = (settings, hooks) => {
10817
10818
  return setKey(settings, "hooks", Object.keys(hooks).length === 0 ? void 0 : hooks);
10818
10819
  };
10819
- var withPermissions = (settings, deny2) => {
10820
+ var BLOCK_READS = "blockReadsOutsideWorkingDirectories";
10821
+ var withPermissions = (settings, lists) => {
10820
10822
  const permissions = asRecord(settings["permissions"]);
10821
- const next2 = setKey(permissions, "deny", deny2.length === 0 ? void 0 : deny2);
10823
+ const withDeny = setKey(permissions, "deny", lists.deny.length === 0 ? void 0 : lists.deny);
10824
+ const withAsk = setKey(withDeny, "ask", lists.ask.length === 0 ? void 0 : lists.ask);
10825
+ const next2 = setKey(withAsk, BLOCK_READS, lists.blockReads);
10822
10826
  return setKey(settings, "permissions", Object.keys(next2).length === 0 ? void 0 : next2);
10823
10827
  };
10828
+ var missing = (wanted, existing) => wanted.filter((rule) => !existing.includes(rule));
10829
+ var withoutAdded = (existing, added) => existing.filter((rule) => typeof rule !== "string" || !added.includes(rule));
10824
10830
  var ownHookMatcher = (recorded) => (handler) => isOwnHandler(handler) || recorded.some((own) => JSON.stringify(own) === JSON.stringify(handler));
10825
10831
  var mergeSettings = (current, wanted, recorded) => {
10826
10832
  const schema = Object.keys(current).length === 0;
10827
10833
  const base = schema ? { $schema: SETTINGS_SCHEMA_URL } : current;
10828
10834
  const hooks = mergeHooks(asRecord(base["hooks"]), ownHookMatcher(recorded), wanted.hooks);
10829
- const existingDeny = asList(asRecord(base["permissions"])["deny"]);
10830
- const deny2 = wanted.deny.filter((rule) => !existingDeny.includes(rule));
10835
+ const permissions = asRecord(base["permissions"]);
10836
+ const existingDeny = asList(permissions["deny"]);
10837
+ const existingAsk = asList(permissions["ask"]);
10838
+ const deny2 = missing(wanted.deny, existingDeny);
10839
+ const ask = missing(wanted.ask, existingAsk);
10840
+ const blockReads = !(BLOCK_READS in permissions);
10831
10841
  const attribution = wanted.attributionOff && !("attribution" in base);
10832
10842
  const marketplace = wanted.marketplace && !hasEntry(base, "extraKnownMarketplaces", MARKETPLACE_KEY);
10833
10843
  const plugin = wanted.marketplace && !hasEntry(base, "enabledPlugins", PLUGIN_ID);
10834
- let settings = withPermissions(withHooks(base, hooks), [...existingDeny, ...deny2]);
10844
+ let settings = withPermissions(withHooks(base, hooks), {
10845
+ deny: [...existingDeny, ...deny2],
10846
+ ask: [...existingAsk, ...ask],
10847
+ blockReads: blockReads ? true : permissions[BLOCK_READS]
10848
+ });
10835
10849
  if (attribution) settings = { ...settings, attribution: ATTRIBUTION_OFF };
10836
10850
  if (marketplace) settings = withEntry(settings, "extraKnownMarketplaces", MARKETPLACE_KEY, MARKETPLACE_ENTRY);
10837
10851
  if (plugin) settings = withEntry(settings, "enabledPlugins", PLUGIN_ID, true);
10838
- return { settings, added: { schema, deny: deny2, attribution, marketplace, plugin } };
10852
+ return { settings, added: { schema, deny: deny2, ask, blockReads, attribution, marketplace, plugin } };
10839
10853
  };
10840
10854
  var sameAttribution = (value) => JSON.stringify(value) === JSON.stringify(ATTRIBUTION_OFF);
10841
10855
  var unmergeSettings = (current, added, recorded) => {
10842
10856
  const hooks = mergeHooks(asRecord(current["hooks"]), ownHookMatcher(recorded), {});
10843
- const deny2 = asList(asRecord(current["permissions"])["deny"]).filter(
10844
- (rule) => typeof rule !== "string" || !added.deny.includes(rule)
10845
- );
10846
- let settings = withPermissions(withHooks(current, hooks), deny2);
10857
+ const permissions = asRecord(current["permissions"]);
10858
+ let settings = withPermissions(withHooks(current, hooks), {
10859
+ deny: withoutAdded(asList(permissions["deny"]), added.deny),
10860
+ ask: withoutAdded(asList(permissions["ask"]), added.ask),
10861
+ blockReads: added.blockReads && permissions[BLOCK_READS] === true ? void 0 : permissions[BLOCK_READS]
10862
+ });
10847
10863
  if (added.attribution && sameAttribution(settings["attribution"])) {
10848
10864
  settings = setKey(settings, "attribution", void 0);
10849
10865
  }
@@ -11049,9 +11065,11 @@ var parseAdded = (value) => {
11049
11065
  const deny2 = strings2(value["deny"]);
11050
11066
  const { schema, attribution } = value;
11051
11067
  if (deny2 === void 0 || typeof schema !== "boolean" || typeof attribution !== "boolean") return void 0;
11068
+ const ask = strings2(value["ask"]) ?? [];
11069
+ const blockReads = value["blockReads"] === true;
11052
11070
  const marketplace = value["marketplace"] === true;
11053
11071
  const plugin = value["plugin"] === true;
11054
- return { schema, attribution, deny: deny2, marketplace, plugin };
11072
+ return { schema, attribution, deny: deny2, ask, blockReads, marketplace, plugin };
11055
11073
  };
11056
11074
  var parseEntry2 = (value) => {
11057
11075
  if (!isRecord(value)) return void 0;
@@ -11098,6 +11116,42 @@ var parseLock = (text6) => {
11098
11116
  var renderLock = (lock) => `${JSON.stringify(lock, null, 2)}
11099
11117
  `;
11100
11118
 
11119
+ // src/install/ask-floor.ts
11120
+ var DEFAULT_BRANCHES = ["main", "master"];
11121
+ var pushTo = (branch) => [
11122
+ `Bash(git push * ${branch})`,
11123
+ `Bash(git push * ${branch} *)`,
11124
+ `Bash(git push *:${branch})`,
11125
+ `Bash(git push *:${branch} *)`
11126
+ ];
11127
+ var ASK_FLOOR = {
11128
+ push: DEFAULT_BRANCHES.flatMap(pushTo),
11129
+ deploy: [
11130
+ "Bash(npm publish *)",
11131
+ "Bash(pnpm publish *)",
11132
+ "Bash(yarn publish *)",
11133
+ "Bash(yarn npm publish *)",
11134
+ "Bash(bun publish *)",
11135
+ "Bash(vercel * --prod*)",
11136
+ "Bash(vercel --prod*)",
11137
+ "Bash(netlify deploy * --prod*)",
11138
+ "Bash(fly deploy *)",
11139
+ "Bash(flyctl deploy *)",
11140
+ "Bash(firebase deploy *)",
11141
+ "Bash(wrangler deploy *)",
11142
+ "Bash(serverless deploy *)",
11143
+ "Bash(sls deploy *)",
11144
+ "Bash(cdk deploy *)",
11145
+ "Bash(terraform apply *)",
11146
+ "Bash(tofu apply *)",
11147
+ "Bash(pulumi up *)",
11148
+ "Bash(kubectl apply *)",
11149
+ "Bash(helm install *)",
11150
+ "Bash(helm upgrade *)"
11151
+ ]
11152
+ };
11153
+ var ASK_RULES = [...ASK_FLOOR.push, ...ASK_FLOOR.deploy];
11154
+
11101
11155
  // src/install/claude-version.ts
11102
11156
  var ON_FAILURE_MIN = [2, 1, 295];
11103
11157
  var PROBE_MS = 8e3;
@@ -11119,6 +11173,23 @@ var detectClaude = (probe) => {
11119
11173
  return { version: parsed.join("."), failClosed: atLeast(parsed, ON_FAILURE_MIN) };
11120
11174
  };
11121
11175
 
11176
+ // src/install/claudeignore.ts
11177
+ var CLAUDEIGNORE_FILES = [".claudeignore", ".claude/.claudeignore"];
11178
+ var ruleFor = (raw) => {
11179
+ const line2 = withoutCarriageReturn(raw).trim();
11180
+ if (line2 === "" || line2.startsWith("#") || line2.startsWith("!") || line2.includes(")")) return void 0;
11181
+ const anchored = line2.startsWith("/");
11182
+ const body = (anchored ? line2.slice(1) : line2).replace(/\/+$/, "");
11183
+ if (body === "" || body === "*" || body === "**") return void 0;
11184
+ const path = line2.endsWith("/") ? `${body}/**` : body;
11185
+ return `Read(${anchored || body.includes("/") ? "./" : ""}${path})`;
11186
+ };
11187
+ var claudeignoreRules = (texts) => [
11188
+ ...new Set(
11189
+ texts.flatMap((text6) => (text6 ?? "").split("\n")).map(ruleFor).filter((rule) => rule !== void 0)
11190
+ )
11191
+ ];
11192
+
11122
11193
  // src/install/config-template.ts
11123
11194
  var configTemplate = (t, preset, lang) => [
11124
11195
  `# ${GENERATED_MARKER}`,
@@ -11277,6 +11348,33 @@ var withoutBlock = (text6) => {
11277
11348
  return [...lines3.slice(0, range.start), ...lines3.slice(range.end + 1)].join(eol);
11278
11349
  };
11279
11350
 
11351
+ // src/install/skill-template.ts
11352
+ var gateCommand2 = (runner) => {
11353
+ const args = "gate --fast --json";
11354
+ if (runner.kind === "local") return `node ${LOCAL_BIN} ${args}`;
11355
+ if (runner.kind === "vendored") return `node ${VENDOR_BIN} ${args}`;
11356
+ return isScript(runner.path) ? `node "${runner.path}" ${args}` : `"${runner.path}" ${args}`;
11357
+ };
11358
+ var verifySkillTemplate = (runner) => [
11359
+ "---",
11360
+ "name: verify",
11361
+ "description: >-",
11362
+ " Use before every commit and before telling the person something works. Runs the fast gate through Finite Monkey so the result is recorded as evidence for the current tree instead of being taken on your word.",
11363
+ "when_to_use: >-",
11364
+ " Trigger phrases include commit, run the tests, check it works, roda os testes, confere, verifica, prueba.",
11365
+ "---",
11366
+ "",
11367
+ "# Verify: evidence, not claims",
11368
+ "",
11369
+ `1. From the project root, run \`${gateCommand2(runner)}\`.`,
11370
+ "2. Read `data`; for every failing item, fix the cause, never the check.",
11371
+ "3. Quote the command and its exit code when you report a result. A result from before your last edit no longer counts.",
11372
+ "",
11373
+ `<!-- ${GENERATED_MARKER} -->`,
11374
+ ""
11375
+ ].join("\n");
11376
+ var isOwnSkill = hasMarker;
11377
+
11280
11378
  // src/install/vendor.ts
11281
11379
  var isBundle = (text6) => text6.startsWith("#!/usr/bin/env node") && text6.includes("finitemonkey");
11282
11380
  var isOwnPackage = (text6) => {
@@ -11318,6 +11416,8 @@ var joinAdded = (previous, next2) => previous === void 0 ? next2 : {
11318
11416
  schema: previous.schema || next2.schema,
11319
11417
  attribution: previous.attribution || next2.attribution,
11320
11418
  deny: union(previous.deny, next2.deny),
11419
+ ask: union(previous.ask, next2.ask),
11420
+ blockReads: previous.blockReads || next2.blockReads,
11321
11421
  marketplace: previous.marketplace || next2.marketplace,
11322
11422
  plugin: previous.plugin || next2.plugin
11323
11423
  };
@@ -11331,7 +11431,8 @@ var settingsItem = (project, detection, input, previous) => {
11331
11431
  const hooks = wantedHooks(detection);
11332
11432
  const wanted = {
11333
11433
  hooks,
11334
- deny: DENY_RULES,
11434
+ deny: [...DENY_RULES, ...claudeignoreRules(CLAUDEIGNORE_FILES.map((path) => project.read(path)))],
11435
+ ask: ASK_RULES,
11335
11436
  attributionOff: input.preset === "gabriel",
11336
11437
  marketplace: input.marketplace
11337
11438
  };
@@ -11365,18 +11466,21 @@ var generatedItem = (project, file, previous) => {
11365
11466
  }
11366
11467
  return { ...base, action: actionFor(before, template, "update"), after: template };
11367
11468
  };
11368
- var generatedFiles = (input, vendor) => {
11369
- const config = { path: CONFIG_FILE, template: configTemplate(input.t, input.preset, input.lang), isOurs: hasMarker };
11370
- if (!vendor) return accept([config]);
11469
+ var generatedFiles = (input, runner, vendor) => {
11470
+ const own = [
11471
+ { path: CONFIG_FILE, template: configTemplate(input.t, input.preset, input.lang), isOurs: hasMarker },
11472
+ { path: VERIFY_SKILL, template: verifySkillTemplate(runner), isOurs: isOwnSkill }
11473
+ ];
11474
+ if (!vendor) return accept(own);
11371
11475
  if (input.bundle === void 0) return reject({ kind: "bundle", detail: "the running finitemonkey is not a bundle" });
11372
- return accept([config, ...vendoredFiles(input.bundle, input.version)]);
11476
+ return accept([...own, ...vendoredFiles(input.bundle, input.version)]);
11373
11477
  };
11374
11478
  var entryFor = (lock, path) => lock?.entries.find((entry2) => entry2.path === path);
11375
11479
  var buildPlan = (project, detection, input, previous) => {
11376
11480
  const settings = settingsItem(project, detection, input, entryFor(previous, SETTINGS_FILE));
11377
11481
  if (!settings.ok) return settings;
11378
11482
  const vendor = detection.runner.kind === "vendored" || entryFor(previous, VENDOR_BIN) !== void 0;
11379
- const files = generatedFiles(input, vendor);
11483
+ const files = generatedFiles(input, detection.runner, vendor);
11380
11484
  if (!files.ok) return files;
11381
11485
  const generated = files.value.map((file) => generatedItem(project, file, entryFor(previous, file.path)));
11382
11486
  return accept({
@@ -11433,7 +11537,7 @@ var nextLock = (plan, version, now, backupDir, createdDirs) => ({
11433
11537
  preset: plan.preset,
11434
11538
  installedAt: plan.previous?.installedAt ?? now,
11435
11539
  updatedAt: now,
11436
- createdDirs: plan.previous?.createdDirs ?? createdDirs,
11540
+ createdDirs: [.../* @__PURE__ */ new Set([...plan.previous?.createdDirs ?? [], ...createdDirs])],
11437
11541
  entries: plan.items.filter((item) => item.action !== "skip-foreign").map((item) => entryOf2(plan, item, backupDir))
11438
11542
  });
11439
11543
  var needsBackup = (plan, item) => item.before !== void 0 && plan.previous?.entries.find((entry2) => entry2.path === item.path)?.backup === void 0;
@@ -11449,7 +11553,8 @@ var writeAll = (project, items) => {
11449
11553
  }
11450
11554
  return accept(void 0);
11451
11555
  };
11452
- var createdBy = (project) => [".claude", STATE_DIR].filter((dir) => !project.exists(dir));
11556
+ var OWN_DIRS = [dirname6(dirname6(VERIFY_SKILL)), dirname6(VERIFY_SKILL), ".claude", STATE_DIR];
11557
+ var createdBy = (project) => OWN_DIRS.filter((dir) => !project.exists(dir));
11453
11558
  var commit = (project, plan, version, createdDirs) => {
11454
11559
  const now = project.runtime.now();
11455
11560
  const changing = plan.items.filter((item) => CHANGING.includes(item.action));
@@ -12539,7 +12644,7 @@ var removalFor = (project, entry2, options) => {
12539
12644
  var planRemoval = (project, lock, options) => ({
12540
12645
  items: lock.entries.map((entry2) => removalFor(project, entry2, options)),
12541
12646
  purge: options.purge,
12542
- removeDirs: lock.createdDirs.filter((dir) => dir !== STATE_DIR)
12647
+ removeDirs: lock.createdDirs.filter((dir) => dir !== STATE_DIR).toSorted((a, b) => b.split("/").length - a.split("/").length)
12543
12648
  });
12544
12649
  var applyItem = (project, item) => {
12545
12650
  if (item.action === "delete") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finitemonkey",
3
- "version": "0.1.0-next.7",
3
+ "version": "0.1.0-next.9",
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",