etymd 0.17.0 → 0.19.1

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 (29) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +10 -5
  3. package/dist/{approve-VN4EDBR5.js → approve-J6JU7MBX.js} +3 -3
  4. package/dist/audit-5N2TU2X7.js +11 -0
  5. package/dist/{brief-7MWP35UI.js → brief-NHHILXIH.js} +3 -3
  6. package/dist/{chunk-YQZDYDAK.js → chunk-2DRA7S67.js} +1 -1
  7. package/dist/{chunk-446EZNAQ.js → chunk-6DJMKZGK.js} +83 -10
  8. package/dist/chunk-7N5JDXQG.js +109 -0
  9. package/dist/{chunk-ANFNXFRI.js → chunk-GFUDKTC5.js} +2 -2
  10. package/dist/{chunk-BUMOGHQA.js → chunk-KQFEKOON.js} +1 -1
  11. package/dist/{chunk-LVR2DPR7.js → chunk-M7XDP6FT.js} +30 -3
  12. package/dist/{chunk-PIJZUDDQ.js → chunk-PQISPOB2.js} +191 -130
  13. package/dist/{chunk-WKP7M2B3.js → chunk-UMKC3OWQ.js} +11 -6
  14. package/dist/{chunk-FNA4R5KT.js → chunk-WKP72Z3J.js} +59 -11
  15. package/dist/cli.js +16 -16
  16. package/dist/{doctor-R4QDCAN5.js → doctor-264Q5PLY.js} +6 -6
  17. package/dist/{fleet-WPYEVHVL.js → fleet-VT625Q2J.js} +22 -6
  18. package/dist/{gates-3HRGYCYY.js → gates-YUMSTKUG.js} +5 -4
  19. package/dist/{generate-FSJQFQPV.js → generate-62GQSCKH.js} +3 -2
  20. package/dist/index.d.ts +21 -3
  21. package/dist/index.js +379 -58
  22. package/dist/{init-6RULX5JZ.js → init-G4XXFXVD.js} +5 -4
  23. package/dist/{premise-GFWHSR3K.js → premise-RNMAKMVA.js} +9 -5
  24. package/dist/{propose-IYNOIWAO.js → propose-35JXMEPH.js} +6 -6
  25. package/dist/scan-F6OW4ANK.js +5 -0
  26. package/dist/{scan-LLWW4YOU.js → scan-PJH32H7C.js} +3 -3
  27. package/package.json +2 -2
  28. package/dist/audit-PHAK5HEW.js +0 -11
  29. package/dist/scan-5SC47FSM.js +0 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # etymd
2
2
 
