etymd 0.16.0 → 0.19.0
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 +83 -0
- package/README.md +28 -5
- package/dist/{approve-K5FFTPA4.js → approve-Y36ZO6IL.js} +3 -3
- package/dist/audit-RONSEYFS.js +11 -0
- package/dist/{brief-DOQ2DQEG.js → brief-ILB6RMSQ.js} +3 -3
- package/dist/{chunk-FKDT4RBP.js → chunk-4IESOWHQ.js} +2 -2
- package/dist/{chunk-EJM47PIF.js → chunk-DLHC7QON.js} +83 -10
- package/dist/{chunk-Y4E6ERCM.js → chunk-EYUW3UJU.js} +59 -11
- package/dist/chunk-EZRK4MV3.js +109 -0
- package/dist/{chunk-OKJ3RGJB.js → chunk-HSRHOVII.js} +113 -117
- package/dist/{chunk-UV3YIBDW.js → chunk-KIPD3N77.js} +11 -6
- package/dist/{chunk-UYCGP5BS.js → chunk-L4KIIKXI.js} +1 -1
- package/dist/{chunk-YQZDYDAK.js → chunk-U7BCBJIH.js} +1 -1
- package/dist/{chunk-ANXGT7V3.js → chunk-ZCOIZVSM.js} +37 -5
- package/dist/cli.js +17 -17
- package/dist/{doctor-X6XAGKV7.js → doctor-EHRADIUZ.js} +6 -6
- package/dist/{fleet-UEFLKIRI.js → fleet-KFF3N2O7.js} +22 -6
- package/dist/{gates-NVWWKMT2.js → gates-UJ23THID.js} +9 -5
- package/dist/{generate-YEARCSHW.js → generate-6IPTCLYG.js} +3 -2
- package/dist/index.d.ts +29 -3
- package/dist/index.js +313 -52
- package/dist/{init-Z5O2WVRJ.js → init-7HWVHZMF.js} +5 -4
- package/dist/{premise-4QRU7LRI.js → premise-U6JIB2QG.js} +9 -5
- package/dist/{propose-IKE4CUIM.js → propose-NZMEPZIL.js} +6 -6
- package/dist/{scan-H3NF5IV5.js → scan-DLKYG5BB.js} +3 -3
- package/dist/scan-V53PGAQ6.js +5 -0
- package/dist/{screen-E4FC7W5N.js → screen-S736DXQ5.js} +56 -24
- package/package.json +2 -2
- package/dist/audit-HBNXNGD6.js +0 -11
- package/dist/scan-64Z6SNYF.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 = "14";
|
|
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.
|
|
33
|
+
version: "0.19.0",
|
|
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",
|
|
@@ -104,7 +104,7 @@ var init_package = __esm({
|
|
|
104
104
|
prettier: "3.4.2",
|
|
105
105
|
tsup: "8.5.1",
|
|
106
106
|
typescript: "5.7.2",
|
|
107
|
-
vitest: "4.1.
|
|
107
|
+
vitest: "4.1.11"
|
|
108
108
|
},
|
|
109
109
|
publishConfig: {
|
|
110
110
|
access: "public"
|
|
@@ -388,10 +388,17 @@ async function hasLintStagedConfig(root, pkg) {
|
|
|
388
388
|
}
|
|
389
389
|
return false;
|
|
390
390
|
}
|
|
391
|
+
function normalizeHooksPath(root, raw) {
|
|
392
|
+
const abs = path2.resolve(root, raw);
|
|
393
|
+
const rel = path2.relative(root, abs);
|
|
394
|
+
const outside = path2.isAbsolute(rel) || rel === ".." || rel.startsWith(`..${path2.sep}`);
|
|
395
|
+
return outside ? abs : normalizeRelPath(rel) || ".";
|
|
396
|
+
}
|
|
391
397
|
async function detectHooks(root, hooksPath, pkg) {
|
|
392
398
|
const lintStaged = await hasLintStagedConfig(root, pkg);
|
|
393
399
|
if (hooksPath) {
|
|
394
|
-
|
|
400
|
+
const dir = normalizeHooksPath(root, hooksPath);
|
|
401
|
+
if (dir === ".husky/_") {
|
|
395
402
|
const base2 = path2.join(root, ".husky");
|
|
396
403
|
return {
|
|
397
404
|
source: "husky",
|
|
@@ -402,10 +409,10 @@ async function detectHooks(root, hooksPath, pkg) {
|
|
|
402
409
|
lintStaged
|
|
403
410
|
};
|
|
404
411
|
}
|
|
405
|
-
const base = path2.
|
|
412
|
+
const base = path2.resolve(root, dir);
|
|
406
413
|
return {
|
|
407
|
-
source:
|
|
408
|
-
dir
|
|
414
|
+
source: dir === ".githooks" ? "githooks" : "custom",
|
|
415
|
+
dir,
|
|
409
416
|
preCommit: await pathExists(path2.join(base, "pre-commit")),
|
|
410
417
|
prePush: await pathExists(path2.join(base, "pre-push")),
|
|
411
418
|
commitMsg: await pathExists(path2.join(base, "commit-msg")),
|
|
@@ -494,6 +501,64 @@ async function detectArtifacts(root) {
|
|
|
494
501
|
});
|
|
495
502
|
return artifacts;
|
|
496
503
|
}
|
|
504
|
+
function detectClaudeCodeVersion() {
|
|
505
|
+
const override = process.env.ETYMD_CLAUDE_VERSION;
|
|
506
|
+
if (override !== void 0) {
|
|
507
|
+
return Promise.resolve(override === "none" || override === "" ? null : override);
|
|
508
|
+
}
|
|
509
|
+
claudeVersionMemo ??= pExecFile2("claude", ["--version"], { timeout: 5e3 }).then(({ stdout }) => /(\d+\.\d+\.\d+)/.exec(stdout)?.[1] ?? null).catch(() => null);
|
|
510
|
+
return claudeVersionMemo;
|
|
511
|
+
}
|
|
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;
|
|
518
|
+
if (x !== y) return x < y;
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
async function checkClaudePointer(root, claudeVersion) {
|
|
523
|
+
const agentsAbs = path2.join(root, "AGENTS.md");
|
|
524
|
+
const claudeAbs = path2.join(root, "CLAUDE.md");
|
|
525
|
+
if (!await pathExists(agentsAbs)) return { ok: true, via: "no-agents" };
|
|
526
|
+
const statOrNone = async (p) => {
|
|
527
|
+
try {
|
|
528
|
+
return await promises.stat(p);
|
|
529
|
+
} catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
const [agentsSt, claudeSt] = await Promise.all([statOrNone(agentsAbs), statOrNone(claudeAbs)]);
|
|
534
|
+
if (agentsSt && claudeSt && agentsSt.ino !== 0 && agentsSt.ino === claudeSt.ino && agentsSt.dev === claudeSt.dev) {
|
|
535
|
+
return { ok: true, via: "same-file" };
|
|
536
|
+
}
|
|
537
|
+
const rootClaude = await readText(claudeAbs);
|
|
538
|
+
if (rootClaude !== null && ROOT_POINTER_IMPORT_RE.test(rootClaude)) {
|
|
539
|
+
return { ok: true, via: "root-import" };
|
|
540
|
+
}
|
|
541
|
+
const dotClaude = await readText(path2.join(root, ".claude", "CLAUDE.md"));
|
|
542
|
+
if (dotClaude !== null && DOT_CLAUDE_IMPORT_RE.test(dotClaude)) {
|
|
543
|
+
return { ok: true, via: "dotclaude-import" };
|
|
544
|
+
}
|
|
545
|
+
if (rootClaude !== null || dotClaude !== null) {
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
kind: "no-import",
|
|
549
|
+
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"
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const version = await detectClaudeCodeVersion() ;
|
|
553
|
+
if (version !== null && versionBefore(version, CLAUDE_AGENTS_FALLBACK_VERSION)) {
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
kind: "old-reader",
|
|
557
|
+
detail: `AGENTS.md present, no CLAUDE.md, and Claude Code ${version} predates the AGENTS.md fallback (${CLAUDE_AGENTS_FALLBACK_VERSION})`
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return { ok: true, via: "native-fallback" };
|
|
561
|
+
}
|
|
497
562
|
async function walkTree(root) {
|
|
498
563
|
const state = { budget: 2e4, truncated: false };
|
|
499
564
|
const countFiles = async (dir) => {
|
|
@@ -539,7 +604,7 @@ async function walkTree(root) {
|
|
|
539
604
|
dirs.sort((a, b) => b.files - a.files);
|
|
540
605
|
return { dirs, truncated: state.truncated };
|
|
541
606
|
}
|
|
542
|
-
var IGNORED_DIRS, MEANINGFUL_DOT_DIRS, isMeta, ROLE_LADDERS, FRAMEWORK_MARKERS, ARTIFACT_SPECS, SHELL_SHEBANG, SHELL_EXT;
|
|
607
|
+
var IGNORED_DIRS, MEANINGFUL_DOT_DIRS, isMeta, ROLE_LADDERS, FRAMEWORK_MARKERS, ARTIFACT_SPECS, SHELL_SHEBANG, SHELL_EXT, ROOT_POINTER_IMPORT_RE, DOT_CLAUDE_IMPORT_RE, CLAUDE_AGENTS_FALLBACK_VERSION, pExecFile2, claudeVersionMemo;
|
|
543
608
|
var init_detect = __esm({
|
|
544
609
|
"src/core/detect.ts"() {
|
|
545
610
|
init_util();
|
|
@@ -714,6 +779,10 @@ var init_detect = __esm({
|
|
|
714
779
|
];
|
|
715
780
|
SHELL_SHEBANG = /^#!\s*\/\S*\b(?:ba|da|z)?sh\b|^#!\s*\/usr\/bin\/env\s+(?:ba|da|z)?sh\b/;
|
|
716
781
|
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;
|
|
784
|
+
CLAUDE_AGENTS_FALLBACK_VERSION = "2.1.277";
|
|
785
|
+
pExecFile2 = promisify(execFile);
|
|
717
786
|
}
|
|
718
787
|
});
|
|
719
788
|
|
|
@@ -795,11 +864,12 @@ async function scanProject(root, opts = {}) {
|
|
|
795
864
|
const [branch, head, hooksPathRaw, authorsRaw, subjectsRaw] = isRepo ? await Promise.all([
|
|
796
865
|
git(abs, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
797
866
|
git(abs, ["rev-parse", "--short", "HEAD"]),
|
|
798
|
-
git
|
|
867
|
+
// `--type=path` expands a leading `~` the way git itself does when it runs the hooks.
|
|
868
|
+
git(abs, ["config", "--type=path", "--get", "core.hooksPath"]),
|
|
799
869
|
git(abs, ["log", "-200", "--format=%ae"]),
|
|
800
870
|
git(abs, ["log", "-50", "--format=%s"])
|
|
801
871
|
]) : [null, null, null, null, null];
|
|
802
|
-
const hooksPath = hooksPathRaw
|
|
872
|
+
const hooksPath = hooksPathRaw ? normalizeHooksPath(abs, hooksPathRaw) : void 0;
|
|
803
873
|
const hooks = await detectHooks(abs, hooksPath, rootPkg);
|
|
804
874
|
const shell = await detectShellSurface(abs, isRepo);
|
|
805
875
|
const freshness = await collectFreshness(abs, isRepo, artifacts, opts.upstreamRemote);
|
|
@@ -1132,6 +1202,19 @@ ${[
|
|
|
1132
1202
|
].filter(Boolean).join("\n") || "# add the project's key commands"}
|
|
1133
1203
|
\`\`\`
|
|
1134
1204
|
|
|
1205
|
+
`,
|
|
1206
|
+
"md"
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
function generateClaudePointerMd() {
|
|
1210
|
+
return stampGenerated(
|
|
1211
|
+
`# CLAUDE.md
|
|
1212
|
+
|
|
1213
|
+
Single source of truth for agent instructions lives in \`AGENTS.md\`. This pointer keeps Claude
|
|
1214
|
+
Code aligned with every other agent \u2014 edit \`AGENTS.md\`, not this file.
|
|
1215
|
+
|
|
1216
|
+
@AGENTS.md
|
|
1217
|
+
|
|
1135
1218
|
`,
|
|
1136
1219
|
"md"
|
|
1137
1220
|
);
|
|
@@ -1228,42 +1311,118 @@ ${localHookCall("commit-msg")}
|
|
|
1228
1311
|
exit 0
|
|
1229
1312
|
`);
|
|
1230
1313
|
}
|
|
1314
|
+
function generateShellDiscoveryScript() {
|
|
1315
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
1316
|
+
# etymd: shell script discovery for the pre-push shellcheck step. Arguments: the scratch
|
|
1317
|
+
# directory the hook created, then the tracked paths to classify (NUL-delimited on the hook's
|
|
1318
|
+
# side, positional here). Verdicts land in the scratch: scripts (NUL-delimited matches) and one
|
|
1319
|
+
# dot per decision into count / zsh-count / skip-count, tallied by the hook after the pipeline.
|
|
1320
|
+
#
|
|
1321
|
+
# A path with nothing readable behind it \u2014 a submodule entry, a dangling symlink, a file
|
|
1322
|
+
# deleted from the worktree while still tracked \u2014 cannot lie about its contents, so it is a
|
|
1323
|
+
# disclosed skip, never a block: an absent worktree file is routine dirty state. A regular file
|
|
1324
|
+
# that EXISTS but cannot be read is the other branch \u2014 coverage would silently shrink, so it
|
|
1325
|
+
# fails, naming the path.
|
|
1326
|
+
#
|
|
1327
|
+
# The two \`[ "$?" -eq 1 ]\` guards are the match/error protocol: grep reports "no match" as 1
|
|
1328
|
+
# and a failure as 2 or more, and only the first is a verdict. Dropping the guard would let a
|
|
1329
|
+
# failing matcher pass as "not a shell script" \u2014 the exact silent coverage-shrink the
|
|
1330
|
+
# fail-closed rules exist to prevent. The checker does not associate \`$?\` with the enclosing
|
|
1331
|
+
# if-condition, which is why this shape survives the pass it serves; keep it that way.
|
|
1332
|
+
work=$1
|
|
1333
|
+
shift
|
|
1334
|
+
for file do
|
|
1335
|
+
if [ ! -f "./$file" ]; then
|
|
1336
|
+
printf . >> "$work/skip-count" || exit 1
|
|
1337
|
+
continue
|
|
1338
|
+
fi
|
|
1339
|
+
# 4096 bytes bound the read \u2014 a binary with no newline would otherwise be copied whole
|
|
1340
|
+
# into the scratch on every push. The second head restores line-1-only semantics, so a
|
|
1341
|
+
# shebang embedded on a LATER line of a document cannot match the patterns below.
|
|
1342
|
+
head -c 4096 "./$file" > "$work/head-bytes" || {
|
|
1343
|
+
echo "etymd: cannot read tracked file for shellcheck: $file" >&2
|
|
1344
|
+
exit 1
|
|
1345
|
+
}
|
|
1346
|
+
head -n 1 "$work/head-bytes" > "$work/first-line" || exit 1
|
|
1347
|
+
if grep -qE "^#!.*[/ ](ba|da)?sh( |$)" "$work/first-line"; then
|
|
1348
|
+
printf "./%s\\0" "$file" >> "$work/scripts" || exit 1
|
|
1349
|
+
printf . >> "$work/count" || exit 1
|
|
1350
|
+
else
|
|
1351
|
+
[ "$?" -eq 1 ] || exit 1
|
|
1352
|
+
if grep -qE "^#!.*[/ ]zsh( |$)" "$work/first-line"; then
|
|
1353
|
+
printf . >> "$work/zsh-count" || exit 1
|
|
1354
|
+
else
|
|
1355
|
+
[ "$?" -eq 1 ] || exit 1
|
|
1356
|
+
fi
|
|
1357
|
+
fi
|
|
1358
|
+
done
|
|
1359
|
+
`);
|
|
1360
|
+
}
|
|
1231
1361
|
function shellcheckStep() {
|
|
1232
1362
|
return `
|
|
1233
1363
|
# Shell correctness. Scripts are discovered by shebang over TRACKED files at push time, so a
|
|
1234
|
-
# script added later is covered without regenerating this hook.
|
|
1364
|
+
# script added later is covered without regenerating this hook. The classifier is
|
|
1365
|
+
# discover-shell-scripts.sh beside this hook \u2014 tracked and shebanged like what it classifies,
|
|
1366
|
+
# so the scan it implements finds and checks it too. zsh is NOT in the checked set:
|
|
1235
1367
|
# the checker cannot parse it (SC1071 is a parser-level error no inline directive can silence),
|
|
1236
1368
|
# so checking it would fail every push on the parser, not on the script. Excluded \u2014 and said so
|
|
1237
1369
|
# at run time below, because a coverage hole that is silent is indistinguishable from coverage.
|
|
1370
|
+
# The same honesty splits the unreadable: a regular file that exists but cannot be read blocks
|
|
1371
|
+
# the push (coverage would otherwise silently shrink), while a tracked path with nothing readable
|
|
1372
|
+
# behind it \u2014 a submodule entry, a dangling symlink, a file deleted from the worktree while
|
|
1373
|
+
# still tracked \u2014 is counted and said so below as skipped, never a block.
|
|
1238
1374
|
#
|
|
1239
1375
|
# "the checker", not its name, on purpose: a comment whose first word is that name is read as
|
|
1240
1376
|
# a DIRECTIVE, and an unparseable directive is itself an error (SC1072/SC1073). A hook that
|
|
1241
1377
|
# explains why it skips a shell dialect must not break the checker while doing it.
|
|
1242
1378
|
if command -v shellcheck >/dev/null 2>&1; then
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1379
|
+
(
|
|
1380
|
+
# POSIX pipelines report only the LAST command's status, so discovery tallies progress in
|
|
1381
|
+
# files \u2014 one dot per decision, counted with wc -c after the pipeline \u2014 rather than
|
|
1382
|
+
# streaming through it: a pipeline that dies halfway cannot then pass as complete coverage,
|
|
1383
|
+
# and the same tallies carry the counts to the reporting below. The classifier writes the
|
|
1384
|
+
# tallies; this half only reads them.
|
|
1385
|
+
# NUL delimiters preserve filenames; positional arguments avoid xargs -I size limits and
|
|
1386
|
+
# interpreting filenames as shell code (never -I{}). The subshell confines cleanup to
|
|
1387
|
+
# this step. The classifier itself is the tracked, shebanged helper beside this hook, so
|
|
1388
|
+
# the discovery it performs finds and checks it too \u2014 the gate covers its own classifier.
|
|
1389
|
+
shellcheck_tmp=$(mktemp -d) || exit 1
|
|
1390
|
+
trap 'rm -rf "$shellcheck_tmp"' 0
|
|
1391
|
+
trap 'exit 1' 1 2 3 15
|
|
1392
|
+
git ls-files -z > "$shellcheck_tmp/tracked" || {
|
|
1393
|
+
echo "etymd: cannot enumerate tracked files for shellcheck" >&2
|
|
1256
1394
|
exit 1
|
|
1257
1395
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1396
|
+
: > "$shellcheck_tmp/scripts" && : > "$shellcheck_tmp/count" && : > "$shellcheck_tmp/zsh-count" && : > "$shellcheck_tmp/skip-count" || exit 1
|
|
1397
|
+
xargs -0 "$(dirname "$0")/discover-shell-scripts.sh" "$shellcheck_tmp" < "$shellcheck_tmp/tracked" || {
|
|
1398
|
+
echo "etymd: shell script discovery failed; shellcheck coverage is incomplete" >&2
|
|
1399
|
+
exit 1
|
|
1400
|
+
}
|
|
1401
|
+
count=$(wc -c < "$shellcheck_tmp/count") || exit 1
|
|
1402
|
+
zsh_count=$(wc -c < "$shellcheck_tmp/zsh-count") || exit 1
|
|
1403
|
+
skip_count=$(wc -c < "$shellcheck_tmp/skip-count") || exit 1
|
|
1404
|
+
if [ "$skip_count" -gt 0 ]; then
|
|
1405
|
+
echo "\u203A shellcheck: $((skip_count)) tracked path(s) with nothing readable behind them (submodule, dangling symlink, or deleted from the worktree) \u2014 not checked, not failed"
|
|
1265
1406
|
fi
|
|
1266
|
-
|
|
1407
|
+
if [ "$zsh_count" -gt 0 ]; then
|
|
1408
|
+
echo "\u203A shellcheck: $((zsh_count)) zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
|
|
1409
|
+
fi
|
|
1410
|
+
if [ "$count" -gt 0 ]; then
|
|
1411
|
+
echo "\u203A shellcheck ($((count)) scripts, blocking at severity=warning)"
|
|
1412
|
+
xargs -0 shellcheck -S warning -- < "$shellcheck_tmp/scripts" || {
|
|
1413
|
+
echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
|
|
1414
|
+
exit 1
|
|
1415
|
+
}
|
|
1416
|
+
# Everything below the blocking bar, shown once the push is already cleared. Never affects
|
|
1417
|
+
# the exit code \u2014 advice that can fail a push is not advice.
|
|
1418
|
+
advice=$(xargs -0 shellcheck -S style -f gcc -- < "$shellcheck_tmp/scripts" 2>/dev/null \\
|
|
1419
|
+
| grep -v ': warning:\\|: error:' || true)
|
|
1420
|
+
if [ -n "$advice" ]; then
|
|
1421
|
+
echo " \xB7 style/info (not blocking):"
|
|
1422
|
+
printf '%s\\n' "$advice" | sed 's/^/ /'
|
|
1423
|
+
fi
|
|
1424
|
+
fi
|
|
1425
|
+
) || exit 1
|
|
1267
1426
|
else
|
|
1268
1427
|
echo "\u203A shellcheck skipped (not on PATH) \u2014 install it to gate this repo's shell scripts"
|
|
1269
1428
|
fi`;
|
|
@@ -1426,6 +1585,7 @@ async function planWorkflow(root, facts, opts) {
|
|
|
1426
1585
|
};
|
|
1427
1586
|
if (opts.agents) {
|
|
1428
1587
|
await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
|
|
1588
|
+
await add("CLAUDE.md", generateClaudePointerMd(), "Claude Code pointer to AGENTS.md");
|
|
1429
1589
|
}
|
|
1430
1590
|
if (opts.gates) {
|
|
1431
1591
|
const existingPrePush = await readText(path2.join(root, ".githooks", "pre-push"));
|
|
@@ -1449,12 +1609,24 @@ async function planWorkflow(root, facts, opts) {
|
|
|
1449
1609
|
commitFormat: opts.gateConfig?.commitFormat,
|
|
1450
1610
|
allowWriting: opts.gateConfig?.allowWriting ?? []
|
|
1451
1611
|
};
|
|
1612
|
+
const tier = deriveFailOn(facts, {
|
|
1613
|
+
failOn: gateConfig.failOn,
|
|
1614
|
+
explicit: opts.gateFailOnPinned ?? false
|
|
1615
|
+
});
|
|
1452
1616
|
await add(
|
|
1453
1617
|
".githooks/pre-push",
|
|
1454
|
-
generatePrePushHook(facts, gateConfig, selfBuild),
|
|
1618
|
+
generatePrePushHook(facts, { ...gateConfig, failOn: tier.failOn }, selfBuild),
|
|
1455
1619
|
"Correctness gate (pre-push)",
|
|
1456
1620
|
true
|
|
1457
1621
|
);
|
|
1622
|
+
if (facts.shell?.scripts) {
|
|
1623
|
+
await add(
|
|
1624
|
+
".githooks/discover-shell-scripts.sh",
|
|
1625
|
+
generateShellDiscoveryScript(),
|
|
1626
|
+
"Shell discovery (pre-push shellcheck helper)",
|
|
1627
|
+
true
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1458
1630
|
if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
|
|
1459
1631
|
await add(
|
|
1460
1632
|
"scripts/artifact-check.sh",
|
|
@@ -1889,7 +2061,7 @@ async function localHookTools(root, facts, scripts) {
|
|
|
1889
2061
|
const inertCompanions = [];
|
|
1890
2062
|
const readHook = async (name) => {
|
|
1891
2063
|
if (!hooks.dir) return empty;
|
|
1892
|
-
const text = await readText(path2.
|
|
2064
|
+
const text = await readText(path2.resolve(root, hooks.dir, name));
|
|
1893
2065
|
if (!text) return empty;
|
|
1894
2066
|
const tools = new Set(matchTools(text, scripts));
|
|
1895
2067
|
const companion = await companionOf(root, hooks.dir, name, text, scripts);
|
|
@@ -2038,7 +2210,7 @@ async function buildGateInventory(root, facts) {
|
|
|
2038
2210
|
|
|
2039
2211
|
// src/lenses/gate-integrity/screener.ts
|
|
2040
2212
|
init_util();
|
|
2041
|
-
var
|
|
2213
|
+
var pExecFile3 = promisify(execFile);
|
|
2042
2214
|
var HOOK_FILES = ["pre-commit", "pre-push", "commit-msg"];
|
|
2043
2215
|
var SCREEN_CALL_RE = /"\$GATE"\s+screen\b/;
|
|
2044
2216
|
var DEV_BUILD_ARM = "[ -x ./dist/cli.js ]";
|
|
@@ -2055,7 +2227,7 @@ async function probeScreener(root, facts) {
|
|
|
2055
2227
|
const doors = [];
|
|
2056
2228
|
let devBuildArm = false;
|
|
2057
2229
|
for (const name of HOOK_FILES) {
|
|
2058
|
-
const text = await readText(path2.
|
|
2230
|
+
const text = await readText(path2.resolve(root, dir, name));
|
|
2059
2231
|
if (!text || !SCREEN_CALL_RE.test(text)) continue;
|
|
2060
2232
|
doors.push(`${dir}/${name}`);
|
|
2061
2233
|
if (text.includes(DEV_BUILD_ARM)) devBuildArm = true;
|
|
@@ -2075,7 +2247,7 @@ async function probeScreener(root, facts) {
|
|
|
2075
2247
|
source = "path";
|
|
2076
2248
|
}
|
|
2077
2249
|
try {
|
|
2078
|
-
await
|
|
2250
|
+
await pExecFile3(runner, ["screen", "--help"], { cwd: root, timeout: 1e4 });
|
|
2079
2251
|
return { present: true, doors, runner, source, answersScreen: true };
|
|
2080
2252
|
} catch (err) {
|
|
2081
2253
|
const e = err;
|
|
@@ -2753,10 +2925,13 @@ function extractCommandClaims(text) {
|
|
|
2753
2925
|
return { scripts, filteredSkipped };
|
|
2754
2926
|
}
|
|
2755
2927
|
var PATH_TOKEN_RE = /^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.$-]+)+\/?$/;
|
|
2928
|
+
var HOSTNAME_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.([a-z]{2,})$/i;
|
|
2756
2929
|
var KNOWN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2757
2930
|
..."ts tsx cts mts js jsx cjs mjs json jsonc json5 md mdx mdc yml yaml toml ini cfg conf env sh bash zsh fish ps1 bat cmd css scss sass less html htm xml svg sql prisma graphql gql proto py rb rs go java kt kts swift c h cc cpp hpp cs php vue svelte astro txt log lock csv tsv png jpg jpeg gif webp ico avif woff woff2 ttf otf wasm map pem key crt tf tfvars example sample local snap ejs hbs pug".split(" ")
|
|
2758
2931
|
]);
|
|
2759
2932
|
var CREATION_CONTEXT_RE = /\b(?:creat(?:e|es|ed|ing)|generat(?:e|es|ed|ing)|scaffold(?:s|ed|ing)?|quarantin(?:e|es|ed|ing)|(?:writ(?:e|es|ten|ing)|output(?:s|ted)?|emit(?:s|ted|ting)?|sav(?:e|es|ed|ing)|mov(?:e|es|ed|ing)|copy|copi(?:es|ed))\s+(?:it\s+|them\s+)?(?:to|into)|new\s+(?:file|directory|folder)|will\s+(?:be\s+)?(?:created|generated|written)|add(?:s|ed|ing)?\s+(?:a|the)\s+new)\b/i;
|
|
2933
|
+
var FETCH_CONTEXT_RE = /\b(?:fetch(?:es|ed|ing)?|pull(?:s|ed|ing)?|clone(?:s|d)?|download(?:s|ed|ing)?)\b/i;
|
|
2934
|
+
var EXTERNAL_URL_RE = /https?:\/\/|\bwww\./i;
|
|
2760
2935
|
var PLACEHOLDER_SEGMENTS = /* @__PURE__ */ new Set(["placeholder", "foo", "bar", "baz", "qux"]);
|
|
2761
2936
|
var PLACEHOLDER_PREFIX_RE = /^(?:my|your)-/i;
|
|
2762
2937
|
function isPlaceholderClaim(token) {
|
|
@@ -2790,14 +2965,21 @@ ${line}`;
|
|
|
2790
2965
|
}
|
|
2791
2966
|
function extractPathClaims(text, opts = {}) {
|
|
2792
2967
|
const prospectiveOnly = /* @__PURE__ */ new Map();
|
|
2968
|
+
const fetchedOnly = /* @__PURE__ */ new Map();
|
|
2793
2969
|
const namespacedOnly = /* @__PURE__ */ new Map();
|
|
2794
2970
|
const placeholder = /* @__PURE__ */ new Set();
|
|
2971
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
2795
2972
|
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
2796
2973
|
const token = m[1].trim();
|
|
2797
2974
|
if (token.includes(" ") || token.length > 120) continue;
|
|
2798
2975
|
if (token.startsWith("/") || token.startsWith("~") || token.startsWith("@") || token.startsWith("$"))
|
|
2799
2976
|
continue;
|
|
2800
2977
|
if (token.includes("://") || token.startsWith("www.")) continue;
|
|
2978
|
+
if (token.includes("/")) {
|
|
2979
|
+
const firstSegment = token.split("/")[0] ?? token;
|
|
2980
|
+
const hostSuffix = HOSTNAME_RE.exec(firstSegment)?.[1]?.toLowerCase();
|
|
2981
|
+
if (hostSuffix && !KNOWN_EXTENSIONS.has(hostSuffix)) continue;
|
|
2982
|
+
}
|
|
2801
2983
|
if (/[*?{}<>|]/.test(token)) continue;
|
|
2802
2984
|
if (token.includes("@")) continue;
|
|
2803
2985
|
if (!PATH_TOKEN_RE.test(token)) continue;
|
|
@@ -2810,8 +2992,12 @@ function extractPathClaims(text, opts = {}) {
|
|
|
2810
2992
|
placeholder.add(claim);
|
|
2811
2993
|
continue;
|
|
2812
2994
|
}
|
|
2813
|
-
|
|
2995
|
+
if (isDirClaim) dirs.add(claim);
|
|
2996
|
+
const context = claimContext(text, m.index ?? 0);
|
|
2997
|
+
const prospective2 = CREATION_CONTEXT_RE.test(context);
|
|
2814
2998
|
prospectiveOnly.set(claim, (prospectiveOnly.get(claim) ?? true) && prospective2);
|
|
2999
|
+
const fetched2 = FETCH_CONTEXT_RE.test(context) && EXTERNAL_URL_RE.test(context);
|
|
3000
|
+
fetchedOnly.set(claim, (fetchedOnly.get(claim) ?? true) && fetched2);
|
|
2815
3001
|
if (opts.namespaces) {
|
|
2816
3002
|
const nsLead = new RegExp(`(${NAMESPACE_IDENT}):[ \\t]*$`).exec(text.slice(0, m.index ?? 0));
|
|
2817
3003
|
const prefixed = isNamespace(nsLead?.[1]);
|
|
@@ -2820,13 +3006,15 @@ function extractPathClaims(text, opts = {}) {
|
|
|
2820
3006
|
}
|
|
2821
3007
|
const paths = [];
|
|
2822
3008
|
const prospective = [];
|
|
3009
|
+
const fetched = [];
|
|
2823
3010
|
const namespaced = [];
|
|
2824
3011
|
for (const [claim, only] of prospectiveOnly) {
|
|
2825
|
-
if (
|
|
3012
|
+
if (fetchedOnly.get(claim)) fetched.push(claim);
|
|
3013
|
+
else if (only) prospective.push(claim);
|
|
2826
3014
|
else if (namespacedOnly.get(claim)) namespaced.push(claim);
|
|
2827
3015
|
else paths.push(claim);
|
|
2828
3016
|
}
|
|
2829
|
-
return { paths, prospective, namespaced, placeholder: [...placeholder] };
|
|
3017
|
+
return { paths, dirs: [...dirs], prospective, fetched, namespaced, placeholder: [...placeholder] };
|
|
2830
3018
|
}
|
|
2831
3019
|
var LOCAL_REF_LEADINS = new Set(
|
|
2832
3020
|
"decision decisions entry entries ruling rulings record records ledger id ids item items see per in of on at by to as is was are were the a an and or but not with under over from via vs than after before since between through against latest newest earliest only also still now supersedes superseded superseding amends amended extends extended cites cited citing adds added adding wrote written writes locked locks closed closes opened opens resolves resolved reopened recorded number numbers".split(" ")
|
|
@@ -2898,12 +3086,14 @@ async function buildTruthEnv(root, facts) {
|
|
|
2898
3086
|
for (const key of Object.keys(pkgJson?.scripts ?? {})) knownScripts.add(key);
|
|
2899
3087
|
}
|
|
2900
3088
|
const bases = [root, ...facts.packages.map((p) => path2.join(root, p.dir))];
|
|
2901
|
-
const pathResolves = async (claim) => {
|
|
3089
|
+
const pathResolves = async (claim, fromDir) => {
|
|
2902
3090
|
for (const base of bases) {
|
|
2903
3091
|
if (await pathExists(path2.join(base, claim))) return true;
|
|
2904
3092
|
if (await pathExists(path2.join(base, "src", claim))) return true;
|
|
2905
3093
|
if (await pathExists(path2.join(base, "scripts", claim))) return true;
|
|
2906
3094
|
}
|
|
3095
|
+
if (fromDir && fromDir !== "." && await pathExists(path2.join(root, fromDir, claim)))
|
|
3096
|
+
return true;
|
|
2907
3097
|
return false;
|
|
2908
3098
|
};
|
|
2909
3099
|
const binResolves = async (name) => {
|
|
@@ -2933,6 +3123,7 @@ function emptyCounters() {
|
|
|
2933
3123
|
unverifiableCommands: 0,
|
|
2934
3124
|
gitignoredSkipped: 0,
|
|
2935
3125
|
prospectiveSkipped: 0,
|
|
3126
|
+
fetchedSkipped: 0,
|
|
2936
3127
|
placeholderSkipped: 0,
|
|
2937
3128
|
qualifiedRefsSkipped: 0,
|
|
2938
3129
|
unresolvableRefs: 0,
|
|
@@ -2975,19 +3166,48 @@ async function checkTextClaims(env, file, opts, counters) {
|
|
|
2975
3166
|
confidence: "high"
|
|
2976
3167
|
});
|
|
2977
3168
|
}
|
|
2978
|
-
const { paths, prospective, placeholder, namespaced } = extractPathClaims(
|
|
2979
|
-
|
|
2980
|
-
|
|
3169
|
+
const { paths, dirs, prospective, fetched, placeholder, namespaced } = extractPathClaims(
|
|
3170
|
+
file.text,
|
|
3171
|
+
{ namespaces: opts.treatNamespacedPrefixes }
|
|
3172
|
+
);
|
|
2981
3173
|
counters.prospectiveSkipped += prospective.length;
|
|
3174
|
+
counters.fetchedSkipped += fetched.length;
|
|
2982
3175
|
counters.placeholderSkipped += placeholder.length;
|
|
2983
3176
|
counters.namespacedSkipped += namespaced.length;
|
|
2984
|
-
const
|
|
3177
|
+
const fromDir = path2.dirname(file.path);
|
|
3178
|
+
const unresolved = [];
|
|
2985
3179
|
for (const claim of paths) {
|
|
2986
|
-
if (await env.pathResolves(claim
|
|
3180
|
+
if (await env.pathResolves(claim, fromDir))
|
|
3181
|
+
examined.push({ kind: "path", value: claim, exists: true });
|
|
3182
|
+
else unresolved.push(claim);
|
|
3183
|
+
}
|
|
3184
|
+
const proseBases = /* @__PURE__ */ new Set();
|
|
3185
|
+
for (const dirClaim of dirs) {
|
|
3186
|
+
if (await env.pathResolves(dirClaim, fromDir)) {
|
|
3187
|
+
proseBases.add(dirClaim);
|
|
3188
|
+
proseBases.add(path2.dirname(dirClaim));
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
const missing = [];
|
|
3192
|
+
for (const claim of unresolved) {
|
|
3193
|
+
let anchored = false;
|
|
3194
|
+
for (const base of proseBases) {
|
|
3195
|
+
if (await pathExists(path2.join(env.root, base, claim))) anchored = true;
|
|
3196
|
+
else if (fromDir !== "." && await pathExists(path2.join(env.root, fromDir, base, claim)))
|
|
3197
|
+
anchored = true;
|
|
3198
|
+
if (anchored) break;
|
|
3199
|
+
}
|
|
3200
|
+
if (anchored) examined.push({ kind: "path", value: claim, exists: true });
|
|
2987
3201
|
else missing.push(claim);
|
|
2988
3202
|
}
|
|
2989
3203
|
const ignoredOut = missing.length ? await git(env.root, ["check-ignore", ...missing]) : null;
|
|
2990
3204
|
const gitignored = new Set((ignoredOut ?? "").split("\n").filter(Boolean));
|
|
3205
|
+
const unproven = missing.filter((claim) => !gitignored.has(claim));
|
|
3206
|
+
if (unproven.length) {
|
|
3207
|
+
const dirOut = await git(env.root, ["check-ignore", ...unproven.map((claim) => `${claim}/`)]);
|
|
3208
|
+
for (const line of (dirOut ?? "").split("\n").filter(Boolean))
|
|
3209
|
+
gitignored.add(line.replace(/\/$/, ""));
|
|
3210
|
+
}
|
|
2991
3211
|
let pathFindings = 0;
|
|
2992
3212
|
for (const claim of missing) {
|
|
2993
3213
|
if (opts.rootedFirstSegments && !opts.rootedFirstSegments.has(claim.split("/")[0] ?? claim)) {
|
|
@@ -3275,6 +3495,11 @@ var instructionTruthLens = {
|
|
|
3275
3495
|
`${counters.prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
|
|
3276
3496
|
);
|
|
3277
3497
|
}
|
|
3498
|
+
if (counters.fetchedSkipped) {
|
|
3499
|
+
disclosures.push(
|
|
3500
|
+
`${counters.fetchedSkipped} path claim(s) sit beside the URL they are fetched from \u2014 another tree's files, unverifiable here; skipped, not flagged.`
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3278
3503
|
if (counters.placeholderSkipped) {
|
|
3279
3504
|
disclosures.push(
|
|
3280
3505
|
`${counters.placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
|
|
@@ -3312,7 +3537,7 @@ var instructionTruthLens = {
|
|
|
3312
3537
|
);
|
|
3313
3538
|
}
|
|
3314
3539
|
disclosures.push(
|
|
3315
|
-
`Checked ${files.length} instruction file(s); commands resolved against root + ${facts.packages.length} workspace manifest(s) plus installed binaries; paths matched against root and
|
|
3540
|
+
`Checked ${files.length} instruction file(s); commands resolved against root + ${facts.packages.length} workspace manifest(s) plus installed binaries; paths matched against root, package roots, the claiming file's directory, and directories the same file names; existence judged on the working tree (gitignored-but-present is true, gitignored-and-absent unverifiable). Heuristics: workspace-filtered commands skipped (${counters.filteredSkipped}); tokens without a recognized extension treated as prose (a dir claim needs a trailing slash); schemeless hosts read as URLs; gitignored claims unverifiable; create-this, fetched-from-URL, and stand-in path claims skipped; absolute/globbed/placeholder tokens skipped; doc mentions inside \`~/\` home paths skipped; framework-pattern staleness not checked.`
|
|
3316
3541
|
);
|
|
3317
3542
|
return {
|
|
3318
3543
|
lens: LENS_ID4,
|
|
@@ -3743,6 +3968,7 @@ async function loadFleetManifest(manifestPath) {
|
|
|
3743
3968
|
|
|
3744
3969
|
// src/engine/fleet.ts
|
|
3745
3970
|
init_config();
|
|
3971
|
+
init_detect();
|
|
3746
3972
|
init_facts();
|
|
3747
3973
|
init_util();
|
|
3748
3974
|
|
|
@@ -3871,7 +4097,7 @@ function parseMilestones(text) {
|
|
|
3871
4097
|
}
|
|
3872
4098
|
|
|
3873
4099
|
// src/engine/fleet.ts
|
|
3874
|
-
var
|
|
4100
|
+
var pExecFile4 = promisify(execFile);
|
|
3875
4101
|
var FLEET_LENS = "fleet-manifest";
|
|
3876
4102
|
var FLEET_JSON_SCHEMA = "fleet-experimental-0.2";
|
|
3877
4103
|
var GUARDED_WALL_ARTIFACTS = ["PROJECT_CONTEXT.md", "DECISIONS.md"];
|
|
@@ -3926,7 +4152,7 @@ function stateBudgetsFor(entry) {
|
|
|
3926
4152
|
async function gitGrepFiles(root, needle, mode = "fixed") {
|
|
3927
4153
|
try {
|
|
3928
4154
|
const matchFlag = mode === "regex" ? "-E" : "-F";
|
|
3929
|
-
const { stdout } = await
|
|
4155
|
+
const { stdout } = await pExecFile4("git", ["grep", "-I", "-l", matchFlag, "-e", needle], {
|
|
3930
4156
|
cwd: root,
|
|
3931
4157
|
timeout: 8e3
|
|
3932
4158
|
});
|
|
@@ -4307,8 +4533,13 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
4307
4533
|
);
|
|
4308
4534
|
continue;
|
|
4309
4535
|
}
|
|
4310
|
-
const { config } = await readConfig2(root);
|
|
4311
|
-
const planned = (await planWorkflow2(root, facts, {
|
|
4536
|
+
const { config, explicit } = await readConfig2(root);
|
|
4537
|
+
const planned = (await planWorkflow2(root, facts, {
|
|
4538
|
+
agents: false,
|
|
4539
|
+
gates: true,
|
|
4540
|
+
gateConfig: config.gates,
|
|
4541
|
+
gateFailOnPinned: explicit.gatesFailOn
|
|
4542
|
+
})).filter((f) => f.executable);
|
|
4312
4543
|
const missing = planned.filter((f) => !f.exists).map((f) => f.path);
|
|
4313
4544
|
const differing = planned.filter((f) => f.exists && f.differs);
|
|
4314
4545
|
const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
|
|
@@ -4344,6 +4575,31 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
4344
4575
|
}
|
|
4345
4576
|
}
|
|
4346
4577
|
}
|
|
4578
|
+
async function checkClaudePointers(manifest, findings) {
|
|
4579
|
+
for (const entry of manifest.entries) {
|
|
4580
|
+
const root = entry.resolvedRoot;
|
|
4581
|
+
if (!root || !await isDirectory(root)) continue;
|
|
4582
|
+
const check = await checkClaudePointer(root);
|
|
4583
|
+
if (check.ok) continue;
|
|
4584
|
+
findings.push(
|
|
4585
|
+
check.kind === "no-import" ? finding3(
|
|
4586
|
+
`${FLEET_LENS}/claude-pointer-missing:${entry.name}`,
|
|
4587
|
+
"risk",
|
|
4588
|
+
`\`${entry.name}\` has a CLAUDE.md that never imports its AGENTS.md`,
|
|
4589
|
+
[`${entry.name}: ${check.detail}`],
|
|
4590
|
+
"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.",
|
|
4591
|
+
"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."
|
|
4592
|
+
) : finding3(
|
|
4593
|
+
`${FLEET_LENS}/claude-pointer-missing:${entry.name}`,
|
|
4594
|
+
"gap",
|
|
4595
|
+
`\`${entry.name}\` relies on the AGENTS.md fallback, which the installed Claude Code predates`,
|
|
4596
|
+
[`${entry.name}: ${check.detail}`],
|
|
4597
|
+
`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.`,
|
|
4598
|
+
"Update Claude Code, or create a CLAUDE.md beside AGENTS.md whose only import line is `@AGENTS.md`."
|
|
4599
|
+
)
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4347
4603
|
async function collectWallFindings(manifest) {
|
|
4348
4604
|
const findings = [];
|
|
4349
4605
|
const disclosures = [];
|
|
@@ -4353,6 +4609,7 @@ async function collectWallFindings(manifest) {
|
|
|
4353
4609
|
await checkHygieneNeedles(manifest, findings, disclosures);
|
|
4354
4610
|
await checkGuardedEmails(manifest, findings, disclosures);
|
|
4355
4611
|
await checkGateDrift(manifest, findings, disclosures);
|
|
4612
|
+
await checkClaudePointers(manifest, findings);
|
|
4356
4613
|
return { findings, disclosures };
|
|
4357
4614
|
}
|
|
4358
4615
|
function stateAgeDays(facts) {
|
|
@@ -4699,7 +4956,6 @@ var STOP_WORDS = new Set(
|
|
|
4699
4956
|
);
|
|
4700
4957
|
var WRAP_RE = /^([([{"']*)(.*?)([.,;:!?)\]}"']*)$/;
|
|
4701
4958
|
var TRAIL_RE = /[.,;:!?)\]}"']*$/;
|
|
4702
|
-
var HOSTNAME_RE = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.([a-z]{2,})$/i;
|
|
4703
4959
|
function promoteBareTokens(text, ctx) {
|
|
4704
4960
|
const skips = {
|
|
4705
4961
|
bareInvocations: 0,
|
|
@@ -4887,6 +5143,11 @@ async function runPremise(opts) {
|
|
|
4887
5143
|
`${counters.prospectiveSkipped} path(s) sit in create-this prose (the task says to create them) \u2014 forward-looking, not missing; skipped.`
|
|
4888
5144
|
);
|
|
4889
5145
|
}
|
|
5146
|
+
if (counters.fetchedSkipped) {
|
|
5147
|
+
disclosures.push(
|
|
5148
|
+
`${counters.fetchedSkipped} path(s) are named beside the URL they are fetched from \u2014 another tree, not this repo; skipped, not flagged.`
|
|
5149
|
+
);
|
|
5150
|
+
}
|
|
4890
5151
|
if (counters.placeholderSkipped) {
|
|
4891
5152
|
disclosures.push(
|
|
4892
5153
|
`${counters.placeholderSkipped} path(s) are naming stand-ins (e.g. \`my-feature\`) rather than real references; skipped.`
|