etymd 0.19.0 → 0.19.2
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 +24 -0
- package/dist/{approve-Y36ZO6IL.js → approve-Z4NMEGDM.js} +3 -3
- package/dist/audit-4PGVS2TW.js +11 -0
- package/dist/{brief-ILB6RMSQ.js → brief-2N6X3CFR.js} +3 -3
- package/dist/{chunk-U7BCBJIH.js → chunk-6FT7KSKU.js} +1 -1
- package/dist/{chunk-EYUW3UJU.js → chunk-DWPMANIC.js} +1 -1
- package/dist/{chunk-KIPD3N77.js → chunk-FG5XKFR2.js} +3 -3
- package/dist/{chunk-DLHC7QON.js → chunk-OKHNNWOV.js} +32 -15
- package/dist/{chunk-EZRK4MV3.js → chunk-TZGRV7UY.js} +1 -1
- package/dist/{chunk-L4KIIKXI.js → chunk-W6GGG4LG.js} +1 -1
- package/dist/{chunk-ZCOIZVSM.js → chunk-WIBN7PBH.js} +4 -4
- package/dist/{chunk-4IESOWHQ.js → chunk-WYJFZ2MB.js} +2 -2
- package/dist/{chunk-HSRHOVII.js → chunk-X5GR2EJK.js} +179 -59
- package/dist/cli.js +16 -16
- package/dist/{doctor-EHRADIUZ.js → doctor-7YG342YP.js} +6 -6
- package/dist/{fleet-KFF3N2O7.js → fleet-XZVQLY53.js} +8 -8
- package/dist/{gates-UJ23THID.js → gates-633B6G2T.js} +5 -5
- package/dist/{generate-6IPTCLYG.js → generate-S5KGZIIM.js} +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +210 -73
- package/dist/{init-7HWVHZMF.js → init-SL3HEB3H.js} +12 -5
- package/dist/{premise-U6JIB2QG.js → premise-62AXRQUK.js} +4 -4
- package/dist/{propose-NZMEPZIL.js → propose-4UWLIFWJ.js} +6 -6
- package/dist/scan-M7STYIYQ.js +5 -0
- package/dist/{scan-DLKYG5BB.js → scan-VEHQEEXY.js} +3 -3
- package/package.json +1 -1
- package/dist/audit-RONSEYFS.js +0 -11
- package/dist/scan-V53PGAQ6.js +0 -5
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ var __export = (target, all) => {
|
|
|
20
20
|
var PACK_VERSION;
|
|
21
21
|
var init_version = __esm({
|
|
22
22
|
"src/pack/version.ts"() {
|
|
23
|
-
PACK_VERSION = "
|
|
23
|
+
PACK_VERSION = "16";
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
@@ -30,7 +30,7 @@ var init_package = __esm({
|
|
|
30
30
|
"package.json"() {
|
|
31
31
|
package_default = {
|
|
32
32
|
name: "etymd",
|
|
33
|
-
version: "0.19.
|
|
33
|
+
version: "0.19.2",
|
|
34
34
|
description: "Keep your agent instructions true \u2014 verify AGENTS.md, CLAUDE.md, rules, skills, and the task you hand an agent against the actual repo, with drift caught over time and a regression ledger.",
|
|
35
35
|
keywords: [
|
|
36
36
|
"cli",
|
|
@@ -501,23 +501,40 @@ async function detectArtifacts(root) {
|
|
|
501
501
|
});
|
|
502
502
|
return artifacts;
|
|
503
503
|
}
|
|
504
|
+
function withoutFencedCode(text) {
|
|
505
|
+
const kept = [];
|
|
506
|
+
let fence = null;
|
|
507
|
+
for (const line of text.split("\n")) {
|
|
508
|
+
if (fence === null) {
|
|
509
|
+
const open = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
510
|
+
if (open?.[1] && !(open[1][0] === "`" && open[2]?.includes("`"))) fence = open[1];
|
|
511
|
+
else kept.push(line);
|
|
512
|
+
} else {
|
|
513
|
+
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line)?.[1];
|
|
514
|
+
if (close && close[0] === fence[0] && close.length >= fence.length) fence = null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return kept.join("\n");
|
|
518
|
+
}
|
|
504
519
|
function detectClaudeCodeVersion() {
|
|
505
520
|
const override = process.env.ETYMD_CLAUDE_VERSION;
|
|
506
521
|
if (override !== void 0) {
|
|
507
522
|
return Promise.resolve(override === "none" || override === "" ? null : override);
|
|
508
523
|
}
|
|
509
|
-
claudeVersionMemo ??= pExecFile2("claude", ["--version"], { timeout: 5e3 }).then(({ stdout }) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? null).catch(() => null);
|
|
524
|
+
claudeVersionMemo ??= pExecFile2("claude", ["--version"], { timeout: 5e3 }).then(({ stdout }) => /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?=\s|$)/.exec(stdout)?.[1] ?? null).catch(() => null);
|
|
510
525
|
return claudeVersionMemo;
|
|
511
526
|
}
|
|
512
|
-
function
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
527
|
+
function mayPredate(a, b) {
|
|
528
|
+
const parse = (v) => /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/.exec(v.trim());
|
|
529
|
+
const pa = parse(a);
|
|
530
|
+
const pb = parse(b);
|
|
531
|
+
if (!pa || !pb) return true;
|
|
532
|
+
for (let i = 1; i <= 3; i++) {
|
|
533
|
+
const x = Number(pa[i]);
|
|
534
|
+
const y = Number(pb[i]);
|
|
518
535
|
if (x !== y) return x < y;
|
|
519
536
|
}
|
|
520
|
-
return
|
|
537
|
+
return pa[4] !== void 0 && pb[4] === void 0;
|
|
521
538
|
}
|
|
522
539
|
async function checkClaudePointer(root, claudeVersion) {
|
|
523
540
|
const agentsAbs = path2.join(root, "AGENTS.md");
|
|
@@ -535,11 +552,11 @@ async function checkClaudePointer(root, claudeVersion) {
|
|
|
535
552
|
return { ok: true, via: "same-file" };
|
|
536
553
|
}
|
|
537
554
|
const rootClaude = await readText(claudeAbs);
|
|
538
|
-
if (rootClaude !== null && ROOT_POINTER_IMPORT_RE.test(rootClaude)) {
|
|
555
|
+
if (rootClaude !== null && ROOT_POINTER_IMPORT_RE.test(withoutFencedCode(rootClaude))) {
|
|
539
556
|
return { ok: true, via: "root-import" };
|
|
540
557
|
}
|
|
541
558
|
const dotClaude = await readText(path2.join(root, ".claude", "CLAUDE.md"));
|
|
542
|
-
if (dotClaude !== null && DOT_CLAUDE_IMPORT_RE.test(dotClaude)) {
|
|
559
|
+
if (dotClaude !== null && DOT_CLAUDE_IMPORT_RE.test(withoutFencedCode(dotClaude))) {
|
|
543
560
|
return { ok: true, via: "dotclaude-import" };
|
|
544
561
|
}
|
|
545
562
|
if (rootClaude !== null || dotClaude !== null) {
|
|
@@ -550,7 +567,7 @@ async function checkClaudePointer(root, claudeVersion) {
|
|
|
550
567
|
};
|
|
551
568
|
}
|
|
552
569
|
const version = await detectClaudeCodeVersion() ;
|
|
553
|
-
if (version !== null &&
|
|
570
|
+
if (version !== null && mayPredate(version, CLAUDE_AGENTS_FALLBACK_VERSION)) {
|
|
554
571
|
return {
|
|
555
572
|
ok: false,
|
|
556
573
|
kind: "old-reader",
|
|
@@ -779,8 +796,8 @@ var init_detect = __esm({
|
|
|
779
796
|
];
|
|
780
797
|
SHELL_SHEBANG = /^#!\s*\/\S*\b(?:ba|da|z)?sh\b|^#!\s*\/usr\/bin\/env\s+(?:ba|da|z)?sh\b/;
|
|
781
798
|
SHELL_EXT = /\.(?:sh|bash|zsh)$/;
|
|
782
|
-
ROOT_POINTER_IMPORT_RE =
|
|
783
|
-
DOT_CLAUDE_IMPORT_RE =
|
|
799
|
+
ROOT_POINTER_IMPORT_RE = /^ {0,3}@(\.\/)?AGENTS\.md[ \t]*$/m;
|
|
800
|
+
DOT_CLAUDE_IMPORT_RE = /^ {0,3}@\.\.\/AGENTS\.md[ \t]*$/m;
|
|
784
801
|
CLAUDE_AGENTS_FALLBACK_VERSION = "2.1.277";
|
|
785
802
|
pExecFile2 = promisify(execFile);
|
|
786
803
|
}
|
|
@@ -1237,15 +1254,21 @@ function contentScreenCall(opts) {
|
|
|
1237
1254
|
`${indent}fi`
|
|
1238
1255
|
].join("\n");
|
|
1239
1256
|
}
|
|
1240
|
-
function localHookCall(hook) {
|
|
1257
|
+
function localHookCall(hook, feedRefs = false) {
|
|
1258
|
+
const refsCapture = feedRefs ? `# git hands the pushed refs to pre-push ONCE, on stdin \u2014 one line per ref:
|
|
1259
|
+
# "<local ref> <local sha> <remote ref> <remote sha>". The shell gate below reads them too, so
|
|
1260
|
+
# they are captured here and fed to each.
|
|
1261
|
+
refs=$(cat)
|
|
1262
|
+
` : "";
|
|
1263
|
+
const call = feedRefs ? `printf '%s\\n' "$refs" | "$LOCAL" "$@" || exit 1` : `"$LOCAL" "$@" || exit 1`;
|
|
1241
1264
|
return `# Repo-owned checks. This file is generated and will be overwritten; \`.githooks/${hook}.local\`
|
|
1242
1265
|
# is yours \u2014 etymd never reads, writes, or regenerates it. Put project-specific guards there.
|
|
1243
1266
|
# A guard running tests that build fixture repositories should scrub git's exported GIT_* names
|
|
1244
1267
|
# first \u2014 a child git inherits them and ignores its cwd, so the suite would hit the real repo:
|
|
1245
1268
|
# env $(env | grep -o '^GIT_[A-Za-z0-9_]*' | sed 's/^/-u /') <your command>
|
|
1246
1269
|
LOCAL="$(dirname "$0")/${hook}.local"
|
|
1247
|
-
if [ -x "$LOCAL" ]; then
|
|
1248
|
-
|
|
1270
|
+
${refsCapture}if [ -x "$LOCAL" ]; then
|
|
1271
|
+
${call}
|
|
1249
1272
|
fi`;
|
|
1250
1273
|
}
|
|
1251
1274
|
function generatePreCommitHook(selfBuild = false) {
|
|
@@ -1254,10 +1277,10 @@ function generatePreCommitHook(selfBuild = false) {
|
|
|
1254
1277
|
|
|
1255
1278
|
${localHookCall("pre-commit")}
|
|
1256
1279
|
|
|
1257
|
-
# Content screen \u2014 staged file bytes. Refuses to commit environment,
|
|
1258
|
-
#
|
|
1259
|
-
# are machine-local by design, so this is a NO-OP wherever no checker is installed:
|
|
1260
|
-
# commit anywhere, active only where you opted in.
|
|
1280
|
+
# Content screen \u2014 staged file bytes. Refuses to commit detail about your environment, work
|
|
1281
|
+
# or identity into a repo whose history is (or could become) public. The checker and its
|
|
1282
|
+
# patterns are machine-local by design, so this is a NO-OP wherever no checker is installed:
|
|
1283
|
+
# safe to commit anywhere, active only where you opted in.
|
|
1261
1284
|
#
|
|
1262
1285
|
# Bypass, with a reason: git commit --no-verify
|
|
1263
1286
|
${contentGateResolution(selfBuild)}
|
|
@@ -1318,17 +1341,18 @@ function generateShellDiscoveryScript() {
|
|
|
1318
1341
|
# side, positional here). Verdicts land in the scratch: scripts (NUL-delimited matches) and one
|
|
1319
1342
|
# dot per decision into count / zsh-count / skip-count, tallied by the hook after the pipeline.
|
|
1320
1343
|
#
|
|
1321
|
-
#
|
|
1322
|
-
#
|
|
1323
|
-
#
|
|
1344
|
+
# The hook runs it from inside a commit materialised from git's object store, so paths resolve
|
|
1345
|
+
# against that tree, never the working tree. A path with nothing readable behind it \u2014 a
|
|
1346
|
+
# submodule entry, a dangling symlink \u2014 cannot lie about its contents, so it is a disclosed
|
|
1347
|
+
# skip, never a block. A regular file
|
|
1324
1348
|
# that EXISTS but cannot be read is the other branch \u2014 coverage would silently shrink, so it
|
|
1325
1349
|
# fails, naming the path.
|
|
1326
1350
|
#
|
|
1327
|
-
# The
|
|
1328
|
-
#
|
|
1329
|
-
#
|
|
1330
|
-
#
|
|
1331
|
-
#
|
|
1351
|
+
# The match/error protocol: grep reports "no match" as 1 and a failure as 2 or more, and only
|
|
1352
|
+
# the first is a verdict. Letting a failure fall through would pass a broken matcher as "not a
|
|
1353
|
+
# shell script" \u2014 the exact silent coverage-shrink the fail-closed rules exist to prevent. The
|
|
1354
|
+
# status is captured on the grep's own line (\`|| st=$?\`), so no command added later can come
|
|
1355
|
+
# between the grep and the check and silently replace the status being read.
|
|
1332
1356
|
work=$1
|
|
1333
1357
|
shift
|
|
1334
1358
|
for file do
|
|
@@ -1344,33 +1368,42 @@ for file do
|
|
|
1344
1368
|
exit 1
|
|
1345
1369
|
}
|
|
1346
1370
|
head -n 1 "$work/head-bytes" > "$work/first-line" || exit 1
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
[ "
|
|
1356
|
-
|
|
1357
|
-
|
|
1371
|
+
st=0
|
|
1372
|
+
grep -qE "^#!.*[/ ](ba|da)?sh( |$)" "$work/first-line" || st=$?
|
|
1373
|
+
case $st in
|
|
1374
|
+
(0)
|
|
1375
|
+
printf "./%s\\0" "$file" >> "$work/scripts" || exit 1
|
|
1376
|
+
printf . >> "$work/count" || exit 1 ;;
|
|
1377
|
+
(1)
|
|
1378
|
+
st=0
|
|
1379
|
+
grep -qE "^#!.*[/ ]zsh( |$)" "$work/first-line" || st=$?
|
|
1380
|
+
case $st in
|
|
1381
|
+
(0) printf . >> "$work/zsh-count" || exit 1 ;;
|
|
1382
|
+
(1) ;;
|
|
1383
|
+
(*) exit 1 ;;
|
|
1384
|
+
esac ;;
|
|
1385
|
+
(*) exit 1 ;;
|
|
1386
|
+
esac
|
|
1358
1387
|
done
|
|
1359
1388
|
`);
|
|
1360
1389
|
}
|
|
1361
1390
|
function shellcheckStep() {
|
|
1362
1391
|
return `
|
|
1363
1392
|
# Shell correctness. Scripts are discovered by shebang over TRACKED files at push time, so a
|
|
1364
|
-
# script added later is covered without regenerating this hook. The
|
|
1365
|
-
#
|
|
1393
|
+
# script added later is covered without regenerating this hook. The commits BEING PUSHED are
|
|
1394
|
+
# the bytes that ship, so each one is materialised from git's object store and checked there \u2014
|
|
1395
|
+
# never the working tree (wrong in both directions: a fixed tree let an unfixed commit ship, and
|
|
1396
|
+
# a dirty tree shared by several sessions blocked an unrelated push) and never the tip alone (a
|
|
1397
|
+
# bad commit under a clean tip shipped while the gate read only the tip's fix). The classifier
|
|
1398
|
+
# is discover-shell-scripts.sh beside this hook \u2014 tracked and shebanged like what it classifies,
|
|
1366
1399
|
# so the scan it implements finds and checks it too. zsh is NOT in the checked set:
|
|
1367
1400
|
# the checker cannot parse it (SC1071 is a parser-level error no inline directive can silence),
|
|
1368
1401
|
# so checking it would fail every push on the parser, not on the script. Excluded \u2014 and said so
|
|
1369
1402
|
# at run time below, because a coverage hole that is silent is indistinguishable from coverage.
|
|
1370
1403
|
# The same honesty splits the unreadable: a regular file that exists but cannot be read blocks
|
|
1371
1404
|
# the push (coverage would otherwise silently shrink), while a tracked path with nothing readable
|
|
1372
|
-
# behind it \u2014 a submodule entry, a dangling symlink
|
|
1373
|
-
#
|
|
1405
|
+
# behind it \u2014 a submodule entry, a dangling symlink \u2014 is counted and said so below as skipped,
|
|
1406
|
+
# never a block.
|
|
1374
1407
|
#
|
|
1375
1408
|
# "the checker", not its name, on purpose: a comment whose first word is that name is read as
|
|
1376
1409
|
# a DIRECTIVE, and an unparseable directive is itself an error (SC1072/SC1073). A hook that
|
|
@@ -1389,39 +1422,143 @@ if command -v shellcheck >/dev/null 2>&1; then
|
|
|
1389
1422
|
shellcheck_tmp=$(mktemp -d) || exit 1
|
|
1390
1423
|
trap 'rm -rf "$shellcheck_tmp"' 0
|
|
1391
1424
|
trap 'exit 1' 1 2 3 15
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1425
|
+
# Every commit in each pushed range, never the tip alone: pushing two commits \u2014 a bad
|
|
1426
|
+
# script, then its fix \u2014 passed a tip-only read while the bad commit landed on the remote.
|
|
1427
|
+
# An all-zero local sha is a delete (nothing to check). An all-zero REMOTE sha is a new
|
|
1428
|
+
# branch: everything no remote already has is being pushed, so the range is the local sha
|
|
1429
|
+
# minus every remote-tracking ref \u2014 commits a remote already received were gated when they
|
|
1430
|
+
# landed there, and the residue is exactly this push's new commits. A remote sha this clone
|
|
1431
|
+
# has never seen (the remote moved on since the last fetch) cannot bound a range, so it takes
|
|
1432
|
+
# the same new-branch rule instead of refusing a push the gate could have read. Enumeration
|
|
1433
|
+
# failure refuses the push: a range the gate could not list is a range it did not read.
|
|
1434
|
+
# (pattern) with both parens: bash 3.2 (macOS /bin/sh) cannot parse an unbalanced )
|
|
1435
|
+
# in a case pattern.
|
|
1436
|
+
: > "$shellcheck_tmp/shas" || exit 1
|
|
1437
|
+
printf '%s\\n' "$refs" | while read -r _lref lsha _rref rsha; do
|
|
1438
|
+
case "$lsha" in
|
|
1439
|
+
(*[!0]*) ;;
|
|
1440
|
+
(*) continue ;;
|
|
1441
|
+
esac
|
|
1442
|
+
case "$rsha" in
|
|
1443
|
+
(*[!0]*)
|
|
1444
|
+
if git cat-file -e "\${rsha}^{commit}" 2>/dev/null; then
|
|
1445
|
+
git rev-list "$rsha..$lsha"
|
|
1446
|
+
else
|
|
1447
|
+
git rev-list "$lsha" --not --remotes
|
|
1448
|
+
fi ;;
|
|
1449
|
+
(*) git rev-list "$lsha" --not --remotes ;;
|
|
1450
|
+
esac >> "$shellcheck_tmp/shas" || exit 1
|
|
1451
|
+
done || {
|
|
1452
|
+
echo "etymd: could not enumerate the commits being pushed for shellcheck" >&2
|
|
1399
1453
|
exit 1
|
|
1400
1454
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
echo "\u203A shellcheck: $(
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
echo "\u203A shellcheck: $((zsh_count)) zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
|
|
1455
|
+
shas=$(sort -u "$shellcheck_tmp/shas") || exit 1
|
|
1456
|
+
if [ -n "$shas" ]; then
|
|
1457
|
+
# Said up front: a first push from a fresh clone can carry the whole history, and a long
|
|
1458
|
+
# silent hook reads as a hung one.
|
|
1459
|
+
echo "\u203A shellcheck: $(printf '%s\\n' "$shas" | wc -l | tr -d ' ') commit(s) in the pushed range"
|
|
1460
|
+
else
|
|
1461
|
+
echo "\u203A shellcheck: no commit in the pushed refs (deletes only, or nothing on stdin) \u2014 nothing to check"
|
|
1409
1462
|
fi
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1463
|
+
# Resolved once, before any cd: the classifier runs inside each materialised tree, and a
|
|
1464
|
+
# relative hook path would no longer point at it from there.
|
|
1465
|
+
discover="$(cd "$(dirname "$0")" && pwd)/discover-shell-scripts.sh" || exit 1
|
|
1466
|
+
for sha in $shas; do
|
|
1467
|
+
# A fresh directory per commit, removed once that commit is checked so a long push does
|
|
1468
|
+
# not pile up one full tree per commit; the subshell trap above still cleans the root on
|
|
1469
|
+
# every early exit.
|
|
1470
|
+
tree=$(mktemp -d "$shellcheck_tmp/commit.XXXXXX") || exit 1
|
|
1471
|
+
# Checked out through a scratch index, never \`git archive\`: archive honours the
|
|
1472
|
+
# export-ignore and export-subst attributes, so a script marked export-ignore would leave
|
|
1473
|
+
# the checked set without a word. read-tree + checkout-index writes every tracked blob.
|
|
1474
|
+
# Large-file pointers stay pointers \u2014 they are never shell, and a push must not need the
|
|
1475
|
+
# network to be checked.
|
|
1476
|
+
if ! git cat-file -e "\${sha}^{commit}" 2>/dev/null \\
|
|
1477
|
+
|| ! GIT_INDEX_FILE="$shellcheck_tmp/index" git read-tree "$sha" 2>/dev/null \\
|
|
1478
|
+
|| ! GIT_INDEX_FILE="$shellcheck_tmp/index" GIT_LFS_SKIP_SMUDGE=1 \\
|
|
1479
|
+
git checkout-index -a -f --prefix="$tree/" 2>/dev/null; then
|
|
1480
|
+
echo "\u2717 shellcheck: could not materialise $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha") \u2014 refusing the push rather than certifying bytes this gate did not read" >&2
|
|
1481
|
+
exit 1
|
|
1482
|
+
fi
|
|
1483
|
+
if [ -n "$(git ls-tree -r --name-only "$sha")" ] && [ -z "$(ls -A "$tree")" ]; then
|
|
1484
|
+
echo "\u2717 shellcheck: $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha") extracted to an empty tree \u2014 refusing the push" >&2
|
|
1485
|
+
exit 1
|
|
1486
|
+
fi
|
|
1487
|
+
: > "$shellcheck_tmp/scripts" && : > "$shellcheck_tmp/count" && : > "$shellcheck_tmp/zsh-count" && : > "$shellcheck_tmp/skip-count" || exit 1
|
|
1488
|
+
# Only the paths this commit adds, copies, modifies, renames or retypes against its first
|
|
1489
|
+
# parent. A script the commit did not touch has its parent's bytes, and that parent was
|
|
1490
|
+
# either gated when it reached the remote or sits in this range and is checked here; a
|
|
1491
|
+
# merge diffed against its first parent brings in everything the other side added.
|
|
1492
|
+
# Checking every script in every commit multiplied the cost by the range length, so a long
|
|
1493
|
+
# push of a script-heavy repo ran long enough to look hung. Two changes alter the verdict
|
|
1494
|
+
# on bytes nobody touched \u2014 the classifier deciding what is a script, and a .shellcheckrc
|
|
1495
|
+
# deciding what is a finding \u2014 so a commit that touches either, deletion included (a
|
|
1496
|
+
# removed .shellcheckrc re-enables every check it disabled), is checked whole. So is a
|
|
1497
|
+
# commit that adds or changes this hook: a repo adopting the gate after the fact has
|
|
1498
|
+
# scripts no gate ever read, and the commit installing it is where they get read once.
|
|
1499
|
+
short=$(git rev-parse --short "$sha" 2>/dev/null || echo "$sha")
|
|
1500
|
+
if git rev-parse -q --verify "\${sha}^1^{commit}" >/dev/null 2>&1; then
|
|
1501
|
+
git diff-tree -r -z --name-only --no-commit-id "\${sha}^1" "$sha" > "$shellcheck_tmp/touched" \\
|
|
1502
|
+
&& git diff-tree -r -z --name-only --no-commit-id --diff-filter=ACMRT "\${sha}^1" "$sha" > "$shellcheck_tmp/tracked"
|
|
1503
|
+
else
|
|
1504
|
+
git diff-tree -r -z --name-only --no-commit-id --root "$sha" > "$shellcheck_tmp/touched" \\
|
|
1505
|
+
&& cp "$shellcheck_tmp/touched" "$shellcheck_tmp/tracked"
|
|
1506
|
+
fi || {
|
|
1507
|
+
echo "etymd: cannot enumerate the paths $short changes for shellcheck" >&2
|
|
1414
1508
|
exit 1
|
|
1415
1509
|
}
|
|
1416
|
-
|
|
1417
|
-
#
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1510
|
+
where="changed in $short"
|
|
1511
|
+
# Through a file, not a pipe: a pipeline reports only grep's status, and a failing tr would
|
|
1512
|
+
# hand grep nothing \u2014 a "no match" that silently takes the narrow path.
|
|
1513
|
+
tr '\\000' '\\n' < "$shellcheck_tmp/touched" > "$shellcheck_tmp/touched-lines" || exit 1
|
|
1514
|
+
# grep: 1 is "no match", a verdict; anything higher is the matcher failing. The status is
|
|
1515
|
+
# taken on the grep's own line so nothing added later can come between them.
|
|
1516
|
+
st=0
|
|
1517
|
+
grep -qE '(^|/)(\\.shellcheckrc|discover-shell-scripts\\.sh|pre-push)$' "$shellcheck_tmp/touched-lines" || st=$?
|
|
1518
|
+
case $st in
|
|
1519
|
+
(0)
|
|
1520
|
+
where="in $short (whole tree: the gate, its classifier or a .shellcheckrc changed)"
|
|
1521
|
+
git ls-tree -r -z --name-only "$sha" > "$shellcheck_tmp/tracked" || {
|
|
1522
|
+
echo "etymd: cannot enumerate the tree of $short for shellcheck" >&2
|
|
1523
|
+
exit 1
|
|
1524
|
+
} ;;
|
|
1525
|
+
(1) ;;
|
|
1526
|
+
(*)
|
|
1527
|
+
echo "etymd: shell script discovery failed; cannot tell whether $short changes the gate, its classifier or a .shellcheckrc" >&2
|
|
1528
|
+
exit 1 ;;
|
|
1529
|
+
esac
|
|
1530
|
+
( cd "$tree" && xargs -0 "$discover" "$shellcheck_tmp" ) < "$shellcheck_tmp/tracked" || {
|
|
1531
|
+
echo "etymd: shell script discovery failed; shellcheck coverage is incomplete" >&2
|
|
1532
|
+
exit 1
|
|
1533
|
+
}
|
|
1534
|
+
count=$(wc -c < "$shellcheck_tmp/count") || exit 1
|
|
1535
|
+
zsh_count=$(wc -c < "$shellcheck_tmp/zsh-count") || exit 1
|
|
1536
|
+
skip_count=$(wc -c < "$shellcheck_tmp/skip-count") || exit 1
|
|
1537
|
+
if [ "$skip_count" -gt 0 ]; then
|
|
1538
|
+
echo "\u203A shellcheck: $((skip_count)) tracked path(s) with nothing readable behind them (submodule or dangling symlink) \u2014 not checked, not failed"
|
|
1423
1539
|
fi
|
|
1424
|
-
|
|
1540
|
+
if [ "$zsh_count" -gt 0 ]; then
|
|
1541
|
+
echo "\u203A shellcheck: $((zsh_count)) zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
|
|
1542
|
+
fi
|
|
1543
|
+
if [ "$count" -eq 0 ]; then
|
|
1544
|
+
echo "\u203A shellcheck: no shell script $where \u2014 nothing to check there"
|
|
1545
|
+
else
|
|
1546
|
+
echo "\u203A shellcheck ($((count)) scripts $where, blocking at severity=warning)"
|
|
1547
|
+
( cd "$tree" && xargs -0 shellcheck -S warning -- < "$shellcheck_tmp/scripts" ) || {
|
|
1548
|
+
echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
|
|
1549
|
+
exit 1
|
|
1550
|
+
}
|
|
1551
|
+
# Everything below the blocking bar, shown once the push is already cleared. Never affects
|
|
1552
|
+
# the exit code \u2014 advice that can fail a push is not advice.
|
|
1553
|
+
advice=$( ( cd "$tree" && xargs -0 shellcheck -S style -f gcc -- < "$shellcheck_tmp/scripts" 2>/dev/null ) \\
|
|
1554
|
+
| grep -v ': warning:\\|: error:' || true)
|
|
1555
|
+
if [ -n "$advice" ]; then
|
|
1556
|
+
echo " \xB7 style/info (not blocking):"
|
|
1557
|
+
printf '%s\\n' "$advice" | sed 's/^/ /'
|
|
1558
|
+
fi
|
|
1559
|
+
fi
|
|
1560
|
+
rm -rf "$tree" || exit 1
|
|
1561
|
+
done
|
|
1425
1562
|
) || exit 1
|
|
1426
1563
|
else
|
|
1427
1564
|
echo "\u203A shellcheck skipped (not on PATH) \u2014 install it to gate this repo's shell scripts"
|
|
@@ -1455,7 +1592,7 @@ fi`;
|
|
|
1455
1592
|
return stampGenerated(`#!/usr/bin/env sh
|
|
1456
1593
|
# etymd: correctness gate. Mirrors CI cheapest-first; blocks the push on any failure.
|
|
1457
1594
|
|
|
1458
|
-
${localHookCall("pre-push")}${runner}
|
|
1595
|
+
${localHookCall("pre-push", true)}${runner}
|
|
1459
1596
|
${body}${shellStep}
|
|
1460
1597
|
${auditStep}
|
|
1461
1598
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { applyFiles } from './chunk-GWGKEPRX.js';
|
|
3
|
-
import { planWorkflow } from './chunk-
|
|
4
|
-
import './chunk-
|
|
3
|
+
import { planWorkflow } from './chunk-TZGRV7UY.js';
|
|
4
|
+
import './chunk-X5GR2EJK.js';
|
|
5
5
|
import { theme, renderFacts, print, renderPlan, section, glyph } from './chunk-HI7NWPRA.js';
|
|
6
|
-
import { scanProject } from './chunk-
|
|
7
|
-
import { VERSION } from './chunk-
|
|
6
|
+
import { scanProject, checkClaudePointer } from './chunk-OKHNNWOV.js';
|
|
7
|
+
import { VERSION } from './chunk-W6GGG4LG.js';
|
|
8
8
|
import { writeCachedFacts, deriveProfile, writeBaseline, CACHE_DIR } from './chunk-P6ATKV2R.js';
|
|
9
|
-
import { PACK_VERSION } from './chunk-
|
|
9
|
+
import { PACK_VERSION } from './chunk-6FT7KSKU.js';
|
|
10
10
|
import { git, readText } from './chunk-4VPBP6K6.js';
|
|
11
11
|
import { promises } from 'fs';
|
|
12
12
|
import path from 'path';
|
|
@@ -102,6 +102,13 @@ async function run(opts) {
|
|
|
102
102
|
section("Onboarded");
|
|
103
103
|
for (const w of result.written) print(` ${glyph.ok} ${theme.dim("wrote")} ${theme.info(w)}`);
|
|
104
104
|
for (const sk of result.skipped) print(` ${glyph.bullet} ${theme.dim("kept")} ${theme.dim(sk)}`);
|
|
105
|
+
const pointer = await checkClaudePointer(opts.cwd);
|
|
106
|
+
if (!pointer.ok && pointer.kind === "no-import") {
|
|
107
|
+
const line = pointer.detail.includes(".claude/CLAUDE.md") ? "@../AGENTS.md" : "@AGENTS.md";
|
|
108
|
+
print(
|
|
109
|
+
` ${glyph.partial} ${theme.warn(pointer.detail)} ${theme.dim(`\u2014 Claude Code reads that file instead; add a full-line ${line} import`)}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
105
112
|
print(
|
|
106
113
|
` ${glyph.ok} ${theme.dim("baseline approved \u2192")} ${theme.info(".etymd/baseline.json")} ${theme.dim("(commit it \u2014 drift is measured against it)")}`
|
|
107
114
|
);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { parseFailOnTier, meetsFailOn, buildTruthEnv, emptyCounters, checkTextClaims, checkDocRefs, loadDecisionLedger, checkDecisionRefs, rankFindings, PATH_TOKEN_RE, HOSTNAME_RE, KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP } from './chunk-
|
|
2
|
+
import { parseFailOnTier, meetsFailOn, buildTruthEnv, emptyCounters, checkTextClaims, checkDocRefs, loadDecisionLedger, checkDecisionRefs, rankFindings, PATH_TOKEN_RE, HOSTNAME_RE, KNOWN_EXTENSIONS, NAMESPACE_IDENT, NAMESPACE_STOP } from './chunk-DWPMANIC.js';
|
|
3
3
|
import { print, section, theme, renderFindings } from './chunk-HI7NWPRA.js';
|
|
4
|
-
import { scanProject } from './chunk-
|
|
5
|
-
import './chunk-
|
|
4
|
+
import { scanProject } from './chunk-OKHNNWOV.js';
|
|
5
|
+
import './chunk-W6GGG4LG.js';
|
|
6
6
|
import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
|
|
7
|
-
import './chunk-
|
|
7
|
+
import './chunk-6FT7KSKU.js';
|
|
8
8
|
import { readText, pathExists } from './chunk-4VPBP6K6.js';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { promises } from 'fs';
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { sweepFleet, loadFleetManifest, FLEET_JSON_SCHEMA } from './chunk-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
2
|
+
import { sweepFleet, loadFleetManifest, FLEET_JSON_SCHEMA } from './chunk-WIBN7PBH.js';
|
|
3
|
+
import './chunk-FG5XKFR2.js';
|
|
4
|
+
import './chunk-DWPMANIC.js';
|
|
5
5
|
import './chunk-3E2IPCRY.js';
|
|
6
6
|
import { print, section, theme, renderFleetNotes } from './chunk-HI7NWPRA.js';
|
|
7
|
-
import './chunk-
|
|
8
|
-
import './chunk-
|
|
7
|
+
import './chunk-OKHNNWOV.js';
|
|
8
|
+
import './chunk-W6GGG4LG.js';
|
|
9
9
|
import './chunk-P6ATKV2R.js';
|
|
10
|
-
import './chunk-
|
|
10
|
+
import './chunk-6FT7KSKU.js';
|
|
11
11
|
import { readText } from './chunk-4VPBP6K6.js';
|
|
12
12
|
import path from 'path';
|
|
13
13
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { print, renderFacts, theme } from './chunk-HI7NWPRA.js';
|
|
3
|
-
import { scanProject } from './chunk-
|
|
4
|
-
import './chunk-
|
|
3
|
+
import { scanProject } from './chunk-OKHNNWOV.js';
|
|
4
|
+
import './chunk-W6GGG4LG.js';
|
|
5
5
|
import { writeCachedFacts } from './chunk-P6ATKV2R.js';
|
|
6
|
-
import './chunk-
|
|
6
|
+
import './chunk-6FT7KSKU.js';
|
|
7
7
|
import './chunk-4VPBP6K6.js';
|
|
8
8
|
|
|
9
9
|
// src/commands/scan.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "etymd",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"description": "Keep your agent instructions true — verify AGENTS.md, CLAUDE.md, rules, skills, and the task you hand an agent against the actual repo, with drift caught over time and a regression ledger.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
package/dist/audit-RONSEYFS.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
export { run } from './chunk-4IESOWHQ.js';
|
|
3
|
-
import './chunk-KIPD3N77.js';
|
|
4
|
-
import './chunk-EYUW3UJU.js';
|
|
5
|
-
import './chunk-3E2IPCRY.js';
|
|
6
|
-
import './chunk-HI7NWPRA.js';
|
|
7
|
-
import './chunk-DLHC7QON.js';
|
|
8
|
-
import './chunk-L4KIIKXI.js';
|
|
9
|
-
import './chunk-P6ATKV2R.js';
|
|
10
|
-
import './chunk-U7BCBJIH.js';
|
|
11
|
-
import './chunk-4VPBP6K6.js';
|