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
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { PACK_VERSION } from './chunk-YQZDYDAK.js';
|
|
4
|
-
import { readText, pathExists } from './chunk-4VPBP6K6.js';
|
|
5
|
-
import path from 'path';
|
|
2
|
+
import { PACK_VERSION } from './chunk-U7BCBJIH.js';
|
|
6
3
|
import { createHash } from 'crypto';
|
|
7
4
|
|
|
8
5
|
var GENERATION_MARKER_RE = /^(?:# |<!-- )etymd:generated pack-v\S+ ([0-9a-f]{16})(?: -->)?$/;
|
|
@@ -128,6 +125,19 @@ ${[
|
|
|
128
125
|
].filter(Boolean).join("\n") || "# add the project's key commands"}
|
|
129
126
|
\`\`\`
|
|
130
127
|
|
|
128
|
+
`,
|
|
129
|
+
"md"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function generateClaudePointerMd() {
|
|
133
|
+
return stampGenerated(
|
|
134
|
+
`# CLAUDE.md
|
|
135
|
+
|
|
136
|
+
Single source of truth for agent instructions lives in \`AGENTS.md\`. This pointer keeps Claude
|
|
137
|
+
Code aligned with every other agent \u2014 edit \`AGENTS.md\`, not this file.
|
|
138
|
+
|
|
139
|
+
@AGENTS.md
|
|
140
|
+
|
|
131
141
|
`,
|
|
132
142
|
"md"
|
|
133
143
|
);
|
|
@@ -239,42 +249,118 @@ ${localHookCall("commit-msg")}
|
|
|
239
249
|
exit 0
|
|
240
250
|
`);
|
|
241
251
|
}
|
|
252
|
+
function generateShellDiscoveryScript() {
|
|
253
|
+
return stampGenerated(`#!/usr/bin/env sh
|
|
254
|
+
# etymd: shell script discovery for the pre-push shellcheck step. Arguments: the scratch
|
|
255
|
+
# directory the hook created, then the tracked paths to classify (NUL-delimited on the hook's
|
|
256
|
+
# side, positional here). Verdicts land in the scratch: scripts (NUL-delimited matches) and one
|
|
257
|
+
# dot per decision into count / zsh-count / skip-count, tallied by the hook after the pipeline.
|
|
258
|
+
#
|
|
259
|
+
# A path with nothing readable behind it \u2014 a submodule entry, a dangling symlink, a file
|
|
260
|
+
# deleted from the worktree while still tracked \u2014 cannot lie about its contents, so it is a
|
|
261
|
+
# disclosed skip, never a block: an absent worktree file is routine dirty state. A regular file
|
|
262
|
+
# that EXISTS but cannot be read is the other branch \u2014 coverage would silently shrink, so it
|
|
263
|
+
# fails, naming the path.
|
|
264
|
+
#
|
|
265
|
+
# The two \`[ "$?" -eq 1 ]\` guards are the match/error protocol: grep reports "no match" as 1
|
|
266
|
+
# and a failure as 2 or more, and only the first is a verdict. Dropping the guard would let a
|
|
267
|
+
# failing matcher pass as "not a shell script" \u2014 the exact silent coverage-shrink the
|
|
268
|
+
# fail-closed rules exist to prevent. The checker does not associate \`$?\` with the enclosing
|
|
269
|
+
# if-condition, which is why this shape survives the pass it serves; keep it that way.
|
|
270
|
+
work=$1
|
|
271
|
+
shift
|
|
272
|
+
for file do
|
|
273
|
+
if [ ! -f "./$file" ]; then
|
|
274
|
+
printf . >> "$work/skip-count" || exit 1
|
|
275
|
+
continue
|
|
276
|
+
fi
|
|
277
|
+
# 4096 bytes bound the read \u2014 a binary with no newline would otherwise be copied whole
|
|
278
|
+
# into the scratch on every push. The second head restores line-1-only semantics, so a
|
|
279
|
+
# shebang embedded on a LATER line of a document cannot match the patterns below.
|
|
280
|
+
head -c 4096 "./$file" > "$work/head-bytes" || {
|
|
281
|
+
echo "etymd: cannot read tracked file for shellcheck: $file" >&2
|
|
282
|
+
exit 1
|
|
283
|
+
}
|
|
284
|
+
head -n 1 "$work/head-bytes" > "$work/first-line" || exit 1
|
|
285
|
+
if grep -qE "^#!.*[/ ](ba|da)?sh( |$)" "$work/first-line"; then
|
|
286
|
+
printf "./%s\\0" "$file" >> "$work/scripts" || exit 1
|
|
287
|
+
printf . >> "$work/count" || exit 1
|
|
288
|
+
else
|
|
289
|
+
[ "$?" -eq 1 ] || exit 1
|
|
290
|
+
if grep -qE "^#!.*[/ ]zsh( |$)" "$work/first-line"; then
|
|
291
|
+
printf . >> "$work/zsh-count" || exit 1
|
|
292
|
+
else
|
|
293
|
+
[ "$?" -eq 1 ] || exit 1
|
|
294
|
+
fi
|
|
295
|
+
fi
|
|
296
|
+
done
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
242
299
|
function shellcheckStep() {
|
|
243
300
|
return `
|
|
244
301
|
# Shell correctness. Scripts are discovered by shebang over TRACKED files at push time, so a
|
|
245
|
-
# script added later is covered without regenerating this hook.
|
|
302
|
+
# script added later is covered without regenerating this hook. The classifier is
|
|
303
|
+
# discover-shell-scripts.sh beside this hook \u2014 tracked and shebanged like what it classifies,
|
|
304
|
+
# so the scan it implements finds and checks it too. zsh is NOT in the checked set:
|
|
246
305
|
# the checker cannot parse it (SC1071 is a parser-level error no inline directive can silence),
|
|
247
306
|
# so checking it would fail every push on the parser, not on the script. Excluded \u2014 and said so
|
|
248
307
|
# at run time below, because a coverage hole that is silent is indistinguishable from coverage.
|
|
308
|
+
# The same honesty splits the unreadable: a regular file that exists but cannot be read blocks
|
|
309
|
+
# the push (coverage would otherwise silently shrink), while a tracked path with nothing readable
|
|
310
|
+
# behind it \u2014 a submodule entry, a dangling symlink, a file deleted from the worktree while
|
|
311
|
+
# still tracked \u2014 is counted and said so below as skipped, never a block.
|
|
249
312
|
#
|
|
250
313
|
# "the checker", not its name, on purpose: a comment whose first word is that name is read as
|
|
251
314
|
# a DIRECTIVE, and an unparseable directive is itself an error (SC1072/SC1073). A hook that
|
|
252
315
|
# explains why it skips a shell dialect must not break the checker while doing it.
|
|
253
316
|
if command -v shellcheck >/dev/null 2>&1; then
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
317
|
+
(
|
|
318
|
+
# POSIX pipelines report only the LAST command's status, so discovery tallies progress in
|
|
319
|
+
# files \u2014 one dot per decision, counted with wc -c after the pipeline \u2014 rather than
|
|
320
|
+
# streaming through it: a pipeline that dies halfway cannot then pass as complete coverage,
|
|
321
|
+
# and the same tallies carry the counts to the reporting below. The classifier writes the
|
|
322
|
+
# tallies; this half only reads them.
|
|
323
|
+
# NUL delimiters preserve filenames; positional arguments avoid xargs -I size limits and
|
|
324
|
+
# interpreting filenames as shell code (never -I{}). The subshell confines cleanup to
|
|
325
|
+
# this step. The classifier itself is the tracked, shebanged helper beside this hook, so
|
|
326
|
+
# the discovery it performs finds and checks it too \u2014 the gate covers its own classifier.
|
|
327
|
+
shellcheck_tmp=$(mktemp -d) || exit 1
|
|
328
|
+
trap 'rm -rf "$shellcheck_tmp"' 0
|
|
329
|
+
trap 'exit 1' 1 2 3 15
|
|
330
|
+
git ls-files -z > "$shellcheck_tmp/tracked" || {
|
|
331
|
+
echo "etymd: cannot enumerate tracked files for shellcheck" >&2
|
|
267
332
|
exit 1
|
|
268
333
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
334
|
+
: > "$shellcheck_tmp/scripts" && : > "$shellcheck_tmp/count" && : > "$shellcheck_tmp/zsh-count" && : > "$shellcheck_tmp/skip-count" || exit 1
|
|
335
|
+
xargs -0 "$(dirname "$0")/discover-shell-scripts.sh" "$shellcheck_tmp" < "$shellcheck_tmp/tracked" || {
|
|
336
|
+
echo "etymd: shell script discovery failed; shellcheck coverage is incomplete" >&2
|
|
337
|
+
exit 1
|
|
338
|
+
}
|
|
339
|
+
count=$(wc -c < "$shellcheck_tmp/count") || exit 1
|
|
340
|
+
zsh_count=$(wc -c < "$shellcheck_tmp/zsh-count") || exit 1
|
|
341
|
+
skip_count=$(wc -c < "$shellcheck_tmp/skip-count") || exit 1
|
|
342
|
+
if [ "$skip_count" -gt 0 ]; then
|
|
343
|
+
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"
|
|
276
344
|
fi
|
|
277
|
-
|
|
345
|
+
if [ "$zsh_count" -gt 0 ]; then
|
|
346
|
+
echo "\u203A shellcheck: $((zsh_count)) zsh script(s) excluded \u2014 shellcheck cannot parse zsh (SC1071); not checked, not failed"
|
|
347
|
+
fi
|
|
348
|
+
if [ "$count" -gt 0 ]; then
|
|
349
|
+
echo "\u203A shellcheck ($((count)) scripts, blocking at severity=warning)"
|
|
350
|
+
xargs -0 shellcheck -S warning -- < "$shellcheck_tmp/scripts" || {
|
|
351
|
+
echo " fix, or justify inline with '# shellcheck disable=SCxxxx # why'"
|
|
352
|
+
exit 1
|
|
353
|
+
}
|
|
354
|
+
# Everything below the blocking bar, shown once the push is already cleared. Never affects
|
|
355
|
+
# the exit code \u2014 advice that can fail a push is not advice.
|
|
356
|
+
advice=$(xargs -0 shellcheck -S style -f gcc -- < "$shellcheck_tmp/scripts" 2>/dev/null \\
|
|
357
|
+
| grep -v ': warning:\\|: error:' || true)
|
|
358
|
+
if [ -n "$advice" ]; then
|
|
359
|
+
echo " \xB7 style/info (not blocking):"
|
|
360
|
+
printf '%s\\n' "$advice" | sed 's/^/ /'
|
|
361
|
+
fi
|
|
362
|
+
fi
|
|
363
|
+
) || exit 1
|
|
278
364
|
else
|
|
279
365
|
echo "\u203A shellcheck skipped (not on PATH) \u2014 install it to gate this repo's shell scripts"
|
|
280
366
|
fi`;
|
|
@@ -359,94 +445,4 @@ exit 0
|
|
|
359
445
|
`);
|
|
360
446
|
}
|
|
361
447
|
|
|
362
|
-
|
|
363
|
-
function derivedCommands(facts, existingHook) {
|
|
364
|
-
const c = facts.commands;
|
|
365
|
-
const base = [c.formatCheck, c.typecheck, c.lint].filter(
|
|
366
|
-
(k) => Boolean(k) && isSafeGateCommand(c.raw[k])
|
|
367
|
-
);
|
|
368
|
-
if (c.test && existingHook && new RegExp(`\\b${c.test}\\b`).test(existingHook)) {
|
|
369
|
-
base.push(c.test);
|
|
370
|
-
}
|
|
371
|
-
return base;
|
|
372
|
-
}
|
|
373
|
-
function riskReachability(facts) {
|
|
374
|
-
const reasons = [];
|
|
375
|
-
if (facts.publishRoute !== "none") {
|
|
376
|
-
reasons.push("an instruction file can name a package script that no longer exists");
|
|
377
|
-
}
|
|
378
|
-
if (facts.artifacts.some((a) => a.kind === "state" && a.exists)) {
|
|
379
|
-
reasons.push("a state doc can fall far enough behind the repo to escalate");
|
|
380
|
-
}
|
|
381
|
-
return reasons;
|
|
382
|
-
}
|
|
383
|
-
function deriveFailOn(facts, recorded) {
|
|
384
|
-
const reachable = riskReachability(facts);
|
|
385
|
-
if (recorded.explicit) return { failOn: recorded.failOn, source: "config", reachable };
|
|
386
|
-
if (recorded.failOn === "risk" && reachable.length === 0) {
|
|
387
|
-
return { failOn: "gap", source: "derived", reachable };
|
|
388
|
-
}
|
|
389
|
-
return { failOn: recorded.failOn, source: "default", reachable };
|
|
390
|
-
}
|
|
391
|
-
async function planWorkflow(root, facts, opts) {
|
|
392
|
-
const out = [];
|
|
393
|
-
const add = async (rel, contents, label, executable = false) => {
|
|
394
|
-
const abs = path.join(root, rel);
|
|
395
|
-
const exists = await pathExists(abs);
|
|
396
|
-
const existing = exists ? await readText(abs) : null;
|
|
397
|
-
const differs = exists ? existing !== contents : void 0;
|
|
398
|
-
const origin = differs && existing !== null ? fileOrigin(existing) : void 0;
|
|
399
|
-
out.push({
|
|
400
|
-
path: rel,
|
|
401
|
-
contents,
|
|
402
|
-
exists,
|
|
403
|
-
differs,
|
|
404
|
-
drift: origin === "pack" ? "stale" : origin,
|
|
405
|
-
executable,
|
|
406
|
-
label
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
if (opts.agents) {
|
|
410
|
-
await add("AGENTS.md", generateAgentsMd(facts), "Minimal operating contract (scaffold)");
|
|
411
|
-
}
|
|
412
|
-
if (opts.gates) {
|
|
413
|
-
const existingPrePush = await readText(path.join(root, ".githooks", "pre-push"));
|
|
414
|
-
const selfBuild = isSelfBuildRepo(facts);
|
|
415
|
-
await add(
|
|
416
|
-
".githooks/pre-commit",
|
|
417
|
-
generatePreCommitHook(selfBuild),
|
|
418
|
-
"Process gate (pre-commit)",
|
|
419
|
-
true
|
|
420
|
-
);
|
|
421
|
-
await add(
|
|
422
|
-
".githooks/commit-msg",
|
|
423
|
-
generateCommitMsgHook(opts.gateConfig),
|
|
424
|
-
"Message gate (content screen, format)",
|
|
425
|
-
true
|
|
426
|
-
);
|
|
427
|
-
const gateConfig = opts.gateConfig?.commands?.length ? opts.gateConfig : {
|
|
428
|
-
commands: derivedCommands(facts, existingPrePush ?? void 0),
|
|
429
|
-
failOn: opts.gateConfig?.failOn ?? DEFAULT_CONFIG.gates.failOn,
|
|
430
|
-
publishGate: opts.gateConfig?.publishGate,
|
|
431
|
-
commitFormat: opts.gateConfig?.commitFormat,
|
|
432
|
-
allowWriting: opts.gateConfig?.allowWriting ?? []
|
|
433
|
-
};
|
|
434
|
-
await add(
|
|
435
|
-
".githooks/pre-push",
|
|
436
|
-
generatePrePushHook(facts, gateConfig, selfBuild),
|
|
437
|
-
"Correctness gate (pre-push)",
|
|
438
|
-
true
|
|
439
|
-
);
|
|
440
|
-
if (opts.gateConfig?.publishGate ?? opts.publishGate ?? facts.publishable) {
|
|
441
|
-
await add(
|
|
442
|
-
"scripts/artifact-check.sh",
|
|
443
|
-
generateArtifactCheckScript(selfBuild),
|
|
444
|
-
"Content screen (published artifact)",
|
|
445
|
-
true
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return out;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
export { deriveFailOn, derivedCommands, isSafeGateCommand, planWorkflow, riskReachability, runPrefix };
|
|
448
|
+
export { fileOrigin, generateAgentsMd, generateArtifactCheckScript, generateClaudePointerMd, generateCommitMsgHook, generatePreCommitHook, generatePrePushHook, generateShellDiscoveryScript, isSafeGateCommand, isSelfBuildRepo, runPrefix };
|
|
@@ -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-
|
|
2
|
+
import { stateFreshnessLens, rankFindings, listInstructionFiles, buildTruthEnv, emptyCounters, packageManagerUsage, checkDocRefs, listStateDocuments, loadDecisionLedger, checkDecisionRefs, checkTextClaims } from './chunk-EYUW3UJU.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-
|
|
5
|
+
import { scanProject } from './chunk-DLHC7QON.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-
|
|
7
|
+
import { PACK_VERSION } from './chunk-U7BCBJIH.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';
|
|
@@ -316,7 +316,7 @@ async function localHookTools(root, facts, scripts) {
|
|
|
316
316
|
const inertCompanions = [];
|
|
317
317
|
const readHook = async (name) => {
|
|
318
318
|
if (!hooks.dir) return empty;
|
|
319
|
-
const text = await readText(path.
|
|
319
|
+
const text = await readText(path.resolve(root, hooks.dir, name));
|
|
320
320
|
if (!text) return empty;
|
|
321
321
|
const tools = new Set(matchTools(text, scripts));
|
|
322
322
|
const companion = await companionOf(root, hooks.dir, name, text, scripts);
|
|
@@ -479,7 +479,7 @@ async function probeScreener(root, facts) {
|
|
|
479
479
|
const doors = [];
|
|
480
480
|
let devBuildArm = false;
|
|
481
481
|
for (const name of HOOK_FILES) {
|
|
482
|
-
const text = await readText(path.
|
|
482
|
+
const text = await readText(path.resolve(root, dir, name));
|
|
483
483
|
if (!text || !SCREEN_CALL_RE.test(text)) continue;
|
|
484
484
|
doors.push(`${dir}/${name}`);
|
|
485
485
|
if (text.includes(DEV_BUILD_ARM)) devBuildArm = true;
|
|
@@ -923,6 +923,11 @@ var instructionTruthLens = {
|
|
|
923
923
|
`${counters.prospectiveSkipped} path claim(s) sit in create-this prose (the file instructs generating them) \u2014 forward-looking, not stale; skipped, not flagged.`
|
|
924
924
|
);
|
|
925
925
|
}
|
|
926
|
+
if (counters.fetchedSkipped) {
|
|
927
|
+
disclosures.push(
|
|
928
|
+
`${counters.fetchedSkipped} path claim(s) sit beside the URL they are fetched from \u2014 another tree's files, unverifiable here; skipped, not flagged.`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
926
931
|
if (counters.placeholderSkipped) {
|
|
927
932
|
disclosures.push(
|
|
928
933
|
`${counters.placeholderSkipped} path claim(s) are naming stand-ins (e.g. \`my-custom-skill\`) rather than real references; skipped, not flagged.`
|
|
@@ -960,7 +965,7 @@ var instructionTruthLens = {
|
|
|
960
965
|
);
|
|
961
966
|
}
|
|
962
967
|
disclosures.push(
|
|
963
|
-
`Checked ${files.length} instruction file(s); commands resolved against root + ${facts.packages.length} workspace manifest(s) plus installed binaries; paths matched against root and
|
|
968
|
+
`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.`
|
|
964
969
|
);
|
|
965
970
|
return {
|
|
966
971
|
lens: LENS_ID3,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runAudit } from './chunk-
|
|
2
|
+
import { runAudit } from './chunk-KIPD3N77.js';
|
|
3
3
|
import { readLedger } from './chunk-3E2IPCRY.js';
|
|
4
|
+
import { checkClaudePointer, CLAUDE_AGENTS_FALLBACK_VERSION } from './chunk-DLHC7QON.js';
|
|
4
5
|
import { DEFAULT_CONFIG, ETYMD_DIR } from './chunk-P6ATKV2R.js';
|
|
5
6
|
import { readText, isDirectory, git, pathExists } from './chunk-4VPBP6K6.js';
|
|
6
7
|
import os from 'os';
|
|
@@ -1005,8 +1006,8 @@ async function checkGuardedEmails(manifest, findings, disclosures) {
|
|
|
1005
1006
|
}
|
|
1006
1007
|
}
|
|
1007
1008
|
async function checkGateDrift(manifest, findings, disclosures) {
|
|
1008
|
-
const { planWorkflow } = await import('./generate-
|
|
1009
|
-
const { scanProject } = await import('./scan-
|
|
1009
|
+
const { planWorkflow } = await import('./generate-6IPTCLYG.js');
|
|
1010
|
+
const { scanProject } = await import('./scan-V53PGAQ6.js');
|
|
1010
1011
|
const { readConfig } = await import('./config-724Y3IOB.js');
|
|
1011
1012
|
for (const entry of manifest.entries) {
|
|
1012
1013
|
const root = entry.resolvedRoot;
|
|
@@ -1024,8 +1025,13 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
1024
1025
|
);
|
|
1025
1026
|
continue;
|
|
1026
1027
|
}
|
|
1027
|
-
const { config } = await readConfig(root);
|
|
1028
|
-
const planned = (await planWorkflow(root, facts, {
|
|
1028
|
+
const { config, explicit } = await readConfig(root);
|
|
1029
|
+
const planned = (await planWorkflow(root, facts, {
|
|
1030
|
+
agents: false,
|
|
1031
|
+
gates: true,
|
|
1032
|
+
gateConfig: config.gates,
|
|
1033
|
+
gateFailOnPinned: explicit.gatesFailOn
|
|
1034
|
+
})).filter((f) => f.executable);
|
|
1029
1035
|
const missing = planned.filter((f) => !f.exists).map((f) => f.path);
|
|
1030
1036
|
const differing = planned.filter((f) => f.exists && f.differs);
|
|
1031
1037
|
const stale = differing.filter((f) => f.drift !== "edited").map((f) => f.path);
|
|
@@ -1061,6 +1067,31 @@ async function checkGateDrift(manifest, findings, disclosures) {
|
|
|
1061
1067
|
}
|
|
1062
1068
|
}
|
|
1063
1069
|
}
|
|
1070
|
+
async function checkClaudePointers(manifest, findings) {
|
|
1071
|
+
for (const entry of manifest.entries) {
|
|
1072
|
+
const root = entry.resolvedRoot;
|
|
1073
|
+
if (!root || !await isDirectory(root)) continue;
|
|
1074
|
+
const check = await checkClaudePointer(root);
|
|
1075
|
+
if (check.ok) continue;
|
|
1076
|
+
findings.push(
|
|
1077
|
+
check.kind === "no-import" ? finding(
|
|
1078
|
+
`${FLEET_LENS}/claude-pointer-missing:${entry.name}`,
|
|
1079
|
+
"risk",
|
|
1080
|
+
`\`${entry.name}\` has a CLAUDE.md that never imports its AGENTS.md`,
|
|
1081
|
+
[`${entry.name}: ${check.detail}`],
|
|
1082
|
+
"Claude Code reads CLAUDE.md when one exists and falls back to AGENTS.md only when none does. A CLAUDE.md without the import shadows AGENTS.md, so the contract looks universal from inside the repo while Claude Code never receives it.",
|
|
1083
|
+
"Add a full-line `@AGENTS.md` import to that CLAUDE.md, symlink either file to the other, or delete the CLAUDE.md if AGENTS.md is the whole contract."
|
|
1084
|
+
) : finding(
|
|
1085
|
+
`${FLEET_LENS}/claude-pointer-missing:${entry.name}`,
|
|
1086
|
+
"gap",
|
|
1087
|
+
`\`${entry.name}\` relies on the AGENTS.md fallback, which the installed Claude Code predates`,
|
|
1088
|
+
[`${entry.name}: ${check.detail}`],
|
|
1089
|
+
`Claude Code reads AGENTS.md on its own only from ${CLAUDE_AGENTS_FALLBACK_VERSION}; older releases load CLAUDE.md alone, so this repo's contract is invisible to them.`,
|
|
1090
|
+
"Update Claude Code, or create a CLAUDE.md beside AGENTS.md whose only import line is `@AGENTS.md`."
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1064
1095
|
async function collectWallFindings(manifest) {
|
|
1065
1096
|
const findings = [];
|
|
1066
1097
|
const disclosures = [];
|
|
@@ -1070,6 +1101,7 @@ async function collectWallFindings(manifest) {
|
|
|
1070
1101
|
await checkHygieneNeedles(manifest, findings, disclosures);
|
|
1071
1102
|
await checkGuardedEmails(manifest, findings, disclosures);
|
|
1072
1103
|
await checkGateDrift(manifest, findings, disclosures);
|
|
1104
|
+
await checkClaudePointers(manifest, findings);
|
|
1073
1105
|
return { findings, disclosures };
|
|
1074
1106
|
}
|
|
1075
1107
|
function stateAgeDays(facts) {
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { VERSION } from './chunk-
|
|
2
|
+
import { VERSION } from './chunk-L4KIIKXI.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-
|
|
34
|
+
const { run } = await import('./audit-RONSEYFS.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-
|
|
47
|
+
const { run } = await import('./init-7HWVHZMF.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-
|
|
55
|
+
const { run } = await import('./approve-Y36ZO6IL.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-
|
|
61
|
+
const { run } = await import('./scan-DLKYG5BB.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-
|
|
67
|
+
const { run } = await import('./doctor-EHRADIUZ.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-
|
|
79
|
+
const { run } = await import('./brief-ILB6RMSQ.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-
|
|
93
|
+
const { run } = await import('./premise-U6JIB2QG.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-
|
|
135
|
+
const { sweep } = await import('./fleet-KFF3N2O7.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-
|
|
150
|
+
const { check } = await import('./fleet-KFF3N2O7.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-
|
|
159
|
+
const { board } = await import('./fleet-KFF3N2O7.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-
|
|
175
|
+
const { add } = await import('./fleet-KFF3N2O7.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-
|
|
198
|
+
const { dismiss } = await import('./fleet-KFF3N2O7.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-
|
|
213
|
+
const { accept } = await import('./fleet-KFF3N2O7.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-
|
|
228
|
+
const { run } = await import('./propose-NZMEPZIL.js');
|
|
229
229
|
await run({
|
|
230
230
|
cwd: resolveCwd(cmd),
|
|
231
231
|
manifest: opts.manifest,
|
|
@@ -242,7 +242,7 @@ program.command("screen").description(
|
|
|
242
242
|
"fork's upstream remote: files byte-identical to its refs skip vocabulary-class patterns (secret patterns stay absolute)"
|
|
243
243
|
).action(
|
|
244
244
|
(opts, cmd) => action(async () => {
|
|
245
|
-
const { run } = await import('./screen-
|
|
245
|
+
const { run } = await import('./screen-S736DXQ5.js');
|
|
246
246
|
const scope = opts.message ? "message" : opts.dir ? "dir" : opts.tree ? "tree" : "staged";
|
|
247
247
|
await run({
|
|
248
248
|
cwd: resolveCwd(cmd),
|
|
@@ -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-
|
|
259
|
+
const { run } = await import('./gates-UJ23THID.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-
|
|
3
|
-
import './chunk-
|
|
4
|
-
import './chunk-
|
|
2
|
+
import { run } from './chunk-4IESOWHQ.js';
|
|
3
|
+
import './chunk-KIPD3N77.js';
|
|
4
|
+
import './chunk-EYUW3UJU.js';
|
|
5
5
|
import './chunk-3E2IPCRY.js';
|
|
6
6
|
import './chunk-HI7NWPRA.js';
|
|
7
|
-
import './chunk-
|
|
8
|
-
import './chunk-
|
|
7
|
+
import './chunk-DLHC7QON.js';
|
|
8
|
+
import './chunk-L4KIIKXI.js';
|
|
9
9
|
import './chunk-P6ATKV2R.js';
|
|
10
|
-
import './chunk-
|
|
10
|
+
import './chunk-U7BCBJIH.js';
|
|
11
11
|
import './chunk-4VPBP6K6.js';
|
|
12
12
|
|
|
13
13
|
// src/commands/doctor.ts
|
|
@@ -1,14 +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-
|
|
4
|
-
import './chunk-
|
|
5
|
-
import { parseFailOnTier, meetsFailOn } from './chunk-
|
|
3
|
+
import { sweepFleet, FLEET_JSON_SCHEMA, checkManifest, FLEET_TRUST_VALUES, isFleetTrust, MILESTONES_FILE, parseMilestones, parseInitiatives, BOARD_JSON_SCHEMA, renderBoard, loadFleetManifest, fleetLedgerTarget, ensureFindingRecorded } from './chunk-ZCOIZVSM.js';
|
|
4
|
+
import './chunk-KIPD3N77.js';
|
|
5
|
+
import { parseFailOnTier, meetsFailOn } from './chunk-EYUW3UJU.js';
|
|
6
6
|
import './chunk-3E2IPCRY.js';
|
|
7
|
+
import { generateClaudePointerMd } from './chunk-HSRHOVII.js';
|
|
7
8
|
import { print, theme, section, renderFleetRows, renderFleetNotes, renderFindings, TIER_BADGE, glyph } from './chunk-HI7NWPRA.js';
|
|
8
|
-
import { scanProject } from './chunk-
|
|
9
|
-
import './chunk-
|
|
9
|
+
import { checkClaudePointer, scanProject } from './chunk-DLHC7QON.js';
|
|
10
|
+
import './chunk-L4KIIKXI.js';
|
|
10
11
|
import './chunk-P6ATKV2R.js';
|
|
11
|
-
import './chunk-
|
|
12
|
+
import './chunk-U7BCBJIH.js';
|
|
12
13
|
import { readText, pathExists, git } from './chunk-4VPBP6K6.js';
|
|
13
14
|
import { promises } from 'fs';
|
|
14
15
|
import os from 'os';
|
|
@@ -234,6 +235,21 @@ async function add(opts) {
|
|
|
234
235
|
if (manifest.entries.some((e) => e.name === name)) {
|
|
235
236
|
throw new Error(`\`${name}\` is already registered \u2014 resolution is keyed by name`);
|
|
236
237
|
}
|
|
238
|
+
const pointer = await checkClaudePointer(absTarget);
|
|
239
|
+
if (!pointer.ok && pointer.kind === "no-import") {
|
|
240
|
+
throw new Error(
|
|
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
|
+
|
|
243
|
+
${generateClaudePointerMd().trimEnd().split("\n").join("\n")}
|
|
244
|
+
|
|
245
|
+
\u2014 or symlink either file to the other, or delete the CLAUDE.md \u2014 then re-run \`etymd fleet add\`.`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (!pointer.ok) {
|
|
249
|
+
print(
|
|
250
|
+
` ${theme.warn("note")} ${pointer.detail}. Update Claude Code, or add a CLAUDE.md containing \`@AGENTS.md\`.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
237
253
|
const facts = await scanProject(absTarget);
|
|
238
254
|
const remote = facts.git.isRepo ? await git(absTarget, ["remote", "get-url", "origin"]) : null;
|
|
239
255
|
section(`Fleet add ${theme.dim(`\xB7 ${name} \xB7 ${absTarget}`)}`);
|