etymd 0.19.1 → 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.
Files changed (28) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{approve-J6JU7MBX.js → approve-Z4NMEGDM.js} +3 -3
  3. package/dist/audit-4PGVS2TW.js +11 -0
  4. package/dist/{brief-NHHILXIH.js → brief-2N6X3CFR.js} +3 -3
  5. package/dist/{chunk-2DRA7S67.js → chunk-6FT7KSKU.js} +1 -1
  6. package/dist/{chunk-WKP72Z3J.js → chunk-DWPMANIC.js} +1 -1
  7. package/dist/{chunk-UMKC3OWQ.js → chunk-FG5XKFR2.js} +3 -3
  8. package/dist/{chunk-6DJMKZGK.js → chunk-OKHNNWOV.js} +32 -15
  9. package/dist/{chunk-7N5JDXQG.js → chunk-TZGRV7UY.js} +1 -1
  10. package/dist/{chunk-KQFEKOON.js → chunk-W6GGG4LG.js} +1 -1
  11. package/dist/{chunk-M7XDP6FT.js → chunk-WIBN7PBH.js} +4 -4
  12. package/dist/{chunk-GFUDKTC5.js → chunk-WYJFZ2MB.js} +2 -2
  13. package/dist/{chunk-PQISPOB2.js → chunk-X5GR2EJK.js} +73 -22
  14. package/dist/cli.js +16 -16
  15. package/dist/{doctor-264Q5PLY.js → doctor-7YG342YP.js} +6 -6
  16. package/dist/{fleet-VT625Q2J.js → fleet-XZVQLY53.js} +8 -8
  17. package/dist/{gates-YUMSTKUG.js → gates-633B6G2T.js} +5 -5
  18. package/dist/{generate-62GQSCKH.js → generate-S5KGZIIM.js} +3 -3
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +104 -36
  21. package/dist/{init-G4XXFXVD.js → init-SL3HEB3H.js} +12 -5
  22. package/dist/{premise-RNMAKMVA.js → premise-62AXRQUK.js} +4 -4
  23. package/dist/{propose-35JXMEPH.js → propose-4UWLIFWJ.js} +6 -6
  24. package/dist/scan-M7STYIYQ.js +5 -0
  25. package/dist/{scan-PJH32H7C.js → scan-VEHQEEXY.js} +3 -3
  26. package/package.json +1 -1
  27. package/dist/audit-5N2TU2X7.js +0 -11
  28. package/dist/scan-F6OW4ANK.js +0 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # etymd
2
2
 
3
+ ## 0.19.2
4
+
5
+ ### Patch Changes
6
+
7
+ - b46bbb9: Generated pre-push hooks check only the shell scripts each pushed commit changes, instead of
8
+ every script in every pushed commit's tree. A commit that changes the gate itself, its
9
+ classifier or a `.shellcheckrc` (deletions included) is still checked whole. So is the commit
10
+ that installs the gate, which reads a repo's existing scripts once. A commit with no shell
11
+ script says so. Pointer checks no longer accept an `@AGENTS.md` import inside a fenced code
12
+ block, an unreadable Claude Code version counts as too old instead of passing, and `init` warns
13
+ when a `CLAUDE.md` or `.claude/CLAUDE.md` hides `AGENTS.md`. Regenerate existing hooks with
14
+ `etymd gates --yes` to apply the change.
15
+
3
16
  ## 0.19.1
4
17
 
5
18
  ### Patch Changes
@@ -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-6DJMKZGK.js';
4
- import { VERSION } from './chunk-KQFEKOON.js';
3
+ import { scanProject } from './chunk-OKHNNWOV.js';
4
+ import { VERSION } from './chunk-W6GGG4LG.js';
5
5
  import { readBaseline, summarizeBaselineDrift, isDriftEmpty, writeBaseline, deriveProfile } from './chunk-P6ATKV2R.js';
6
- import { PACK_VERSION } from './chunk-2DRA7S67.js';
6
+ import { PACK_VERSION } from './chunk-6FT7KSKU.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-WYJFZ2MB.js';
3
+ import './chunk-FG5XKFR2.js';
4
+ import './chunk-DWPMANIC.js';
5
+ import './chunk-3E2IPCRY.js';
6
+ import './chunk-HI7NWPRA.js';
7
+ import './chunk-OKHNNWOV.js';
8
+ import './chunk-W6GGG4LG.js';
9
+ import './chunk-P6ATKV2R.js';
10
+ import './chunk-6FT7KSKU.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-6DJMKZGK.js';
4
- import './chunk-KQFEKOON.js';
3
+ import { scanProject } from './chunk-OKHNNWOV.js';
4
+ import './chunk-W6GGG4LG.js';
5
5
  import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
6
- import './chunk-2DRA7S67.js';
6
+ import './chunk-6FT7KSKU.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 = "15";
3
+ var PACK_VERSION = "16";
4
4
 
5
5
  export { PACK_VERSION };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { expandFileGlobs } from './chunk-6DJMKZGK.js';
2
+ import { expandFileGlobs } from './chunk-OKHNNWOV.js';
3
3
  import { DEFAULT_CONFIG, CONFIG_FILE } from './chunk-P6ATKV2R.js';
4
4
  import { readText, isDirectory, matchesAnyGlob, normalizeRelPath, readJson, pathExists, git } from './chunk-4VPBP6K6.js';
5
5
  import path3 from 'path';
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-WKP72Z3J.js';
2
+ import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-DWPMANIC.js';
3
3
  import { readLedger, reconcileLedger, writeLedger, visibleFindings } from './chunk-3E2IPCRY.js';
4
4
  import { measureContext, contextFileLabel } from './chunk-HI7NWPRA.js';
