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.
- package/CHANGELOG.md +68 -0
- package/README.md +10 -5
- package/dist/{approve-VN4EDBR5.js → approve-J6JU7MBX.js} +3 -3
- package/dist/audit-5N2TU2X7.js +11 -0
- package/dist/{brief-7MWP35UI.js → brief-NHHILXIH.js} +3 -3
- package/dist/{chunk-YQZDYDAK.js → chunk-2DRA7S67.js} +1 -1
- package/dist/{chunk-446EZNAQ.js → chunk-6DJMKZGK.js} +83 -10
- package/dist/chunk-7N5JDXQG.js +109 -0
- package/dist/{chunk-ANFNXFRI.js → chunk-GFUDKTC5.js} +2 -2
- package/dist/{chunk-BUMOGHQA.js → chunk-KQFEKOON.js} +1 -1
- package/dist/{chunk-LVR2DPR7.js → chunk-M7XDP6FT.js} +30 -3
- package/dist/{chunk-PIJZUDDQ.js → chunk-PQISPOB2.js} +191 -130
- package/dist/{chunk-WKP7M2B3.js → chunk-UMKC3OWQ.js} +11 -6
- package/dist/{chunk-FNA4R5KT.js → chunk-WKP72Z3J.js} +59 -11
- package/dist/cli.js +16 -16
- package/dist/{doctor-R4QDCAN5.js → doctor-264Q5PLY.js} +6 -6
- package/dist/{fleet-WPYEVHVL.js → fleet-VT625Q2J.js} +22 -6
- package/dist/{gates-3HRGYCYY.js → gates-YUMSTKUG.js} +5 -4
- package/dist/{generate-FSJQFQPV.js → generate-62GQSCKH.js} +3 -2
- package/dist/index.d.ts +21 -3
- package/dist/index.js +379 -58
- package/dist/{init-6RULX5JZ.js → init-G4XXFXVD.js} +5 -4
- package/dist/{premise-GFWHSR3K.js → premise-RNMAKMVA.js} +9 -5
- package/dist/{propose-IYNOIWAO.js → propose-35JXMEPH.js} +6 -6
- package/dist/scan-F6OW4ANK.js +5 -0
- package/dist/{scan-LLWW4YOU.js → scan-PJH32H7C.js} +3 -3
- package/package.json +2 -2
- package/dist/audit-PHAK5HEW.js +0 -11
- 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
|
|
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,
|
|
547
|
-
|
|
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-
|
|
4
|
-
import { VERSION } from './chunk-
|
|
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-
|
|
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-
|
|
4
|
-
import './chunk-
|
|
3
|
+
import { scanProject } from './chunk-6DJMKZGK.js';
|
|
4
|
+
import './chunk-KQFEKOON.js';
|
|
5
5
|
import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
|
|
6
|
-
import './chunk-
|
|
6
|
+
import './chunk-2DRA7S67.js';
|
|
7
7
|
import './chunk-4VPBP6K6.js';
|
|
8
8
|
import { promises } from 'fs';
|
|
9
9
|
import path from 'path';
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { VERSION } from './chunk-
|
|
3
|
-
import { PACK_VERSION } from './chunk-
|
|
4
|
-
import { normalizeRelPath, isDirectory,
|
|
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
|
-
|
|
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.
|
|
291
|
+
const base = path.resolve(root, dir);
|
|
283
292
|
return {
|
|
284
|
-
source:
|
|
285
|
-
dir
|
|
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
|
|
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
|
|
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-
|
|
3
|
-
import { parseFailOnTier, meetsFailOn } from './chunk-
|
|
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,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runAudit } from './chunk-
|
|
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-
|
|
1009
|
-
const { scanProject } = await import('./scan-
|
|
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) {
|