vigiles 16.1.1 → 16.1.3
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/dist/adapter-conformance.js +17 -0
- package/dist/adapters/claude-code/dialect.d.ts +19 -13
- package/dist/adapters/claude-code/dialect.js +40 -62
- package/dist/adapters/claude-code/vocabulary.d.ts +133 -0
- package/dist/adapters/claude-code/vocabulary.js +208 -0
- package/dist/core/bash-effects.d.ts +57 -0
- package/dist/core/bash-effects.js +147 -0
- package/dist/core/compile.js +6 -1
- package/dist/core/dialect.d.ts +27 -0
- package/dist/core/hook-events.d.ts +32 -15
- package/dist/core/hook-events.js +23 -29
- package/dist/core/hook-program.js +12 -4
- package/dist/core/markdown.d.ts +53 -0
- package/dist/core/markdown.js +99 -0
- package/dist/core/rule-meta.js +2 -2
- package/dist/core/skill-resources.js +58 -57
- package/dist/core/source-refs.d.ts +118 -0
- package/dist/core/source-refs.js +206 -0
- package/dist/core/tool-contract.d.ts +69 -30
- package/dist/core/tool-contract.js +59 -57
- package/dist/core/vocabulary-consistency.d.ts +35 -0
- package/dist/core/vocabulary-consistency.js +81 -0
- package/dist/core/vocabulary.d.ts +138 -0
- package/dist/core/vocabulary.js +262 -0
- package/dist/coverage-evidence.js +15 -3
- package/dist/plugin-loader.js +22 -28
- package/dist/scan-core.d.ts +8 -1
- package/dist/scan-core.js +105 -20
- package/dist/scan-files.js +17 -20
- package/dist/scan.d.ts +24 -0
- package/dist/scan.js +8 -1
- package/package.json +1 -1
|
@@ -29,6 +29,7 @@ exports.isReadOnlyBash = isReadOnlyBash;
|
|
|
29
29
|
exports.leafCommands = leafCommands;
|
|
30
30
|
exports.leafCommandsNormalized = leafCommandsNormalized;
|
|
31
31
|
exports.leafArgvSource = leafArgvSource;
|
|
32
|
+
exports.commandWords = commandWords;
|
|
32
33
|
// mvdan-sh is a CJS package (GopherJS build) with no bundled TypeScript types.
|
|
33
34
|
// The project compiles to CommonJS (Node16, no "type":"module"), so plain
|
|
34
35
|
// require() works and is the idiomatic pattern here (see linters.ts).
|
|
@@ -1098,4 +1099,150 @@ function normalizeCallExpr(node, redirs) {
|
|
|
1098
1099
|
chdir: stripped.chdir,
|
|
1099
1100
|
};
|
|
1100
1101
|
}
|
|
1102
|
+
// ===========================================================================
|
|
1103
|
+
// FILE-OPERAND extraction (the reference question, not the effect question)
|
|
1104
|
+
// ===========================================================================
|
|
1105
|
+
/**
|
|
1106
|
+
* Interpreters, and the flags after which the NEXT word is a PROGRAM rather
|
|
1107
|
+
* than a path. `node -e "<js>"`, `python -c "<py>"`, `perl -E "<pl>"`.
|
|
1108
|
+
*
|
|
1109
|
+
* Keyed by the head's basename, so `/usr/local/bin/node` and `node` behave the
|
|
1110
|
+
* same.
|
|
1111
|
+
*/
|
|
1112
|
+
const INLINE_PROGRAM_FLAGS = new Map([
|
|
1113
|
+
["node", ["-e", "--eval", "-p", "--print"]],
|
|
1114
|
+
["nodejs", ["-e", "--eval", "-p", "--print"]],
|
|
1115
|
+
["bun", ["-e", "--eval", "-p", "--print"]],
|
|
1116
|
+
["deno", ["-e", "--eval", "-p", "--print"]],
|
|
1117
|
+
["python", ["-c"]],
|
|
1118
|
+
["python2", ["-c"]],
|
|
1119
|
+
["python3", ["-c"]],
|
|
1120
|
+
["ruby", ["-e"]],
|
|
1121
|
+
["perl", ["-e", "-E"]],
|
|
1122
|
+
["php", ["-r"]],
|
|
1123
|
+
]);
|
|
1124
|
+
/**
|
|
1125
|
+
* Shells, whose `-c` argument is a nested SHELL program. Not program text to be
|
|
1126
|
+
* discarded — program text to be PARSED, so `bash -c 'exec "$ROOT/hooks/x.sh"'`
|
|
1127
|
+
* still yields its script.
|
|
1128
|
+
*/
|
|
1129
|
+
const SHELL_HEADS = new Set([
|
|
1130
|
+
"sh",
|
|
1131
|
+
"bash",
|
|
1132
|
+
"zsh",
|
|
1133
|
+
"dash",
|
|
1134
|
+
"ksh",
|
|
1135
|
+
"ash",
|
|
1136
|
+
"busybox",
|
|
1137
|
+
]);
|
|
1138
|
+
/** Guard against a pathological `sh -c 'sh -c "sh -c …"'` nest. */
|
|
1139
|
+
const MAX_SHELL_NESTING = 3;
|
|
1140
|
+
/**
|
|
1141
|
+
* Every word of every simple command in `command` that could name a FILE, with
|
|
1142
|
+
* inline PROGRAM TEXT removed — the primitive behind "does this hook's script
|
|
1143
|
+
* exist?".
|
|
1144
|
+
*
|
|
1145
|
+
* 🔴 WHY A FOURTH EXTRACTOR, stated against the three that already exist,
|
|
1146
|
+
* because "there is already one that returns words" is exactly the reasoning
|
|
1147
|
+
* that produced the bug this replaces.
|
|
1148
|
+
*
|
|
1149
|
+
* - `leafCommands` drops every word it cannot reduce to a LITERAL, so
|
|
1150
|
+
* `${CLAUDE_PLUGIN_ROOT}/hooks/x.sh` — the standard spelling of a hook path —
|
|
1151
|
+
* disappears entirely. Unusable for a file question.
|
|
1152
|
+
* - `leafCommandsNormalized` basenames the head, so `./hooks/x.sh` becomes
|
|
1153
|
+
* `x.sh` and no resolver can find it.
|
|
1154
|
+
* - `leafArgvSource` keeps the spelling but answers a DIFFERENT question:
|
|
1155
|
+
* "which leaves unconditionally RUN". It drops the right-hand side of `&&`
|
|
1156
|
+
* by design, so `cd "$ROOT" && node hooks/x.mjs` yields no script. For
|
|
1157
|
+
* coverage attribution that abstention is correct; for "must this file
|
|
1158
|
+
* exist?" it is a miss, because a conditionally-run script still has to be
|
|
1159
|
+
* on disk.
|
|
1160
|
+
*
|
|
1161
|
+
* So this walks EVERY simple command, keeps every word at source level, and
|
|
1162
|
+
* subtracts only the words that are provably not paths.
|
|
1163
|
+
*
|
|
1164
|
+
* 🔴 WHAT IT SUBTRACTS, and the defect that motivated it. The hook scanner used
|
|
1165
|
+
* to run a regex over the raw command STRING. Against the standard portable
|
|
1166
|
+
* plugin idiom —
|
|
1167
|
+
*
|
|
1168
|
+
* node -e "(async()=>{…await import(…join(root,'hooks','always-on.mjs'))…})()"
|
|
1169
|
+
*
|
|
1170
|
+
* — it grabbed a character run ending at `.mjs` and reported the hook's script
|
|
1171
|
+
* as `import(require(node:url).pathToFileURL(require(node:path).join(root,hooks,always-on.mjs`,
|
|
1172
|
+
* MISSING. `hooks/always-on.mjs` was 1,766 bytes on disk. Nine such findings
|
|
1173
|
+
* across the 32-repo corpus, contributing to two `F/0` grades. Inside a shell
|
|
1174
|
+
* parse the argument of `-e` is not a word the shell will ever resolve to a
|
|
1175
|
+
* file, so it is not returned.
|
|
1176
|
+
*
|
|
1177
|
+
* ⚠️ HOW MUCH OF THAT THE FLAG TABLE ACTUALLY DID, measured rather than
|
|
1178
|
+
* assumed: none of it, on that corpus. Mutating the `-e`/`-c` subtraction OFF
|
|
1179
|
+
* and re-auditing all three affected repos still yields ZERO false hook-script
|
|
1180
|
+
* findings, because both real payloads are single words that either contain
|
|
1181
|
+
* whitespace or do not end in a script extension, and the caller anchors its
|
|
1182
|
+
* match to a whole word. The table is kept because it is the difference
|
|
1183
|
+
* between a function whose contract ("words that could name a file") is true
|
|
1184
|
+
* and one whose contract is merely true-so-far: without it a JavaScript
|
|
1185
|
+
* program is handed to every caller as a candidate filename, and the next
|
|
1186
|
+
* caller inherits the bug. Recorded here so nobody reads a corpus number back
|
|
1187
|
+
* onto the wrong mechanism.
|
|
1188
|
+
*
|
|
1189
|
+
* Words beginning with `-` are dropped as flags: a flag is not a path, and the
|
|
1190
|
+
* one caller anchors its match to a whole word anyway.
|
|
1191
|
+
*
|
|
1192
|
+
* Returns `null` — not `[]` — when the text does not parse as shell, so a
|
|
1193
|
+
* caller can tell "no file operands" from "no analysis", and cannot silently
|
|
1194
|
+
* treat the second as the first.
|
|
1195
|
+
*/
|
|
1196
|
+
function commandWords(command) {
|
|
1197
|
+
return commandWordsAt(command, 0);
|
|
1198
|
+
}
|
|
1199
|
+
function commandWordsAt(command, depth) {
|
|
1200
|
+
let file;
|
|
1201
|
+
try {
|
|
1202
|
+
file = sh.syntax.NewParser().Parse(command, "cmd.sh");
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
const out = [];
|
|
1208
|
+
sh.syntax.Walk(file, (node) => {
|
|
1209
|
+
if (sh.syntax.NodeType(node) === "CallExpr" && node.Args?.length)
|
|
1210
|
+
fileOperandsOf(node.Args, depth, out);
|
|
1211
|
+
return true;
|
|
1212
|
+
});
|
|
1213
|
+
return out;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* The file-operand words of ONE simple command, appended to `out`.
|
|
1217
|
+
*
|
|
1218
|
+
* Wrappers are resolved through with the same table `leafArgvSource` uses (it
|
|
1219
|
+
* keys on the BASENAME head, and wrappers only ever drop words off the FRONT,
|
|
1220
|
+
* so a count maps the result back onto the original spellings — not a second
|
|
1221
|
+
* copy of the rule). Flags never name a file, so they are dropped; the word
|
|
1222
|
+
* AFTER an inline-program flag is dropped with them, and the word after a
|
|
1223
|
+
* shell's `-c` is parsed as shell instead.
|
|
1224
|
+
*/
|
|
1225
|
+
function fileOperandsOf(args, depth, out) {
|
|
1226
|
+
const raw = args.map((w) => sourceParts(w.Parts) ?? "");
|
|
1227
|
+
const probe = [normalizeHead(raw[0] ?? ""), ...raw.slice(1)];
|
|
1228
|
+
const argv = raw.slice(probe.length - stripWrappers(probe).argv.length);
|
|
1229
|
+
const head = normalizeHead(argv[0] ?? "");
|
|
1230
|
+
const programFlags = INLINE_PROGRAM_FLAGS.get(head);
|
|
1231
|
+
const nestsShell = SHELL_HEADS.has(head) && depth < MAX_SHELL_NESTING;
|
|
1232
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1233
|
+
const w = argv[i] ?? "";
|
|
1234
|
+
if (w === "")
|
|
1235
|
+
continue;
|
|
1236
|
+
if (!w.startsWith("-")) {
|
|
1237
|
+
out.push(w);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
if (nestsShell && w === "-c") {
|
|
1241
|
+
out.push(...(commandWordsAt(argv[++i] ?? "", depth + 1) ?? []));
|
|
1242
|
+
}
|
|
1243
|
+
else if (programFlags?.includes(w)) {
|
|
1244
|
+
i++; // the program text — not a word any shell resolves to a file
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1101
1248
|
//# sourceMappingURL=bash-effects.js.map
|
package/dist/core/compile.js
CHANGED
|
@@ -827,7 +827,12 @@ function compileSkill(spec, options = {}) {
|
|
|
827
827
|
* detection lives in the shared `verifyToolContract` detector (one-detector-no-
|
|
828
828
|
* drift: compile + scan + the subagent-tool-contract lint rule call the same code). */
|
|
829
829
|
function validateAgentTools(tools, dialect) {
|
|
830
|
-
|
|
830
|
+
// `authoringIssues` drops the `conditional` verdicts: `Agent`, `ExitPlanMode`
|
|
831
|
+
// and the foreground-only built-ins are REAL tools, legitimate to declare, and
|
|
832
|
+
// erroring on them is what made `tools: Agent, Read, Bash` — a worked example
|
|
833
|
+
// in the vendor's own docs — fail to compile. Everything else stays an error,
|
|
834
|
+
// because authoring your own spec is a closed world.
|
|
835
|
+
return (0, tool_contract_js_1.authoringIssues)((0, tool_contract_js_1.verifyToolContract)(tools, dialect)).map((issue) => ({
|
|
831
836
|
type: "unknown-tool",
|
|
832
837
|
message: issue.message,
|
|
833
838
|
}));
|
package/dist/core/dialect.d.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* Which SKILL.md frontmatter keys a harness understands — see
|
|
23
23
|
* `HarnessDialect.skillFrontmatter`.
|
|
24
24
|
*/
|
|
25
|
+
import type { HarnessVocabulary } from "./vocabulary.js";
|
|
25
26
|
export type SkillFrontmatterProfile = "claude-code" | "minimal";
|
|
26
27
|
export interface HarnessDialect {
|
|
27
28
|
/** Stable identifier, e.g. "claude-code". */
|
|
@@ -83,5 +84,31 @@ export interface HarnessDialect {
|
|
|
83
84
|
* Optional (additive, non-breaking) — absent ⇒ no tool is known-side-effecting.
|
|
84
85
|
*/
|
|
85
86
|
readonly sideEffectingTools?: readonly string[];
|
|
87
|
+
/**
|
|
88
|
+
* The hook-event catalog as a {@link HarnessVocabulary} — a status and a
|
|
89
|
+
* recorded vendor capture per term, rather than bare membership in
|
|
90
|
+
* `hookEvents`. When present it is what `verifyHookEvents` classifies against,
|
|
91
|
+
* so a name the catalog doesn't hold produces an `unrecognised` ADVISORY
|
|
92
|
+
* (naming vigiles's capture as the possibly-stale party) instead of the old
|
|
93
|
+
* behaviour, where an unknown name drew an accusation or silence depending on
|
|
94
|
+
* its edit distance to the list.
|
|
95
|
+
*
|
|
96
|
+
* Optional (additive, non-breaking). Absent ⇒ one is synthesised from
|
|
97
|
+
* `hookEvents` via `vocabularyFromLists`, so a legacy adapter keeps working
|
|
98
|
+
* and its unknowns become advisories rather than silence.
|
|
99
|
+
*/
|
|
100
|
+
readonly hookEventVocabulary?: HarnessVocabulary;
|
|
101
|
+
/**
|
|
102
|
+
* The subagent-tool catalog as a {@link HarnessVocabulary}. Same contract as
|
|
103
|
+
* `hookEventVocabulary`; absent ⇒ synthesised from `builtinAgentTools`
|
|
104
|
+
* (available) + `neverAvailableTools` (withheld).
|
|
105
|
+
*
|
|
106
|
+
* The third status is why this exists: the vendor removes `Agent` only at the
|
|
107
|
+
* spawn depth limit and `ExitPlanMode` only outside plan mode, and removes
|
|
108
|
+
* most built-ins from a background subagent but not a foreground one — so
|
|
109
|
+
* "available to a subagent" is not a property of the name, and a two-way
|
|
110
|
+
* split had to encode one of those conditions as an unconditional fact.
|
|
111
|
+
*/
|
|
112
|
+
readonly subagentToolVocabulary?: HarnessVocabulary;
|
|
86
113
|
}
|
|
87
114
|
//# sourceMappingURL=dialect.d.ts.map
|
|
@@ -4,31 +4,48 @@
|
|
|
4
4
|
* (`PreToolUse`, `SessionStart`, …); a TYPO (`PreToolUSe`) means the hook
|
|
5
5
|
* silently never fires — a dead registration no generic JSON linter catches.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `WorktreeRemove`, …
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `
|
|
7
|
+
* The event set is NOT closed in practice: the vendor keeps adding events, and
|
|
8
|
+
* frameworks ship custom runtimes with their own (TheBushidoCollective/han fires
|
|
9
|
+
* `TeammateIdle`, `WorktreeRemove`, … from its own `hooks.json` — both of which
|
|
10
|
+
* have since become real Claude Code events). This check used to handle that by
|
|
11
|
+
* reporting an unknown event ONLY when it sat within edit distance 2 of a known
|
|
12
|
+
* one. That is not a confidence signal, and it failed both ways at once:
|
|
13
|
+
* `Setup`, a documented event, was accused of never firing and told to become
|
|
14
|
+
* `Stop`; twenty-one other documented events drew nothing, because they happened
|
|
15
|
+
* to be further than two characters from anything in a nine-name list.
|
|
16
|
+
*
|
|
17
|
+
* Now every name is CLASSIFIED against the dialect's vocabulary
|
|
18
|
+
* (`core/vocabulary.ts`) and every verdict is reported — with the severity
|
|
19
|
+
* coming from the verdict rather than from the caller. An event vigiles doesn't
|
|
20
|
+
* hold is an `advisory` that names vigiles's own capture as the thing that may
|
|
21
|
+
* be stale; it is surfaced and never scored, so a newer or custom event cannot
|
|
22
|
+
* cost anyone a grade. ONE detector (one-detector-no-drift): scan + the
|
|
23
|
+
* `hook-events` lint rule + compiled-hook `on:` validation call the same code.
|
|
24
|
+
* Dialect injected (core ⊄ adapter).
|
|
14
25
|
*/
|
|
15
26
|
import type { HarnessDialect } from "./dialect.js";
|
|
27
|
+
import { type HarnessVocabulary, type IssueSeverity, type TermVerdict } from "./vocabulary.js";
|
|
16
28
|
export interface HookEventIssue {
|
|
17
29
|
readonly event: string;
|
|
18
|
-
/**
|
|
30
|
+
/** Which vocabulary verdict produced this — the input to every policy. */
|
|
31
|
+
readonly verdict: TermVerdict["kind"];
|
|
32
|
+
/** Closest known event (did-you-mean), or null. Message decoration only. */
|
|
19
33
|
readonly suggestion: string | null;
|
|
34
|
+
/** `"scored"` counts toward the grade; `"advisory"` never does. */
|
|
35
|
+
readonly severity: IssueSeverity;
|
|
20
36
|
readonly message: string;
|
|
21
37
|
}
|
|
22
38
|
/**
|
|
23
|
-
* The
|
|
24
|
-
*
|
|
25
|
-
*
|
|
39
|
+
* The event vocabulary this dialect verifies against — its declared one, else a
|
|
40
|
+
* synthesised one built from the flat `hookEvents` list so an adapter that
|
|
41
|
+
* predates vocabularies keeps working.
|
|
26
42
|
*/
|
|
27
|
-
export declare function
|
|
43
|
+
export declare function hookEventVocabulary(dialect: HarnessDialect): HarnessVocabulary;
|
|
28
44
|
/**
|
|
29
|
-
* Verify hook-event names against the dialect
|
|
30
|
-
*
|
|
31
|
-
*
|
|
45
|
+
* Verify hook-event names against the dialect vocabulary. Returns one issue per
|
|
46
|
+
* name that isn't plainly available, each already carrying its severity — see
|
|
47
|
+
* {@link scoredIssues} / {@link advisoryIssues} to split them.
|
|
32
48
|
*/
|
|
33
49
|
export declare function verifyHookEvents(events: readonly string[], dialect: HarnessDialect): HookEventIssue[];
|
|
50
|
+
export { scoredIssues, advisoryIssues, authoringIssues } from "./vocabulary.js";
|
|
34
51
|
//# sourceMappingURL=hook-events.d.ts.map
|
package/dist/core/hook-events.js
CHANGED
|
@@ -1,48 +1,42 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.authoringIssues = exports.advisoryIssues = exports.scoredIssues = void 0;
|
|
4
|
+
exports.hookEventVocabulary = hookEventVocabulary;
|
|
4
5
|
exports.verifyHookEvents = verifyHookEvents;
|
|
5
|
-
const
|
|
6
|
-
/** Closest known hook event by edit distance (≤ 2) — a confidence signal. */
|
|
7
|
-
function closestEvent(event, dialect) {
|
|
8
|
-
let best = null;
|
|
9
|
-
let bestDistance = Infinity;
|
|
10
|
-
for (const known of dialect.hookEvents) {
|
|
11
|
-
const d = (0, edit_distance_js_1.editDistance)(event.toLowerCase(), known.toLowerCase());
|
|
12
|
-
if (d < bestDistance) {
|
|
13
|
-
bestDistance = d;
|
|
14
|
-
best = known;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return bestDistance <= 2 ? best : null;
|
|
18
|
-
}
|
|
6
|
+
const vocabulary_js_1 = require("./vocabulary.js");
|
|
19
7
|
/**
|
|
20
|
-
* The
|
|
21
|
-
*
|
|
22
|
-
*
|
|
8
|
+
* The event vocabulary this dialect verifies against — its declared one, else a
|
|
9
|
+
* synthesised one built from the flat `hookEvents` list so an adapter that
|
|
10
|
+
* predates vocabularies keeps working.
|
|
23
11
|
*/
|
|
24
|
-
function
|
|
25
|
-
return
|
|
12
|
+
function hookEventVocabulary(dialect) {
|
|
13
|
+
return (dialect.hookEventVocabulary ??
|
|
14
|
+
(0, vocabulary_js_1.vocabularyFromLists)(`${dialect.name} hook event`, `${dialect.name} adapter (no recorded capture)`, dialect.hookEvents));
|
|
26
15
|
}
|
|
27
16
|
/**
|
|
28
|
-
* Verify hook-event names against the dialect
|
|
29
|
-
*
|
|
30
|
-
*
|
|
17
|
+
* Verify hook-event names against the dialect vocabulary. Returns one issue per
|
|
18
|
+
* name that isn't plainly available, each already carrying its severity — see
|
|
19
|
+
* {@link scoredIssues} / {@link advisoryIssues} to split them.
|
|
31
20
|
*/
|
|
32
21
|
function verifyHookEvents(events, dialect) {
|
|
33
|
-
const
|
|
22
|
+
const vocab = hookEventVocabulary(dialect);
|
|
34
23
|
const issues = [];
|
|
35
24
|
for (const event of events) {
|
|
36
|
-
|
|
25
|
+
const issue = (0, vocabulary_js_1.termIssue)(vocab, (0, vocabulary_js_1.classify)(vocab, event), "Hook event", "a hook here never fires");
|
|
26
|
+
if (issue === null)
|
|
37
27
|
continue;
|
|
38
|
-
const near = closestEvent(event, dialect);
|
|
39
|
-
const hint = near ? ` Did you mean "${near}"?` : "";
|
|
40
28
|
issues.push({
|
|
41
29
|
event,
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
verdict: issue.verdict,
|
|
31
|
+
suggestion: issue.suggestion,
|
|
32
|
+
severity: issue.severity,
|
|
33
|
+
message: issue.message,
|
|
44
34
|
});
|
|
45
35
|
}
|
|
46
36
|
return issues;
|
|
47
37
|
}
|
|
38
|
+
var vocabulary_js_2 = require("./vocabulary.js");
|
|
39
|
+
Object.defineProperty(exports, "scoredIssues", { enumerable: true, get: function () { return vocabulary_js_2.scoredIssues; } });
|
|
40
|
+
Object.defineProperty(exports, "advisoryIssues", { enumerable: true, get: function () { return vocabulary_js_2.advisoryIssues; } });
|
|
41
|
+
Object.defineProperty(exports, "authoringIssues", { enumerable: true, get: function () { return vocabulary_js_2.authoringIssues; } });
|
|
48
42
|
//# sourceMappingURL=hook-events.js.map
|
|
@@ -679,11 +679,19 @@ function compileHookProgram(source, hook, opts = {}) {
|
|
|
679
679
|
`regex (that is why "Edit|Write" works), so it must parse as one.`);
|
|
680
680
|
}
|
|
681
681
|
}
|
|
682
|
-
// A hook registered under an event the harness never fires is dead — reject
|
|
682
|
+
// A hook registered under an event the harness never fires is dead — reject
|
|
683
|
+
// it. AUTHORING is a closed world (you are writing this hook now, against the
|
|
684
|
+
// vigiles you have), so an unrecognised event is still an error — the typo
|
|
685
|
+
// guarantee this exists for. What changed on 2026-08-17 is the catalog it
|
|
686
|
+
// asks: this used to throw on `Setup`, `PostCompact`, `ConfigChange` and 19
|
|
687
|
+
// other REAL events, because vigiles held 9 of the vendor's 31. The fix is the
|
|
688
|
+
// right vocabulary, not a weaker check. A genuinely newer event still fails
|
|
689
|
+
// here, and now says so — the message names vigiles's capture as the thing
|
|
690
|
+
// that may be stale, instead of asserting the event does not exist.
|
|
683
691
|
if (opts.dialect) {
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
throw new HookCompileError(
|
|
692
|
+
const fatal = (0, hook_events_js_1.authoringIssues)((0, hook_events_js_1.verifyHookEvents)([on], opts.dialect));
|
|
693
|
+
if (fatal.length > 0) {
|
|
694
|
+
throw new HookCompileError(fatal[0].message);
|
|
687
695
|
}
|
|
688
696
|
}
|
|
689
697
|
// A `needs` entry that isn't a built-in provider never resolves — reject it
|
package/dist/core/markdown.d.ts
CHANGED
|
@@ -42,4 +42,57 @@ export interface FencedBlock {
|
|
|
42
42
|
* so a caller's message points at the real line.
|
|
43
43
|
*/
|
|
44
44
|
export declare function fencedCodeBlocks(src: string): FencedBlock[];
|
|
45
|
+
/**
|
|
46
|
+
* How a reference appeared in the markdown.
|
|
47
|
+
*
|
|
48
|
+
* `link` is a DESTINATION — the thing the reader follows: an inline link's
|
|
49
|
+
* target, or an image's `src`. `code` is a bare backtick span standing on its
|
|
50
|
+
* own in prose.
|
|
51
|
+
*/
|
|
52
|
+
export type MarkdownRefKind = "link" | "code";
|
|
53
|
+
/** One reference recovered from markdown STRUCTURE. */
|
|
54
|
+
export interface MarkdownRef {
|
|
55
|
+
readonly kind: MarkdownRefKind;
|
|
56
|
+
/**
|
|
57
|
+
* For `link`, the destination exactly as written. For `code`, the span's
|
|
58
|
+
* content. Never the display text of a link — see {@link markdownRefs}.
|
|
59
|
+
*/
|
|
60
|
+
readonly value: string;
|
|
61
|
+
/** 1-based source line the reference sits on. */
|
|
62
|
+
readonly line: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Every reference a markdown body makes, taken from the PARSE rather than from
|
|
66
|
+
* the characters.
|
|
67
|
+
*
|
|
68
|
+
* 🔴 WHY THIS EXISTS: A LINK'S TEXT IS NOT A REFERENCE. The detector this
|
|
69
|
+
* replaces ran two regexes over each line — one for `[..](..)`, one for
|
|
70
|
+
* `` `..` `` — and the second one could not see that it was standing inside the
|
|
71
|
+
* first. Measured 2026-08-17 on `microsoft/power-platform-skills`:
|
|
72
|
+
*
|
|
73
|
+
* See [`references/dataverse-reference.md` § Setting Lookups](../add-dataverse/references/dataverse-reference.md#setting-lookups)
|
|
74
|
+
*
|
|
75
|
+
* The DESTINATION resolves — the file is 23KB and present. The backtick span in
|
|
76
|
+
* the link's TEXT is a human-readable label for it. vigiles reported the label
|
|
77
|
+
* as a missing bundled resource, i.e. it accused a correct link of being broken
|
|
78
|
+
* by reading the half of it that is display. The same shape cost
|
|
79
|
+
* `rohitg00/pro-workflow` a second false accusation.
|
|
80
|
+
*
|
|
81
|
+
* So a code span nested inside a link's (or an image's) text is NOT emitted.
|
|
82
|
+
* The destination is right there, it is what the agent follows, and it is
|
|
83
|
+
* returned instead. The bug is not fixed here so much as made unsayable: a
|
|
84
|
+
* caller of this function is never handed link text at all.
|
|
85
|
+
*
|
|
86
|
+
* Fenced and indented code blocks contribute nothing ({@link fencedLineFlags}
|
|
87
|
+
* decides which lines those are, so no caller re-derives it).
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ ONE LINE AT A TIME, and the reason is the line number. Callers report a
|
|
90
|
+
* reference by source line, and markdown-it's inline tokens carry no line
|
|
91
|
+
* information — only the enclosing block does — so a paragraph-wide parse would
|
|
92
|
+
* point every reference in a paragraph at the paragraph's first line. Parsing
|
|
93
|
+
* each line's inline content keeps the number exact. The cost is a construct
|
|
94
|
+
* split across two source lines (a link whose `](` sits on the next line),
|
|
95
|
+
* which is not recovered — the regexes this replaces did not recover it either.
|
|
96
|
+
*/
|
|
97
|
+
export declare function markdownRefs(src: string): MarkdownRef[];
|
|
45
98
|
//# sourceMappingURL=markdown.d.ts.map
|
package/dist/core/markdown.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.fencedLineFlags = fencedLineFlags;
|
|
7
7
|
exports.fencedCodeBlocks = fencedCodeBlocks;
|
|
8
|
+
exports.markdownRefs = markdownRefs;
|
|
8
9
|
/**
|
|
9
10
|
* vigiles — the ONE markdown-structure helper.
|
|
10
11
|
*
|
|
@@ -25,6 +26,23 @@ exports.fencedCodeBlocks = fencedCodeBlocks;
|
|
|
25
26
|
const markdown_it_1 = __importDefault(require("markdown-it"));
|
|
26
27
|
// One reusable parser; parse() is stateless across calls.
|
|
27
28
|
const md = new markdown_it_1.default();
|
|
29
|
+
/**
|
|
30
|
+
* A SECOND parser, used only by {@link markdownRefs}, with link handling turned
|
|
31
|
+
* down to "report exactly what the author wrote":
|
|
32
|
+
*
|
|
33
|
+
* - `normalizeLink` is neutered because the default percent-ENCODES the
|
|
34
|
+
* destination. A detector downstream reads `%NN` as the signature of a URL or
|
|
35
|
+
* of a documentation example about escaping spaces, and skips it; letting
|
|
36
|
+
* markdown-it encode on the way in would manufacture that signature for any
|
|
37
|
+
* destination holding a space or a non-ASCII character.
|
|
38
|
+
* - `validateLink` is opened because the default silently REFUSES to build a
|
|
39
|
+
* link token for schemes it distrusts (`javascript:`, `data:`), which would
|
|
40
|
+
* turn "a destination this tool declines to resolve" into "no destination at
|
|
41
|
+
* all". Skipping by scheme is the caller's job and it already does it.
|
|
42
|
+
*/
|
|
43
|
+
const mdRefs = new markdown_it_1.default();
|
|
44
|
+
mdRefs.normalizeLink = (url) => url;
|
|
45
|
+
mdRefs.validateLink = () => true;
|
|
28
46
|
/**
|
|
29
47
|
* A boolean per source line (0-based): `true` when the line lies inside a fenced
|
|
30
48
|
* code block (` ``` ` or `~~~`), the delimiter lines included — matching the
|
|
@@ -85,4 +103,85 @@ function fencedCodeBlocks(src) {
|
|
|
85
103
|
}
|
|
86
104
|
return out;
|
|
87
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Every reference a markdown body makes, taken from the PARSE rather than from
|
|
108
|
+
* the characters.
|
|
109
|
+
*
|
|
110
|
+
* 🔴 WHY THIS EXISTS: A LINK'S TEXT IS NOT A REFERENCE. The detector this
|
|
111
|
+
* replaces ran two regexes over each line — one for `[..](..)`, one for
|
|
112
|
+
* `` `..` `` — and the second one could not see that it was standing inside the
|
|
113
|
+
* first. Measured 2026-08-17 on `microsoft/power-platform-skills`:
|
|
114
|
+
*
|
|
115
|
+
* See [`references/dataverse-reference.md` § Setting Lookups](../add-dataverse/references/dataverse-reference.md#setting-lookups)
|
|
116
|
+
*
|
|
117
|
+
* The DESTINATION resolves — the file is 23KB and present. The backtick span in
|
|
118
|
+
* the link's TEXT is a human-readable label for it. vigiles reported the label
|
|
119
|
+
* as a missing bundled resource, i.e. it accused a correct link of being broken
|
|
120
|
+
* by reading the half of it that is display. The same shape cost
|
|
121
|
+
* `rohitg00/pro-workflow` a second false accusation.
|
|
122
|
+
*
|
|
123
|
+
* So a code span nested inside a link's (or an image's) text is NOT emitted.
|
|
124
|
+
* The destination is right there, it is what the agent follows, and it is
|
|
125
|
+
* returned instead. The bug is not fixed here so much as made unsayable: a
|
|
126
|
+
* caller of this function is never handed link text at all.
|
|
127
|
+
*
|
|
128
|
+
* Fenced and indented code blocks contribute nothing ({@link fencedLineFlags}
|
|
129
|
+
* decides which lines those are, so no caller re-derives it).
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ ONE LINE AT A TIME, and the reason is the line number. Callers report a
|
|
132
|
+
* reference by source line, and markdown-it's inline tokens carry no line
|
|
133
|
+
* information — only the enclosing block does — so a paragraph-wide parse would
|
|
134
|
+
* point every reference in a paragraph at the paragraph's first line. Parsing
|
|
135
|
+
* each line's inline content keeps the number exact. The cost is a construct
|
|
136
|
+
* split across two source lines (a link whose `](` sits on the next line),
|
|
137
|
+
* which is not recovered — the regexes this replaces did not recover it either.
|
|
138
|
+
*/
|
|
139
|
+
function markdownRefs(src) {
|
|
140
|
+
const lines = src.split("\n");
|
|
141
|
+
const fenced = fencedLineFlags(src);
|
|
142
|
+
const out = [];
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
const line = lines[i] ?? "";
|
|
145
|
+
// Cheap reject: no link syntax and no backtick means no reference, and most
|
|
146
|
+
// lines of a real corpus are that.
|
|
147
|
+
if (fenced[i] || (!line.includes("`") && !line.includes("](")))
|
|
148
|
+
continue;
|
|
149
|
+
for (const tok of mdRefs.parseInline(line, {})) {
|
|
150
|
+
refsInInline(tok.children ?? [], i + 1, out);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** Which attribute carries the DESTINATION, per inline token type. */
|
|
156
|
+
const DESTINATION_ATTR = {
|
|
157
|
+
link_open: "href",
|
|
158
|
+
image: "src",
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Walk ONE line's inline token stream, appending its references.
|
|
162
|
+
*
|
|
163
|
+
* `linkDepth` is the whole point: markdown-it emits `link_open` … `link_close`
|
|
164
|
+
* around the link's TEXT, so a `code_inline` seen while the depth is non-zero
|
|
165
|
+
* is display, and its destination has already been recorded.
|
|
166
|
+
*/
|
|
167
|
+
function refsInInline(children, line, out) {
|
|
168
|
+
let linkDepth = 0;
|
|
169
|
+
for (const child of children) {
|
|
170
|
+
if (child.type === "link_close") {
|
|
171
|
+
linkDepth--;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (child.type === "link_open")
|
|
175
|
+
linkDepth++;
|
|
176
|
+
// An image's alt text is a nested inline stream markdown-it keeps in
|
|
177
|
+
// `children`; it is display, exactly like link text, so only the `src` is
|
|
178
|
+
// taken and the alt is not descended into.
|
|
179
|
+
const destAttr = DESTINATION_ATTR[child.type];
|
|
180
|
+
const dest = destAttr === undefined ? null : child.attrGet(destAttr);
|
|
181
|
+
if (dest)
|
|
182
|
+
out.push({ kind: "link", value: dest, line });
|
|
183
|
+
else if (child.type === "code_inline" && linkDepth === 0)
|
|
184
|
+
out.push({ kind: "code", value: child.content, line });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
88
187
|
//# sourceMappingURL=markdown.js.map
|
package/dist/core/rule-meta.js
CHANGED
|
@@ -85,7 +85,7 @@ exports.RULE_META = {
|
|
|
85
85
|
surface: ["subagent"],
|
|
86
86
|
defaultSeverity: "warn",
|
|
87
87
|
summary: "A subagent's tools: are all real (no never-available / typo).",
|
|
88
|
-
detector: "
|
|
88
|
+
detector: "verifyToolContract / scoredIssues",
|
|
89
89
|
upstreamPrevention: "typed agent() vocabulary + compileAgent — an unknown tool is a tsc/compile error",
|
|
90
90
|
},
|
|
91
91
|
"disallowed-tools-contract": {
|
|
@@ -113,7 +113,7 @@ exports.RULE_META = {
|
|
|
113
113
|
surface: ["hook"],
|
|
114
114
|
defaultSeverity: "warn",
|
|
115
115
|
summary: "A hook's event name is one the harness defines (it can fire).",
|
|
116
|
-
detector: "
|
|
116
|
+
detector: "verifyHookEvents / scoredIssues",
|
|
117
117
|
upstreamPrevention: "compiled hook on: is dialect-validated at compile",
|
|
118
118
|
},
|
|
119
119
|
"hook-script-exists": {
|