5
- import { scanProject } from './chunk-6DJMKZGK.js';
5
+ import { scanProject } from './chunk-OKHNNWOV.js';
6
6
  import { DEFAULT_CONFIG, ETYMD_DIR, writeCachedFacts, readBaseline, readConfig, deriveProfile, baselineCarriesMachinePath, BASELINE_FILE, CONFIG_FILE } from './chunk-P6ATKV2R.js';
7
- import { PACK_VERSION } from './chunk-2DRA7S67.js';
7
+ import { PACK_VERSION } from './chunk-6FT7KSKU.js';
8
8
  import { pathExists, readText, readJson, isCiEnvironment, isExecutable } from './chunk-4VPBP6K6.js';
9
9
  import path from 'path';
10
10
  import YAML from 'yaml';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-KQFEKOON.js';
3
- import { PACK_VERSION } from './chunk-2DRA7S67.js';
2
+ import { VERSION } from './chunk-W6GGG4LG.js';
3
+ import { PACK_VERSION } from './chunk-6FT7KSKU.js';
4
4
  import { normalizeRelPath, isDirectory, pathExists, readText, readJson, git, matchesAnyGlob } from './chunk-4VPBP6K6.js';
5
5
  import path from 'path';
6
6
  import { execFile } from 'child_process';
@@ -447,8 +447,23 @@ async function detectArtifacts(root) {
447
447
  });
448
448
  return artifacts;
449
449
  }
450
- var ROOT_POINTER_IMPORT_RE = /^\s*@(\.\/)?AGENTS\.md\s*$/m;
451
- var DOT_CLAUDE_IMPORT_RE = /^\s*@\.\.\/AGENTS\.md\s*$/m;
450
+ var ROOT_POINTER_IMPORT_RE = /^ {0,3}@(\.\/)?AGENTS\.md[ \t]*$/m;
451
+ var DOT_CLAUDE_IMPORT_RE = /^ {0,3}@\.\.\/AGENTS\.md[ \t]*$/m;
452
+ function withoutFencedCode(text) {
453
+ const kept = [];
454
+ let fence = null;
455
+ for (const line of text.split("\n")) {
456
+ if (fence === null) {
457
+ const open = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
458
+ if (open?.[1] && !(open[1][0] === "`" && open[2]?.includes("`"))) fence = open[1];
459
+ else kept.push(line);
460
+ } else {
461
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line)?.[1];
462
+ if (close && close[0] === fence[0] && close.length >= fence.length) fence = null;
463
+ }
464
+ }
465
+ return kept.join("\n");
466
+ }
452
467
  var CLAUDE_AGENTS_FALLBACK_VERSION = "2.1.277";
453
468
  var pExecFile = promisify(execFile);
454
469
  var claudeVersionMemo;
@@ -457,18 +472,20 @@ function detectClaudeCodeVersion() {
457
472
  if (override !== void 0) {
458
473
  return Promise.resolve(override === "none" || override === "" ? null : override);
459
474
  }
460
- claudeVersionMemo ??= pExecFile("claude", ["--version"], { timeout: 5e3 }).then(({ stdout }) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? null).catch(() => null);
475
+ claudeVersionMemo ??= pExecFile("claude", ["--version"], { timeout: 5e3 }).then(({ stdout }) => /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?=\s|$)/.exec(stdout)?.[1] ?? null).catch(() => null);
461
476
  return claudeVersionMemo;
462
477
  }
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;
478
+ function mayPredate(a, b) {
479
+ const parse = (v) => /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/.exec(v.trim());
480
+ const pa = parse(a);
481
+ const pb = parse(b);
482
+ if (!pa || !pb) return true;
483
+ for (let i = 1; i <= 3; i++) {
484
+ const x = Number(pa[i]);
485
+ const y = Number(pb[i]);
469
486
  if (x !== y) return x < y;
470
487
  }
471
- return false;
488
+ return pa[4] !== void 0 && pb[4] === void 0;
472
489
  }