3
+ ## 0.19.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 51ecbc9: Generated pre-push hooks shellcheck every commit in the pushed range, each materialised from
8
+ git's object store, instead of reading the working tree. A push whose tip is clean but whose
9
+ earlier commits carry a broken script is now refused; the working tree no longer substitutes
10
+ for the pushed bytes; a push that creates a branch checks every commit no remote already has;
11
+ and a commit that cannot be materialised refuses the push rather than passing as unread.
12
+ Regenerate existing hooks with `etymd gates --yes` to apply the correction.
13
+
14
+ ## 0.19.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 29c7b31: The Claude Code pointer check follows Claude Code 2.1.277, which reads `AGENTS.md` when a
19
+ directory has no `CLAUDE.md`. A repo with `AGENTS.md` alone now passes and `fleet add` registers
20
+ it; it is reported (`claude-pointer-missing`, tier gap) only when the installed Claude Code is
21
+ older than 2.1.277, and `fleet add` prints a note instead of refusing. A `CLAUDE.md` that exists
22
+ without importing `@AGENTS.md` stays a risk and a refusal, because Claude Code reads that file
23
+ instead on every version. The version is read from `claude --version`; `ETYMD_CLAUDE_VERSION`
24
+ pins it, and `none` means no Claude Code on the machine.
25
+
26
+ ### Patch Changes
27
+
28
+ - 3dd2683: Dependency advisories cleared inside the declared ranges. `vitest` 4.1.10 → 4.1.11 closes
29
+ GHSA-82fw-gwwq-j7x9 (moderate: path traversal / arbitrary file read through the `@vitest/mocker`
30
+ redirect mock), and `js-yaml` 4.3.1 → 4.3.2 with its transitive 3.15.1 → 3.15.2 closes
31
+ GHSA-2883-xcg3-v3hh (high: `maxTotalMergeKeys` does not limit CPU use for empty merge sources).
32
+ Both are dev-only — the test runner and the changeset tooling — so the published build is
33
+ byte-identical; the lockfile also picks up the range-satisfying refresh those two pull in.
34
+
35
+ One advisory stays open, and deliberately: GHSA-g7r4-m6w7-qqqr (low: `esbuild` allows an arbitrary
36
+ file read when its development server runs on Windows) is fixed in esbuild 0.28.1, while `tsup`
37
+ 8.5.1 — the newest release — declares `esbuild: ^0.27.0`, so no in-range version of esbuild carries
38
+ the fix. The only automatic fix on offer is a downgrade to esbuild 0.27.2, which gives up five
39
+ patch releases to dodge a dev-server path this repo never takes: the build runs tsup, never an
40
+ esbuild server. It clears once tsup widens its range.
41
+
42
+ - 7d4e81f: `core.hooksPath` is read the way git reads it: a repo-relative path, an absolute path and a `~`
43
+ path can all name one directory, and the directory is the fact. An absolute path to the tracked
44
+ `.githooks/` now scans as `githooks` (wired) instead of `custom`, and the hooks are read from the
45
+ directory git actually runs — the literal-string comparison had also joined the absolute path
46
+ onto the repo root and looked for hooks at a path that does not exist, so a working pre-push gate
47
+ could read as absent. The fact is recorded repo-relative whenever the directory sits inside the
48
+ worktree, so a committed baseline never carries a machine path; a hooks directory outside the
49
+ repo stays absolute and is disclosed as `custom`.
50
+
51
+ ## 0.18.0
52
+
53
+ ### Minor Changes
54
+
55
+ - 41b91f6: Generated pre-push hooks preserve long filenames, whitespace, quotes, and leading
56
+ hyphens during shell-script discovery and checking, and a name carrying shell metacharacters
57
+ reaches the scan as data, never as code. Discovery fails closed instead of reporting success
58
+ with incomplete coverage: failed enumeration blocks, and a tracked regular file that exists
59
+ but cannot be read blocks, naming the file. Tracked paths with nothing readable behind them —
60
+ submodule entries, dangling symlinks, files deleted from the worktree while still tracked —
61
+ are counted and disclosed as skipped rather than blocking, so a submodule can no longer wedge
62
+ every push. Shebang reads are bounded to the first 4 KiB of the first line, so a large binary
63
+ cannot be copied into the gate's scratch file on every push and a shebang embedded deeper in
64
+ a document no longer drags the whole document into the checker. Regenerate existing hooks
65
+ with `etymd gates --yes` to apply the correction.
66
+ - 4a92435: The shebang classifier ships as a tracked, shebanged helper beside the hook
67
+ (`.githooks/discover-shell-scripts.sh`), so the scan it implements finds and checks it too —
68
+ the gate covers its own classifier, and the hook's most intricate code is lintable on its
69
+ own lines.
70
+
3
71
  ## 0.17.0
4
72
 
5
73
  ### Minor Changes