473
490
  async function checkClaudePointer(root, claudeVersion) {
474
491
  const agentsAbs = path.join(root, "AGENTS.md");
@@ -486,11 +503,11 @@ async function checkClaudePointer(root, claudeVersion) {
486
503
  return { ok: true, via: "same-file" };
487
504
  }
488
505
  const rootClaude = await readText(claudeAbs);
489
- if (rootClaude !== null && ROOT_POINTER_IMPORT_RE.test(rootClaude)) {
506
+ if (rootClaude !== null && ROOT_POINTER_IMPORT_RE.test(withoutFencedCode(rootClaude))) {
490
507
  return { ok: true, via: "root-import" };
491
508
  }
492
509
  const dotClaude = await readText(path.join(root, ".claude", "CLAUDE.md"));
493
- if (dotClaude !== null && DOT_CLAUDE_IMPORT_RE.test(dotClaude)) {
510
+ if (dotClaude !== null && DOT_CLAUDE_IMPORT_RE.test(withoutFencedCode(dotClaude))) {
494
511
  return { ok: true, via: "dotclaude-import" };
495
512
  }
496
513
  if (rootClaude !== null || dotClaude !== null) {
@@ -501,7 +518,7 @@ async function checkClaudePointer(root, claudeVersion) {
501
518
  };
502
519
  }
503
520
  const version = claudeVersion === void 0 ? await detectClaudeCodeVersion() : claudeVersion;
504
- if (version !== null && versionBefore(version, CLAUDE_AGENTS_FALLBACK_VERSION)) {
521
+ if (version !== null && mayPredate(version, CLAUDE_AGENTS_FALLBACK_VERSION)) {
505
522
  return {
506
523
  ok: false,
507
524
  kind: "old-reader",
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { isSafeGateCommand, generateAgentsMd, generateClaudePointerMd, isSelfBuildRepo, generatePreCommitHook, generateCommitMsgHook, generatePrePushHook, generateShellDiscoveryScript, generateArtifactCheckScript, fileOrigin } from './chunk-PQISPOB2.js';
2
+ import { isSafeGateCommand, generateAgentsMd, generateClaudePointerMd, isSelfBuildRepo, generatePreCommitHook, generateCommitMsgHook, generatePrePushHook, generateShellDiscoveryScript, generateArtifactCheckScript, fileOrigin } from './chunk-X5GR2EJK.js';
3
3
  import { DEFAULT_CONFIG } from './chunk-P6ATKV2R.js';
4
4
  import { readText, pathExists } from './chunk-4VPBP6K6.js';
5
5
  import path from 'path';
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // package.json
3
3
  var package_default = {
4
- version: "0.19.1"};
4
+ version: "0.19.2"};
5
5
 
6
6
  // src/version.ts
7
7
  var VERSION = package_default.version;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { runAudit } from './chunk-UMKC3OWQ.js';
2
+ import { runAudit } from './chunk-FG5XKFR2.js';
3
3
  import { readLedger } from './chunk-3E2IPCRY.js';
4
- import { checkClaudePointer, CLAUDE_AGENTS_FALLBACK_VERSION } from './chunk-6DJMKZGK.js';
4
+ import { checkClaudePointer, CLAUDE_AGENTS_FALLBACK_VERSION } from './chunk-OKHNNWOV.js';
5
5
  import { DEFAULT_CONFIG, ETYMD_DIR } from './chunk-P6ATKV2R.js';
6
6
  import { readText, isDirectory, git, pathExists } from './chunk-4VPBP6K6.js';
7
7
  import os from 'os';
@@ -1006,8 +1006,8 @@ async function checkGuardedEmails(manifest, findings, disclosures) {
1006
1006
  }
1007
1007
  }
1008
1008
  async function checkGateDrift(manifest, findings, disclosures) {
1009
- const { planWorkflow } = await import('./generate-62GQSCKH.js');
1010
- const { scanProject } = await import('./scan-F6OW4ANK.js');
1009
+ const { planWorkflow } = await import('./generate-S5KGZIIM.js');
1010
+ const { scanProject } = await import('./scan-M7STYIYQ.js');
1011
1011
  const { readConfig } = await import('./config-724Y3IOB.js');
1012
1012
  for (const entry of manifest.entries) {
1013
1013
  const root = entry.resolvedRoot;
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { runAudit } from './chunk-UMKC3OWQ.js';
3
- import { parseFailOnTier, meetsFailOn } from './chunk-WKP72Z3J.js';
2
+ import { runAudit } from './chunk-FG5XKFR2.js';
3
+ import { parseFailOnTier, meetsFailOn } from './chunk-DWPMANIC.js';
4
4
  import { print, section, theme, renderLensCoverage, renderFindings, renderLedgerDiff } from './chunk-HI7NWPRA.js';
5
5
 
6
6
  // src/commands/audit.ts
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { PACK_VERSION } from './chunk-2DRA7S67.js';
2
+ import { PACK_VERSION } from './chunk-6FT7KSKU.js';
3
3
  import { createHash } from 'crypto';
4
4
 
5
5
  var GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
@@ -269,11 +269,11 @@ function generateShellDiscoveryScript() {
269
269
  # that EXISTS but cannot be read is the other branch \u2014 coverage would silently shrink, so it
270
270
  # fails, naming the path.
271
271
  #
272
- # The two \`[ "$?" -eq 1 ]\` guards are the match/error protocol: grep reports "no match" as 1
273
- # and a failure as 2 or more, and only the first is a verdict. Dropping the guard would let a
274
- # failing matcher pass as "not a shell script" \u2014 the exact silent coverage-shrink the
275
- # fail-closed rules exist to prevent. The checker does not associate \`$?\` with the enclosing
276
- # if-condition, which is why this shape survives the pass it serves; keep it that way.
272
+ # The match/error protocol: grep reports "no match" as 1 and a failure as 2 or more, and only
273
+ # the first is a verdict. Letting a failure fall through would pass a broken matcher as "not a
274
+ # shell script" \u2014 the exact silent coverage-shrink the fail-closed rules exist to prevent. The
275
+ # status is captured on the grep's own line (\`|| st=$?\`), so no command added later can come
276
+ # between the grep and the check and silently replace the status being read.
277
277
  work=$1
278
278
  shift
279
279
  for file do
@@ -289,17 +289,22 @@ for file do
289
289
  exit 1
290
290
  }
291
291
  head -n 1 "$work/head-bytes" > "$work/first-line" || exit 1
292
- if grep -qE "^#!.*[/ ](ba|da)?sh( |$)" "$work/first-line"; then
293
- printf "./%s\\0" "$file" >> "$work/scripts" || exit 1
294
- printf . >> "$work/count" || exit 1
295
- else
296
- [ "$?" -eq 1 ] || exit 1
297
- if grep -qE "^#!.*[/ ]zsh( |$)" "$work/first-line"; then
298
- printf . >> "$work/zsh-count" || exit 1
299
- else
300
- [ "$?" -eq 1 ] || exit 1
301
- fi
302
- fi
292
+ st=0
293
+ grep -qE "^#!.*[/ ](ba|da)?sh( |$)" "$work/first-line" || st=$?
294
+ case $st in
295
+ (0)
296
+ printf "./%s\\0" "$file" >> "$work/scripts" || exit 1
297
+ printf . >> "$work/count" || exit 1 ;;
298
+ (1)
299
+ st=0
300
+ grep -qE "^#!.*[/ ]zsh( |$)" "$work/first-line" || st=$?
301
+ case $st in
302
+ (0) printf . >> "$work/zsh-count" || exit 1 ;;
303
+ (1) ;;
304
+ (*) exit 1 ;;
305
+ esac ;;
306
+ (*) exit 1 ;;
307
+ esac
303
308
  done
304
309
  `);
305
310
  }
@@ -369,7 +374,13 @@ if command -v shellcheck >/dev/null 2>&1; then
369
374
  exit 1
370
375
  }
371
376
  shas=$(sort -u "$shellcheck_tmp/shas") || exit 1
372
- [ -n "$shas" ] || echo "\u203A shellcheck: no commit in the pushed refs (deletes only, or nothing on stdin) \u2014 nothing to check"
377
+ if [ -n "$shas" ]; then
378
+ # Said up front: a first push from a fresh clone can carry the whole history, and a long
379
+ # silent hook reads as a hung one.
380
+ echo "\u203A shellcheck: $(printf '%s\\n' "$shas" | wc -l | tr -d ' ') commit(s) in the pushed range"
381
+ else
382
+ echo "\u203A shellcheck: no commit in the pushed refs (deletes only, or nothing on stdin) \u2014 nothing to check"
383
+ fi
373
384
  # Resolved once, before any cd: the classifier runs inside each materialised tree, and a
374
385
  # relative hook path would no longer point at it from there.
375
386
  discover="$(cd "$(dirname "$0")" && pwd)/discover-shell-scripts.sh" || exit 1
@@ -395,10 +406,48 @@ if command -v shellcheck >/dev/null 2>&1; then
395
406
  exit 1
396
407
  fi
397
408
  : > "$shellcheck_tmp/scripts" && : > "$shellcheck_tmp/count" && : > "$shellcheck_tmp/zsh-count" && : > "$shellcheck_tmp/skip-count" || exit 1
398
- git ls-tree -r -z --name-only "$sha" > "$shellcheck_tmp/tracked" || {
399
- echo "etymd: cannot enumerate the tree of $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha") for shellcheck" >&2
409
+ # Only the paths this commit adds, copies, modifies, renames or retypes against its first
410
+ # parent. A script the commit did not touch has its parent's bytes, and that parent was
411
+ # either gated when it reached the remote or sits in this range and is checked here; a
412
+ # merge diffed against its first parent brings in everything the other side added.
413
+ # Checking every script in every commit multiplied the cost by the range length, so a long
414
+ # push of a script-heavy repo ran long enough to look hung. Two changes alter the verdict
415
+ # on bytes nobody touched \u2014 the classifier deciding what is a script, and a .shellcheckrc
416
+ # deciding what is a finding \u2014 so a commit that touches either, deletion included (a
417
+ # removed .shellcheckrc re-enables every check it disabled), is checked whole. So is a
418
+ # commit that adds or changes this hook: a repo adopting the gate after the fact has
419
+ # scripts no gate ever read, and the commit installing it is where they get read once.
420
+ short=$(git rev-parse --short "$sha" 2>/dev/null || echo "$sha")
421
+ if git rev-parse -q --verify "\${sha}^1^{commit}" >/dev/null 2>&1; then
422
+ git diff-tree -r -z --name-only --no-commit-id "\${sha}^1" "$sha" > "$shellcheck_tmp/touched" \\
423
+ && git diff-tree -r -z --name-only --no-commit-id --diff-filter=ACMRT "\${sha}^1" "$sha" > "$shellcheck_tmp/tracked"
424
+ else
425
+ git diff-tree -r -z --name-only --no-commit-id --root "$sha" > "$shellcheck_tmp/touched" \\
426
+ && cp "$shellcheck_tmp/touched" "$shellcheck_tmp/tracked"
427
+ fi || {
428
+ echo "etymd: cannot enumerate the paths $short changes for shellcheck" >&2
400
429
  exit 1
401
430
  }
431
+ where="changed in $short"
432
+ # Through a file, not a pipe: a pipeline reports only grep's status, and a failing tr would
433
+ # hand grep nothing \u2014 a "no match" that silently takes the narrow path.
434
+ tr '\\000' '\\n' < "$shellcheck_tmp/touched" > "$shellcheck_tmp/touched-lines" || exit 1
435
+ # grep: 1 is "no match", a verdict; anything higher is the matcher failing. The status is
436
+ # taken on the grep's own line so nothing added later can come between them.
437
+ st=0
438
+ grep -qE '(^|/)(\\.shellcheckrc|discover-shell-scripts\\.sh|pre-push)$' "$shellcheck_tmp/touched-lines" || st=$?
439
+ case $st in
440
+ (0)
441
+ where="in $short (whole tree: the gate, its classifier or a .shellcheckrc changed)"
442
+ git ls-tree -r -z --name-only "$sha" > "$shellcheck_tmp/tracked" || {
443
+ echo "etymd: cannot enumerate the tree of $short for shellcheck" >&2
444
+ exit 1
445
+ } ;;
446
+ (1) ;;
447
+ (*)
448
+ echo "etymd: shell script discovery failed; cannot tell whether $short changes the gate, its classifier or a .shellcheckrc" >&2
449
+ exit 1 ;;
450
+ esac
402
451
  ( cd "$tree" && xargs -0 "$discover" "$shellcheck_tmp" ) < "$shellcheck_tmp/tracked" || {
403
452
  echo "etymd: shell script discovery failed; shellcheck coverage is incomplete" >&2
404
453
  exit 1
@@ -412,8 +461,10 @@ if command -v shellcheck >/dev/null 2>&1; then
412
461
  if [ "$zsh_count" -gt 0 ]; then
413
462
  echo "\u203A shellcheck: $((zsh_count)) zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
414
463
  fi
415
- if [ "$count" -gt 0 ]; then
416
- echo "\u203A shellcheck ($((count)) scripts in $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha"), blocking at severity=warning)"
464
+ if [ "$count" -eq 0 ]; then
465
+ echo "\u203A shellcheck: no shell script $where \u2014 nothing to check there"
466
+ else
467
+ echo "\u203A shellcheck ($((count)) scripts $where, blocking at severity=warning)"
417
468
  ( cd "$tree" && xargs -0 shellcheck -S warning -- < "$shellcheck_tmp/scripts" ) || {
418
469
  echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
419
470
  exit 1
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { VERSION } from './chunk-KQFEKOON.js';
2
+ import { VERSION } from './chunk-W6GGG4LG.js';
3
3
  import path from 'path';
4
4
  import { Command } from 'commander';
5
5
  import pc from 'picocolors';
@@ -31,7 +31,7 @@ program.command("audit").description("Verify every instruction claim against the
31
31
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
32
32
  ).action(
33
33
  (opts, cmd) => action(async () => {
34
- const { run } = await import('./audit-5N2TU2X7.js');
34
+ const { run } = await import('./audit-4PGVS2TW.js');
35
35
  await run({
36
36
  cwd: resolveCwd(cmd),
37
37
  json: opts.json,
@@ -44,7 +44,7 @@ program.command("audit").description("Verify every instruction claim against the
44
44
  );
45
45
  program.command("init").description("Onboard the truth guard: approve the baseline; scaffold AGENTS.md only if asked").option("-y, --yes", "accept defaults without prompting (never overwrites)").option("--with-agents", "also scaffold a minimal AGENTS.md where none exists (off by default)").action(
46
46
  (opts, cmd) => action(async () => {
47
- const { run } = await import('./init-G4XXFXVD.js');
47
+ const { run } = await import('./init-SL3HEB3H.js');
48
48
  await run({ cwd: resolveCwd(cmd), yes: opts.yes, withAgents: opts.withAgents });
49
49
  })
50
50
  );
@@ -52,19 +52,19 @@ program.command("approve").description(
52
52
  "Re-approve the committed baseline after intentional structural changes (non-interactive)"
53
53
  ).action(
54
54
  (_opts, cmd) => action(async () => {
55
- const { run } = await import('./approve-J6JU7MBX.js');
55
+ const { run } = await import('./approve-Z4NMEGDM.js');
56
56
  await run({ cwd: resolveCwd(cmd) });
57
57
  })
58
58
  );
59
59
  program.command("scan").description("Deterministically reckon the project into a facts index").option("--json", "print the raw facts as JSON").option("--no-save", "do not write the .etymd cache").action(
60
60
  (opts, cmd) => action(async () => {
61
- const { run } = await import('./scan-PJH32H7C.js');
61
+ const { run } = await import('./scan-VEHQEEXY.js');
62
62
  await run({ cwd: resolveCwd(cmd), json: opts.json, save: opts.save });
63
63
  })
64
64
  );
65
65
  program.command("doctor").description('Alias for `audit --truth` \u2014 "are the recorded instructions still true?"').option("--json", "print findings as JSON").action(
66
66
  (opts, cmd) => action(async () => {
67
- const { run } = await import('./doctor-264Q5PLY.js');
67
+ const { run } = await import('./doctor-7YG342YP.js');
68
68
  await run({ cwd: resolveCwd(cmd), json: opts.json });
69
69
  })
70
70
  );
@@ -76,7 +76,7 @@ program.command("context").description("Measure the always-loaded context footpr
76
76
  );
77
77
  program.command("brief").description("Emit a grounded briefing for the in-repo agent to complete the semantic layer").option("--human", "write a human onboarding brief instead of the agent briefing").action(
78
78
  (opts, cmd) => action(async () => {
79
- const { run } = await import('./brief-NHHILXIH.js');
79
+ const { run } = await import('./brief-2N6X3CFR.js');
80
80
  await run({ cwd: resolveCwd(cmd), human: opts.human });
81
81
  })
82
82
  );
@@ -90,7 +90,7 @@ program.command("premise").argument("[task]", "the task you are about to hand an
90
90
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
91
91
  ).action(
92
92
  (task, opts, cmd) => action(async () => {
93
- const { run } = await import('./premise-RNMAKMVA.js');
93
+ const { run } = await import('./premise-62AXRQUK.js');
94
94
  await run({
95
95
  cwd: resolveCwd(cmd),
96
96
  task,
@@ -132,7 +132,7 @@ var fleet = program.command("fleet").description(
132
132
  "exit non-zero when findings at/above this tier exist (risk|gap|polish)"
133
133
  ).action(
134
134
  (opts, cmd) => action(async () => {
135
- const { sweep } = await import('./fleet-VT625Q2J.js');
135
+ const { sweep } = await import('./fleet-XZVQLY53.js');
136
136
  await sweep({
137
137
  cwd: resolveCwd(cmd),
138
138
  manifest: opts.manifest,
@@ -147,7 +147,7 @@ var fleet = program.command("fleet").description(
147
147
  );
148
148
  fleet.command("check").description("Validate the manifest pair only \u2014 no lenses: dangling mappings, duplicates, leaks").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").option("--json", "print the findings as JSON (EXPERIMENTAL through 0.2.x)").action(
149
149
  (_opts, cmd) => action(async () => {
150
- const { check } = await import('./fleet-VT625Q2J.js');
150
+ const { check } = await import('./fleet-XZVQLY53.js');
151
151
  const opts = cmd.optsWithGlobals();
152
152
  await check({ cwd: resolveCwd(cmd), manifest: opts.manifest, json: opts.json });
153
153
  })
@@ -156,7 +156,7 @@ fleet.command("board").description(
156
156
  "Render the fleet board: every project's milestones (contract key `milestones`) plus a ranked initiatives table"
157
157
  ).option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").option("--initiatives <file>", "the hand-edited initiatives table (rank | id | initiative | \u2026)").option("--out <file>", "write the Markdown board here instead of printing it").option("--json", "print the board as JSON (EXPERIMENTAL)").action(
158
158
  (opts, cmd) => action(async () => {
159
- const { board } = await import('./fleet-VT625Q2J.js');
159
+ const { board } = await import('./fleet-XZVQLY53.js');
160
160
  const shared = cmd.optsWithGlobals();
161
161
  await board({
162
162
  cwd: resolveCwd(cmd),
@@ -172,7 +172,7 @@ fleet.command("add").argument("<dir>", "directory of the project to register").d
172
172
  "public-repo | public-bound | private \u2014 mandatory for personal entries"
173
173
  ).option("-y, --yes", "skip prompts; every mandatory value must be passed as a flag").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
174
174
  (dir, opts, cmd) => action(async () => {
175
- const { add } = await import('./fleet-VT625Q2J.js');
175
+ const { add } = await import('./fleet-XZVQLY53.js');
176
176
  const shared = cmd.optsWithGlobals();
177
177
  await add({
178
178
  cwd: resolveCwd(cmd),
@@ -195,7 +195,7 @@ fleet.command("dismiss").argument("<name>", "the registered project name").argum
195
195
  "Dismiss a project's finding from any cwd \u2014 guarded ledgers persist beside the manifest"
196
196
  ).requiredOption("--reason <text>", "why it is dismissed \u2014 recorded so the decision survives").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
197
197
  (name, id, _opts, cmd) => action(async () => {
198
- const { dismiss } = await import('./fleet-VT625Q2J.js');
198
+ const { dismiss } = await import('./fleet-XZVQLY53.js');
199
199
  const opts = cmd.optsWithGlobals();
200
200
  await dismiss({
201
201
  cwd: resolveCwd(cmd),
@@ -210,7 +210,7 @@ fleet.command("accept").argument("<name>", "the registered project name").argume
210
210
  "Accept a project's finding as a known trade-off \u2014 guarded ledgers persist beside the manifest"
211
211
  ).option("--reason <text>", "optional note on why the trade-off is accepted").option("--manifest <file>", "the fleet manifest \u2014 required unless the cwd holds registry.json").action(
212
212
  (name, id, _opts, cmd) => action(async () => {
213
- const { accept } = await import('./fleet-VT625Q2J.js');
213
+ const { accept } = await import('./fleet-XZVQLY53.js');
214
214
  const opts = cmd.optsWithGlobals();
215
215
  await accept({ cwd: resolveCwd(cmd), manifest: opts.manifest, name, id, reason: opts.reason });
216
216
  })
@@ -225,7 +225,7 @@ program.command("propose").description(
225
225
  "the fleet manifest \u2014 runs a read-only sweep now (no delta baseline move)"
226
226
  ).option("--from <file>", "read a stored `etymd fleet --json` output instead of sweeping").option("--json", "print the proposal/1 records as JSON (EXPERIMENTAL)").action(
227
227
  (opts, cmd) => action(async () => {
228
- const { run } = await import('./propose-35JXMEPH.js');
228
+ const { run } = await import('./propose-4UWLIFWJ.js');
229
229
  await run({
230
230
  cwd: resolveCwd(cmd),
231
231
  manifest: opts.manifest,
@@ -256,7 +256,7 @@ program.command("screen").description(
256
256
  );
257
257
  program.command("gates").description("Install the local git-hook gates (process \u2192 pre-commit, correctness \u2192 pre-push)").option("--ci", "note about the CI review gate (ships later; local gates install now)").option("-y, --yes", "skip prompts; never overwrites a hand-edited hook").action(
258
258
  (opts, cmd) => action(async () => {
259
- const { run } = await import('./gates-YUMSTKUG.js');
259
+ const { run } = await import('./gates-633B6G2T.js');
260
260
  await run({ cwd: resolveCwd(cmd), ci: opts.ci, yes: opts.yes });
261
261
  })
262
262
  );
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { run } from './chunk-GFUDKTC5.js';
3
- import './chunk-UMKC3OWQ.js';
4
- import './chunk-WKP72Z3J.js';
2
+ import { run } from './chunk-WYJFZ2MB.js';
3
+ import './chunk-FG5XKFR2.js';
4
+ import './chunk-DWPMANIC.js';
5
5
  import './chunk-3E2IPCRY.js';
6
6
  import './chunk-HI7NWPRA.js';
7
- import './chunk-6DJMKZGK.js';
8
- import './chunk-KQFEKOON.js';
7
+ import './chunk-OKHNNWOV.js';
8
+ import './chunk-W6GGG4LG.js';
9
9
  import './chunk-P6ATKV2R.js';
10
- import './chunk-2DRA7S67.js';
10
+ import './chunk-6FT7KSKU.js';
11
11
  import './chunk-4VPBP6K6.js';
12
12
 
13
13
  // src/commands/doctor.ts
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { dismiss, accept } from './chunk-HOR4M6EC.js';
3
- import { sweepFleet, FLEET_JSON_SCHEMA, checkManifest, FLEET_TRUST_VALUES, isFleetTrust, MILESTONES_FILE, parseMilestones, parseInitiatives, BOARD_JSON_SCHEMA, renderBoard, loadFleetManifest, fleetLedgerTarget, ensureFindingRecorded } from './chunk-M7XDP6FT.js';
4
- import './chunk-UMKC3OWQ.js';
5
- import { parseFailOnTier, meetsFailOn } from './chunk-WKP72Z3J.js';
3
+ import { sweepFleet, FLEET_JSON_SCHEMA, checkManifest, FLEET_TRUST_VALUES, isFleetTrust, MILESTONES_FILE, parseMilestones, parseInitiatives, BOARD_JSON_SCHEMA, renderBoard, loadFleetManifest, fleetLedgerTarget, ensureFindingRecorded } from './chunk-WIBN7PBH.js';
4
+ import './chunk-FG5XKFR2.js';
5
+ import { parseFailOnTier, meetsFailOn } from './chunk-DWPMANIC.js';
6
6
  import './chunk-3E2IPCRY.js';
7
- import { generateClaudePointerMd } from './chunk-PQISPOB2.js';
7
+ import { generateClaudePointerMd } from './chunk-X5GR2EJK.js';
8
8
  import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-HI7NWPRA.js';
9
- import { checkClaudePointer, scanProject } from './chunk-6DJMKZGK.js';
10
- import './chunk-KQFEKOON.js';
9
+ import { checkClaudePointer, scanProject } from './chunk-OKHNNWOV.js';
10
+ import './chunk-W6GGG4LG.js';
11
11
  import './chunk-P6ATKV2R.js';
12
- import './chunk-2DRA7S67.js';
12
+ import './chunk-6FT7KSKU.js';
13
13
  import { readText, pathExists, git } from './chunk-4VPBP6K6.js';
14
14
  import { promises } from 'fs';
15
15
  import os from 'os';
@@ -240,7 +240,7 @@ async function add(opts) {
240
240
  throw new Error(
241
241
  `\`${name}\` has a CLAUDE.md that hides its AGENTS.md from Claude Code (${pointer.detail}). Add a full-line \`@AGENTS.md\` import to it, e.g.:
242
242
 
243
- ${generateClaudePointerMd().trimEnd().split("\n").join("\n")}
243
+ ${generateClaudePointerMd().trimEnd()}
244
244
 
245
245
  \u2014 or symlink either file to the other, or delete the CLAUDE.md \u2014 then re-run \`etymd fleet add\`.`
246
246
  );
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { applyFiles } from './chunk-GWGKEPRX.js';
3
- import { deriveFailOn, derivedCommands, planWorkflow } from './chunk-7N5JDXQG.js';
4
- import { runPrefix, isSafeGateCommand } from './chunk-PQISPOB2.js';
3
+ import { deriveFailOn, derivedCommands, planWorkflow } from './chunk-TZGRV7UY.js';
4
+ import { runPrefix, isSafeGateCommand } from './chunk-X5GR2EJK.js';
5
5
  import { section, print, glyph, theme, renderPlan } from './chunk-HI7NWPRA.js';
6
- import { scanProject } from './chunk-6DJMKZGK.js';
7
- import './chunk-KQFEKOON.js';
6
+ import { scanProject } from './chunk-OKHNNWOV.js';
7
+ import './chunk-W6GGG4LG.js';
8
8
  import { readConfig, CONFIG_FILE, configPath } from './chunk-P6ATKV2R.js';
9
- import './chunk-2DRA7S67.js';
9
+ import './chunk-6FT7KSKU.js';
10
10
  import { readText, git } from './chunk-4VPBP6K6.js';
11
11
  import { promises } from 'fs';
12
12
  import path from 'path';
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-7N5JDXQG.js';
3
- import './chunk-PQISPOB2.js';
2
+ export { deriveFailOn, derivedCommands, planWorkflow, riskReachability } from './chunk-TZGRV7UY.js';
3
+ import './chunk-X5GR2EJK.js';
4
4
  import './chunk-P6ATKV2R.js';
5
- import './chunk-2DRA7S67.js';
5
+ import './chunk-6FT7KSKU.js';
6
6
  import './chunk-4VPBP6K6.js';
package/dist/index.d.ts CHANGED
@@ -1127,7 +1127,7 @@ declare const gateIntegrityLens: Lens;
1127
1127
  * Gaps in the sequence are deliberate: a number stays retired even when the change that took it
1128
1128
  * never shipped, so a version cited in a baseline or a record means exactly one pack, ever.
1129
1129
  */
1130
- declare const PACK_VERSION = "15";
1130
+ declare const PACK_VERSION = "16";
1131
1131
 
1132
1132
  declare const VERSION: string;
1133
1133
  declare const NAME: string;
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 = "15";
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.1",
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 versionBefore(a, b) {
513
- const pa = a.split(".").map(Number);
514
- const pb = b.split(".").map(Number);
515
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
516
- const x = pa[i] ?? 0;
517
- const y = pb[i] ?? 0;
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 false;
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 && versionBefore(version, CLAUDE_AGENTS_FALLBACK_VERSION)) {
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 = /^\s*@(\.\/)?AGENTS\.md\s*$/m;
783
- DOT_CLAUDE_IMPORT_RE = /^\s*@\.\.\/AGENTS\.md\s*$/m;
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
  }
@@ -1331,11 +1348,11 @@ function generateShellDiscoveryScript() {
1331
1348
  # that EXISTS but cannot be read is the other branch \u2014 coverage would silently shrink, so it
1332
1349
  # fails, naming the path.
1333
1350
  #
1334
- # The two \`[ "$?" -eq 1 ]\` guards are the match/error protocol: grep reports "no match" as 1
1335
- # and a failure as 2 or more, and only the first is a verdict. Dropping the guard would let a
1336
- # failing matcher pass as "not a shell script" \u2014 the exact silent coverage-shrink the
1337
- # fail-closed rules exist to prevent. The checker does not associate \`$?\` with the enclosing
1338
- # if-condition, which is why this shape survives the pass it serves; keep it that way.
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.
1339
1356
  work=$1
1340
1357
  shift
1341
1358
  for file do
@@ -1351,17 +1368,22 @@ for file do
1351
1368
  exit 1
1352
1369
  }
1353
1370
  head -n 1 "$work/head-bytes" > "$work/first-line" || exit 1
1354
- if grep -qE "^#!.*[/ ](ba|da)?sh( |$)" "$work/first-line"; then
1355
- printf "./%s\\0" "$file" >> "$work/scripts" || exit 1
1356
- printf . >> "$work/count" || exit 1
1357
- else
1358
- [ "$?" -eq 1 ] || exit 1
1359
- if grep -qE "^#!.*[/ ]zsh( |$)" "$work/first-line"; then
1360
- printf . >> "$work/zsh-count" || exit 1
1361
- else
1362
- [ "$?" -eq 1 ] || exit 1
1363
- fi
1364
- fi
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
1365
1387
  done
1366
1388
  `);
1367
1389
  }
@@ -1431,7 +1453,13 @@ if command -v shellcheck >/dev/null 2>&1; then
1431
1453
  exit 1
1432
1454
  }
1433
1455
  shas=$(sort -u "$shellcheck_tmp/shas") || exit 1
1434
- [ -n "$shas" ] || echo "\u203A shellcheck: no commit in the pushed refs (deletes only, or nothing on stdin) \u2014 nothing to check"
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"
1462
+ fi
1435
1463
  # Resolved once, before any cd: the classifier runs inside each materialised tree, and a
1436
1464
  # relative hook path would no longer point at it from there.
1437
1465
  discover="$(cd "$(dirname "$0")" && pwd)/discover-shell-scripts.sh" || exit 1
@@ -1457,10 +1485,48 @@ if command -v shellcheck >/dev/null 2>&1; then
1457
1485
  exit 1
1458
1486
  fi
1459
1487
  : > "$shellcheck_tmp/scripts" && : > "$shellcheck_tmp/count" && : > "$shellcheck_tmp/zsh-count" && : > "$shellcheck_tmp/skip-count" || exit 1
1460
- git ls-tree -r -z --name-only "$sha" > "$shellcheck_tmp/tracked" || {
1461
- echo "etymd: cannot enumerate the tree of $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha") for shellcheck" >&2
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
1462
1508
  exit 1
1463
1509
  }
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
1464
1530
  ( cd "$tree" && xargs -0 "$discover" "$shellcheck_tmp" ) < "$shellcheck_tmp/tracked" || {
1465
1531
  echo "etymd: shell script discovery failed; shellcheck coverage is incomplete" >&2
1466
1532
  exit 1
@@ -1474,8 +1540,10 @@ if command -v shellcheck >/dev/null 2>&1; then
1474
1540
  if [ "$zsh_count" -gt 0 ]; then
1475
1541
  echo "\u203A shellcheck: $((zsh_count)) zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
1476
1542
  fi
1477
- if [ "$count" -gt 0 ]; then
1478
- echo "\u203A shellcheck ($((count)) scripts in $(git rev-parse --short "$sha" 2>/dev/null || echo "$sha"), blocking at severity=warning)"
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)"
1479
1547
  ( cd "$tree" && xargs -0 shellcheck -S warning -- < "$shellcheck_tmp/scripts" ) || {
1480
1548
  echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
1481
1549
  exit 1
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { applyFiles } from './chunk-GWGKEPRX.js';
3
- import { planWorkflow } from './chunk-7N5JDXQG.js';
4
- import './chunk-PQISPOB2.js';
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-6DJMKZGK.js';
7
- import { VERSION } from './chunk-KQFEKOON.js';
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-2DRA7S67.js';
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-WKP72Z3J.js';
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-6DJMKZGK.js';
5
- import './chunk-KQFEKOON.js';
4
+ import { scanProject } from './chunk-OKHNNWOV.js';
5
+ import './chunk-W6GGG4LG.js';
6
6
  import { ETYMD_DIR } from './chunk-P6ATKV2R.js';
7
- import './chunk-2DRA7S67.js';
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-M7XDP6FT.js';
3
- import './chunk-UMKC3OWQ.js';
4
- import './chunk-WKP72Z3J.js';
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-6DJMKZGK.js';
8
- import './chunk-KQFEKOON.js';
7
+ import './chunk-OKHNNWOV.js';
8
+ import './chunk-W6GGG4LG.js';
9
9
  import './chunk-P6ATKV2R.js';
10
- import './chunk-2DRA7S67.js';
10
+ import './chunk-6FT7KSKU.js';
11
11
  import { readText } from './chunk-4VPBP6K6.js';
12
12
  import path from 'path';
13
13
 
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ export { scanProject } from './chunk-OKHNNWOV.js';
3
+ import './chunk-W6GGG4LG.js';
4
+ import './chunk-6FT7KSKU.js';
5
+ import './chunk-4VPBP6K6.js';
@@ -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-6DJMKZGK.js';
4
- import './chunk-KQFEKOON.js';
3
+ import { scanProject } from './chunk-OKHNNWOV.js';
4
+ import './chunk-W6GGG4LG.js';
5
5
  import { writeCachedFacts } from './chunk-P6ATKV2R.js';
6
- import './chunk-2DRA7S67.js';
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.1",
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",
@@ -1,11 +0,0 @@
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,5 +0,0 @@
1
- #!/usr/bin/env node
2
- export { scanProject } from './chunk-6DJMKZGK.js';
3
- import './chunk-KQFEKOON.js';
4
- import './chunk-2DRA7S67.js';
5
- import './chunk-4VPBP6K6.js';