package/README.md CHANGED
@@ -174,7 +174,7 @@ personal and guarded repos. See [the fleet manifest](#the-fleet-manifest-experim
174
174
  | Command | What it does |
175
175
  | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
176
176
  | `etymd audit` | Verify every claim; ranked findings (risk → gap → polish) + ledger diff. `--lens`, `--truth`, `--json`, `--no-ledger`, `--fail-on <tier>`. |
177
- | `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**. Never overwrites. |
177
+ | `etymd init` | Onboard: approve the committed baseline; scaffold a minimal AGENTS.md **only if missing**, with its `CLAUDE.md` pointer. Never overwrites. |
178
178
  | `etymd doctor` | Alias for `audit --truth`. |
179
179
  | `etymd context` | The economy view: per-file always-loaded footprint + extraction candidates. |
180
180
  | `etymd gates` | Install local git-hook gates (pre-commit / commit-msg / pre-push, plus a publish screen where something ships) built from your own check scripts — and from the repo's shell surface, where it has one. |
@@ -188,7 +188,7 @@ personal and guarded repos. See [the fleet manifest](#the-fleet-manifest-experim
188
188
  | `etymd accept` | `accept <id>` — record a finding as accepted reality; visible in the ledger, out of the report. |
189
189
  | `etymd fleet` | Sweep every project in a fleet manifest: read-only per-repo audits + manifest/wall checks. `--manifest`, `--only`, `--profile`, `--truth`, `--persist-ledgers`, `--json`, `--fail-on`. |
190
190
  | `etymd fleet check` | Validate the manifest pair alone (no lenses): dangling mappings, duplicate names, privacy leaks, undeclared trust, machine paths. Non-zero exit on any finding. |
191
- | `etymd fleet add` | `add <dir>` — register a project: scans it, asks for what no scan can derive, and refuses to write an entry missing a mandatory field. `--name`, `--kind`, `--profile`, `--trust`, `-y`. |
191
+ | `etymd fleet add` | `add <dir>` — register a project: scans it, asks for what no scan can derive, and refuses to write an entry missing a mandatory field, or a repo whose `AGENTS.md` Claude Code cannot see. `--name`, `--kind`, `--profile`, `--trust`, `-y`. |
192
192
  | `etymd fleet board` | Render the fleet board: every project's `MILESTONES.md` (contract key `milestones`, shape-checked by the sweep) plus a ranked initiatives table on one page. `--initiatives <file>`, `--out <file>`, `--json`. |
193
193
  | `etymd propose` | Score the sweep's improvement findings + recurring classes against a fleet-authored rubric — stable `proposal/1` records, read-only, deterministic, guarded entries excluded. `--rubric <file>` (required), `--manifest <file>` or `--from <fleet.json>`, `--json`. |
194
194
  | `etymd fleet dismiss` / `accept` | `<name> <id>` — resolve a project's finding from any cwd; guarded findings persist beside the manifest, never in the guarded worktree. |
@@ -520,7 +520,11 @@ Two fields the scan can never derive, so the manifest must declare them:
520
520
 
521
521
  `etymd fleet add <dir>` is the gate that keeps both true: it scans the project, prompts for what
522
522
  no scan can derive, and **refuses to write an incomplete entry**. Non-interactive runs (`--yes`,
523
- CI) must pass every mandatory value as a flag — there is deliberately no default.
523
+ CI) must pass every mandatory value as a flag — there is deliberately no default. It also refuses
524
+ a repo whose `CLAUDE.md` hides its `AGENTS.md`: Claude Code reads `CLAUDE.md` when one exists and
525
+ falls back to `AGENTS.md` (from 2.1.277) only when none does, so a `CLAUDE.md` must import
526
+ `@AGENTS.md` or be a symlink to it — the refusal prints the fix. A repo with `AGENTS.md` alone
527
+ registers; on a Claude Code older than 2.1.277 it gets a note, since that version cannot see it.
524
528
 
525
529
  How the sweep behaves:
526
530
 
@@ -543,8 +547,9 @@ How the sweep behaves:
543
547
  - **Wall checks.** Guarded contract files found inside a guarded worktree, unregistered checkouts
544
548
  under the fleet root whose remotes match `guardedHosts`, tracked `/Users/` paths in the manifest
545
549
  repo, private **needles** — the identifiers the local file holds (labels, dir names, hosts) —
546
- inside `trust: "public-repo"` entries, and guarded-host commit emails on personal entries — each
547
- a risk finding; each check that cannot run is disclosed.
550
+ inside `trust: "public-repo"` entries, guarded-host commit emails on personal entries, and
551
+ repos whose `AGENTS.md` no `CLAUDE.md` pointer or symlink makes visible to Claude Code
552
+ (`claude-pointer-missing`) — each a risk finding; each check that cannot run is disclosed.
548
553
  - **No global pointer.** `--manifest` is required unless the cwd holds `registry.json` — there
549
554
  is deliberately no env var and no home-directory pointer.
550
555
  - **Milestones and the fleet board.** A project declares its plan in one file — `MILESTONES.md`,
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { section, theme, print, renderBaselineDrift, glyph } from './chunk-HI7NWPRA.js';
3
- import { scanProject } from './chunk-446EZNAQ.js';
4
- import { VERSION } from './chunk-BUMOGHQA.js';
3
+ import { scanProject } from './chunk-6DJMKZGK.js';
4
+ import { VERSION } from './chunk-KQFEKOON.js';
5
5
  import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-P6ATKV2R.js';
6
- import { PACK_VERSION } from './chunk-YQZDYDAK.js';
6
+ import { PACK_VERSION } from './chunk-2DRA7S67.js';
7
7
  import './chunk-4VPBP6K6.js';
8
8
 
9
9
  // src/commands/approve.ts
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ export { run } from './chunk-GFUDKTC5.js';
3
+ import './chunk-UMKC3OWQ.js';
4
+ import './chunk-WKP72Z3J.js';
5
+ import './chunk-3E2IPCRY.js';
6
+ import './chunk-HI7NWPRA.js';
7
+ import './chunk-6DJMKZGK.js';
8
+ import './chunk-KQFEKOON.js';
9
+ import './chunk-P6ATKV2R.js';
10
+ import './chunk-2DRA7S67.js';
11
+ import './chunk-4VPBP6K6.js';
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { print, theme } from './chunk-HI7NWPRA.js';
3
- import { scanProject } from './chunk-446EZNAQ.js';
4
- import './chunk-BUMOGHQA.js';
3
+ import { scanProject } from './chunk-6DJMKZGK.js';
4
+ import './chunk-KQFEKOON.js';
5
5
  import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
6
- import './chunk-YQZDYDAK.js';
6
+ import './chunk-2DRA7S67.js';
7
7
  import './chunk-4VPBP6K6.js';
8
8
  import { promises } from 'fs';
9
9
  import path from 'path';
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  // src/pack/version.ts
3
- var PACK_VERSION = "12";
3
+ var PACK_VERSION = "15";
4
4
 
5
5
  export { PACK_VERSION };
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-BUMOGHQA.js';
3
- import { PACK_VERSION } from './chunk-YQZDYDAK.js';
4
- import { normalizeRelPath, isDirectory, readJson, git, matchesAnyGlob, pathExists, readText } from './chunk-4VPBP6K6.js';
2
+ import { VERSION } from './chunk-KQFEKOON.js';
3
+ import { PACK_VERSION } from './chunk-2DRA7S67.js';
4
+ import { normalizeRelPath, isDirectory, pathExists, readText, readJson, git, matchesAnyGlob } from './chunk-4VPBP6K6.js';
5
5
  import path from 'path';
6
+ import { execFile } from 'child_process';
6
7
  import { promises } from 'fs';
8
+ import { promisify } from 'util';
7
9
 
8
10
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
9
11
  "node_modules",
@@ -265,10 +267,17 @@ async function hasLintStagedConfig(root, pkg) {
265
267
  }
266
268
  return false;
267
269
  }
270
+ function normalizeHooksPath(root, raw) {
271
+ const abs = path.resolve(root, raw);
272
+ const rel = path.relative(root, abs);
273
+ const outside = path.isAbsolute(rel) || rel === ".." || rel.startsWith(`..${path.sep}`);
274
+ return outside ? abs : normalizeRelPath(rel) || ".";
275
+ }
268
276
  async function detectHooks(root, hooksPath, pkg) {
269
277
  const lintStaged = await hasLintStagedConfig(root, pkg);
270
278
  if (hooksPath) {
271
- if (hooksPath.replace(/\/+$/, "") === ".husky/_") {
279
+ const dir = normalizeHooksPath(root, hooksPath);
280
+ if (dir === ".husky/_") {
272
281
  const base2 = path.join(root, ".husky");
273
282
  return {
274
283
  source: "husky",
@@ -279,10 +288,10 @@ async function detectHooks(root, hooksPath, pkg) {
279
288
  lintStaged
280
289
  };
281
290
  }
282
- const base = path.join(root, hooksPath);
291
+ const base = path.resolve(root, dir);
283
292
  return {
284
- source: hooksPath === ".githooks" ? "githooks" : "custom",
285
- dir: hooksPath,
293
+ source: dir === ".githooks" ? "githooks" : "custom",
294
+ dir,
286
295
  preCommit: await pathExists(path.join(base, "pre-commit")),
287
296
  prePush: await pathExists(path.join(base, "pre-push")),
288
297
  commitMsg: await pathExists(path.join(base, "commit-msg")),
@@ -438,6 +447,69 @@ async function detectArtifacts(root) {
438
447
  });
439
448
  return artifacts;
440
449
  }
450
+ var ROOT_POINTER_IMPORT_RE = /^\s*@(\.\/)?AGENTS\.md\s*$/m;
451
+ var DOT_CLAUDE_IMPORT_RE = /^\s*@\.\.\/AGENTS\.md\s*$/m;
452
+ var CLAUDE_AGENTS_FALLBACK_VERSION = "2.1.277";
453
+ var pExecFile = promisify(execFile);
454
+ var claudeVersionMemo;
455
+ function detectClaudeCodeVersion() {
456
+ const override = process.env.ETYMD_CLAUDE_VERSION;
457
+ if (override !== void 0) {
458
+ return Promise.resolve(override === "none" || override === "" ? null : override);
459
+ }
460
+ claudeVersionMemo ??= pExecFile("claude", ["--version"], { timeout: 5e3 }).then(({ stdout }) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? null).catch(() => null);
461
+ return claudeVersionMemo;
462
+ }
463
+ function versionBefore(a, b) {
464
+ const pa = a.split(".").map(Number);
465
+ const pb = b.split(".").map(Number);
466
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
467
+ const x = pa[i] ?? 0;
468
+ const y = pb[i] ?? 0;
469
+ if (x !== y) return x < y;
470
+ }
471
+ return false;
472
+ }
473
+ async function checkClaudePointer(root, claudeVersion) {
474
+ const agentsAbs = path.join(root, "AGENTS.md");
475
+ const claudeAbs = path.join(root, "CLAUDE.md");
476
+ if (!await pathExists(agentsAbs)) return { ok: true, via: "no-agents" };
477
+ const statOrNone = async (p) => {
478
+ try {
479
+ return await promises.stat(p);
480
+ } catch {
481
+ return null;
482
+ }
483
+ };
484
+ const [agentsSt, claudeSt] = await Promise.all([statOrNone(agentsAbs), statOrNone(claudeAbs)]);
485
+ if (agentsSt && claudeSt && agentsSt.ino !== 0 && agentsSt.ino === claudeSt.ino && agentsSt.dev === claudeSt.dev) {
486
+ return { ok: true, via: "same-file" };
487
+ }
488
+ const rootClaude = await readText(claudeAbs);
489
+ if (rootClaude !== null && ROOT_POINTER_IMPORT_RE.test(rootClaude)) {
490
+ return { ok: true, via: "root-import" };
491
+ }
492
+ const dotClaude = await readText(path.join(root, ".claude", "CLAUDE.md"));
493
+ if (dotClaude !== null && DOT_CLAUDE_IMPORT_RE.test(dotClaude)) {
494
+ return { ok: true, via: "dotclaude-import" };
495
+ }
496
+ if (rootClaude !== null || dotClaude !== null) {
497
+ return {
498
+ ok: false,
499
+ kind: "no-import",
500
+ detail: rootClaude !== null ? "AGENTS.md and CLAUDE.md both present, but CLAUDE.md does not import it" : "AGENTS.md and .claude/CLAUDE.md both present, but .claude/CLAUDE.md does not import it"
501
+ };
502
+ }
503
+ const version = claudeVersion === void 0 ? await detectClaudeCodeVersion() : claudeVersion;
504
+ if (version !== null && versionBefore(version, CLAUDE_AGENTS_FALLBACK_VERSION)) {
505
+ return {
506
+ ok: false,
507
+ kind: "old-reader",
508
+ detail: `AGENTS.md present, no CLAUDE.md, and Claude Code ${version} predates the AGENTS.md fallback (${CLAUDE_AGENTS_FALLBACK_VERSION})`
509
+ };
510
+ }
511
+ return { ok: true, via: "native-fallback" };
512
+ }
441
513
  async function walkTree(root) {
442
514
  const state = { budget: 2e4, truncated: false };
443
515
  const countFiles = async (dir) => {
@@ -558,11 +630,12 @@ async function scanProject(root, opts = {}) {
558
630
  const [branch, head, hooksPathRaw, authorsRaw, subjectsRaw] = isRepo ? await Promise.all([
559
631
  git(abs, ["rev-parse", "--abbrev-ref", "HEAD"]),
560
632
  git(abs, ["rev-parse", "--short", "HEAD"]),
561
- git(abs, ["config", "--get", "core.hooksPath"]),
633
+ // `--type=path` expands a leading `~` the way git itself does when it runs the hooks.
634
+ git(abs, ["config", "--type=path", "--get", "core.hooksPath"]),
562
635
  git(abs, ["log", "-200", "--format=%ae"]),
563
636
  git(abs, ["log", "-50", "--format=%s"])
564
637
  ]) : [null, null, null, null, null];
565
- const hooksPath = hooksPathRaw ?? void 0;
638
+ const hooksPath = hooksPathRaw ? normalizeHooksPath(abs, hooksPathRaw) : void 0;
566
639
  const hooks = await detectHooks(abs, hooksPath, rootPkg);
567
640
  const shell = await detectShellSurface(abs, isRepo);
568
641
  const freshness = await collectFreshness(abs, isRepo, artifacts, opts.upstreamRemote);
@@ -602,4 +675,4 @@ async function scanProject(root, opts = {}) {
602
675
  };
603
676
  }
604
677
 
605
- export { expandFileGlobs, scanProject };
678
+ export { CLAUDE_AGENTS_FALLBACK_VERSION, checkClaudePointer, expandFileGlobs, scanProject };
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import { isSafeGateCommand, generateAgentsMd, generateClaudePointerMd, isSelfBuildRepo, generatePreCommitHook, generateCommitMsgHook, generatePrePushHook, generateShellDiscoveryScript, generateArtifactCheckScript, fileOrigin } from './chunk-PQISPOB2.js';
3
+ import { DEFAULT_CONFIG } from './chunk-P6ATKV2R.js';
4
+ import { readText, pathExists } from './chunk-4VPBP6K6.js';
5
+ import path from 'path';
6
+
7
+ function derivedCommands(facts, existingHook) {
8
+ const c = facts.commands;
9
+ const base = [c.formatCheck, c.typecheck, c.lint].filter(
10
+ (k) => Boolean(k) && isSafeGateCommand(c.raw[k])
11
+ );
12
+ if (c.test && existingHook && new RegExp(`\\b${c.test}\\b`).test(existingHook)) {
13
+ base.push(c.test);
14
+ }
15
+ return base;
16
+ }
17
+ function riskReachability(facts) {
18
+ const reasons = [];
19
+ if (facts.publishRoute !== "none") {
20
+ reasons.push("an instruction file can name a package script that no longer exists");
21
+ }
22
+ if (facts.artifacts.some((a) => a.kind === "state" && a.exists)) {
23
+ reasons.push("a state doc can fall far enough behind the repo to escalate");
24
+ }
25
+ return reasons;
26
+ }
27
+ function deriveFailOn(facts, recorded) {
28
+ const reachable = riskReachability(facts);
29
+ if (recorded.explicit) return { failOn: recorded.failOn, source: "config", reachable };
30
+ if (recorded.failOn === "risk" && reachable.length === 0) {
31
+ return { failOn: "gap", source: "derived", reachable };
32
+ }
33
+ return { failOn: recorded.failOn, source: "default", reachable };
34
+ }
35
+ async function planWorkflow(root, facts, opts) {
36
+ const out = [];
37
+ const add = async (rel, contents, label, executable = false) => {
38
+ const abs = path.join(root, rel);
39
+ const exists = await pathExists(abs);
40
+ const existing = exists ? await readText(abs) : null;
41
+ const differs = exists ? existing !== contents : void 0;
42
+ const origin = differs && existing !== null ? fileOrigin(existing) : void 0;
43
+ out.push({
44
+ path: rel,
45
+ contents,
46
+ exists,
47
+ differs,
48
+ drift: origin === "pack" ? "stale" : origin,
49
+ executable,
50
+ label
51
+ });
52
+ };
53
+ if (opts.agents) {
54
+ await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
55
+ await add("CLAUDE.md", generateClaudePointerMd(), "Claude Code pointer to AGENTS.md");
56
+ }
57
+ if (opts.gates) {
58
+ const existingPrePush = await readText(path.join(root, ".githooks", "pre-push"));
59
+ const selfBuild = isSelfBuildRepo(facts);
60
+ await add(
61
+ ".githooks/pre-commit",
62
+ generatePreCommitHook(selfBuild),
63
+ "Process gate (pre-commit)",
64
+ true
65
+ );
66
+ await add(
67
+ ".githooks/commit-msg",
68
+ generateCommitMsgHook(opts.gateConfig),
69
+ "Message gate (content screen, format)",
70
+ true
71
+ );
72
+ const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
73
+ commands: derivedCommands(facts, existingPrePush ?? void 0),
74
+ failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
75
+ publishGate: opts.gateConfig?.publishGate,
76
+ commitFormat: opts.gateConfig?.commitFormat,
77
+ allowWriting: opts.gateConfig?.allowWriting ?? []
78
+ };
79
+ const tier = deriveFailOn(facts, {
80
+ failOn: gateConfig.failOn,
81
+ explicit: opts.gateFailOnPinned ?? false
82
+ });
83
+ await add(
84
+ ".githooks/pre-push",
85
+ generatePrePushHook(facts, { ...gateConfig, failOn: tier.failOn }, selfBuild),
86
+ "Correctness gate (pre-push)",
87
+ true
88
+ );
89
+ if (facts.shell?.scripts) {
90
+ await add(
91
+ ".githooks/discover-shell-scripts.sh",
92
+ generateShellDiscoveryScript(),
93
+ "Shell discovery (pre-push shellcheck helper)",
94
+ true
95
+ );
96
+ }
97
+ if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
98
+ await add(
99
+ "scripts/artifact-check.sh",
100
+ generateArtifactCheckScript(selfBuild),
101
+ "Content screen (published artifact)",
102
+ true
103
+ );
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+
109
+ export { deriveFailOn, derivedCommands, planWorkflow, riskReachability };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runAudit } from './chunk-WKP7M2B3.js';
3
- import { parseFailOnTier, meetsFailOn } from './chunk-FNA4R5KT.js';
2
+ import { runAudit } from './chunk-UMKC3OWQ.js';
3
+ import { parseFailOnTier, meetsFailOn } from './chunk-WKP72Z3J.js';
4
4
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-HI7NWPRA.js';
5
5
 
6
6
  // src/commands/audit.ts
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.17.0"};
4
+ version: "0.19.1"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { runAudit } from './chunk-WKP7M2B3.js';
2
+ import { runAudit } from './chunk-UMKC3OWQ.js';
3
3
  import { readLedger } from './chunk-3E2IPCRY.js';
4
+ import { checkClaudePointer, CLAUDE_AGENTS_FALLBACK_VERSION } from './chunk-6DJMKZGK.js';
4
5
  import { DEFAULT_CONFIG, ETYMD_DIR } from './chunk-P6ATKV2R.js';
5
6
  import { readText, isDirectory, git, pathExists } from './chunk-4VPBP6K6.js';
6
7
  import os from 'os';
@@ -1005,8 +1006,8 @@ async function checkGuardedEmails(manifest, findings, disclosures) {
1005
1006
  }
1006
1007
  }
1007
1008
  async function checkGateDrift(manifest, findings, disclosures) {
1008
- const { planWorkflow } = await import('./generate-FSJQFQPV.js');
1009
- const { scanProject } = await import('./scan-5SC47FSM.js');
1009
+ const { planWorkflow } = await import('./generate-62GQSCKH.js');
1010
+ const { scanProject } = await import('./scan-F6OW4ANK.js');
1010
1011
  const { readConfig } = await import('./config-724Y3IOB.js');
1011
1012
  for (const entry of manifest.entries) {
1012
1013
  const root = entry.resolvedRoot;
@@ -1066,6 +1067,31 @@ async function checkGateDrift(manifest, findings, disclosures) {
1066
1067
  }
1067
1068
  }
1068
1069
  }
1070
+ async function checkClaudePointers(manifest, findings) {
1071
+ for (const entry of manifest.entries) {
1072
+ const root = entry.resolvedRoot;
1073
+ if (!root || !await isDirectory(root)) continue;
1074
+ const check = await checkClaudePointer(root);
1075
+ if (check.ok) continue;
1076
+ findings.push(
1077
+ check.kind === "no-import" ? finding(
1078
+ `${FLEET_LENS}/claude-pointer-missing:${entry.name}`,
1079
+ "risk",
1080
+ `\`${entry.name}\` has a CLAUDE.md that never imports its AGENTS.md`,
1081
+ [`${entry.name}: ${check.detail}`],
1082
+ "Claude Code reads CLAUDE.md when one exists and falls back to AGENTS.md only when none does. A CLAUDE.md without the import shadows AGENTS.md, so the contract looks universal from inside the repo while Claude Code never receives it.",
1083
+ "Add a full-line `@AGENTS.md` import to that CLAUDE.md, symlink either file to the other, or delete the CLAUDE.md if AGENTS.md is the whole contract."
1084
+ ) : finding(
1085
+ `${FLEET_LENS}/claude-pointer-missing:${entry.name}`,
1086
+ "gap",
1087
+ `\`${entry.name}\` relies on the AGENTS.md fallback, which the installed Claude Code predates`,
1088
+ [`${entry.name}: ${check.detail}`],
1089
+ `Claude Code reads AGENTS.md on its own only from ${CLAUDE_AGENTS_FALLBACK_VERSION}; older releases load CLAUDE.md alone, so this repo's contract is invisible to them.`,
1090
+ "Update Claude Code, or create a CLAUDE.md beside AGENTS.md whose only import line is `@AGENTS.md`."
1091
+ )
1092
+ );
1093
+ }
1094
+ }
1069
1095
  async function collectWallFindings(manifest) {
1070
1096
  const findings = [];
1071
1097
  const disclosures = [];
@@ -1075,6 +1101,7 @@ async function collectWallFindings(manifest) {
1075
1101
  await checkHygieneNeedles(manifest, findings, disclosures);
1076
1102
  await checkGuardedEmails(manifest, findings, disclosures);
1077
1103
  await checkGateDrift(manifest, findings, disclosures);
1104
+ await checkClaudePointers(manifest, findings);
1078
1105
  return { findings, disclosures };
1079
1106
  }
1080
1107
  function stateAgeDays(facts) {