vigiles 27.1.1 → 27.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -131,12 +131,85 @@ function salvageList(block, key) {
131
131
  return null;
132
132
  return splitList(match[1]);
133
133
  }
134
- /** Split a comma list or inline-array string into trimmed, de-quoted tokens. */
134
+ /**
135
+ * Split a tool-list string into trimmed, de-quoted tokens.
136
+ *
137
+ * WHITESPACE IS A SEPARATOR, BUT ONLY AT PAREN DEPTH 0 — and that qualifier is
138
+ * the whole rule. Claude Code documents three equivalent spellings of
139
+ * `allowed-tools:`/`disallowed-tools:` (a comma-separated string, a
140
+ * SPACE-separated string, a YAML list), and its own documented example is
141
+ * space-separated with spaces INSIDE the tokens:
142
+ *
143
+ * allowed-tools: Bash(git add *) Bash(git commit *) Bash(git status *)
144
+ *
145
+ * Until #217 this split on `,` alone, so a space-separated fence arrived as ONE
146
+ * token that matches no built-in name. That failed in the direction that misleads:
147
+ * a fence the harness really enforces was reported as closing no lethal-trifecta
148
+ * leg, so an author who wrote a correct fence was told it did nothing. Reported
149
+ * with a reproduction by @vlad-ryzhkov, measured on a 22-skill harness where the
150
+ * separator alone moved Safety 81 → 80 → 81.
151
+ *
152
+ * THE NAIVE FIX IS WORSE THAN THE BUG, which is why this is a tokenizer and not
153
+ * `split(/[,\s]+/)`: that shreds `Bash(git push *)` into `Bash(git`, `push`, `*)`,
154
+ * and since `bashGrantIsUnbounded()` answers "unbounded" when it cannot recognise
155
+ * a grant, a BOUNDED grant would start reading as an unbounded one. That is the
156
+ * false-exposed side of the trade `core/lethal-trifecta.ts` already reasons about
157
+ * — a scarier verdict for a reason the author cannot see anywhere in their file.
158
+ *
159
+ * Quotes are stripped per token AFTER splitting, never treated as a delimiter:
160
+ * `allowed-tools: "Read Write Glob"` is a YAML-quoted scalar whose VALUE is a
161
+ * space-separated list, and Claude Code splits it. Treating the quotes as token
162
+ * boundaries would keep exactly the bug this fixes for every skill that quotes
163
+ * its list.
164
+ */
135
165
  function splitList(raw) {
136
- return raw
137
- .replace(/^\[|\]$/g, "")
138
- .split(",")
139
- .map((t) => t.trim().replace(/^["']|["']$/g, ""))
140
- .filter((t) => t.length > 0);
166
+ const out = [];
167
+ let token = "";
168
+ let depth = 0;
169
+ let quote = null;
170
+ let escaped = false;
171
+ const flush = () => {
172
+ const t = token.trim().replace(/^["']|["']$/g, "");
173
+ if (t.length > 0)
174
+ out.push(t);
175
+ token = "";
176
+ };
177
+ for (const ch of raw.trim().replace(/^\[|\]$/g, "")) {
178
+ if (escaped) {
179
+ escaped = false;
180
+ token += ch;
181
+ continue;
182
+ }
183
+ if (ch === "\\" && quote !== "'") {
184
+ escaped = true;
185
+ token += ch;
186
+ continue;
187
+ }
188
+ if (quote === null && (ch === '"' || ch === "'"))
189
+ quote = ch;
190
+ else if (quote === ch)
191
+ quote = null;
192
+ // Depth tracks STRUCTURE, so a parenthesis inside a quoted string is a
193
+ // character, not a bracket. Without this, `Bash(printf '( %s' foo) Read`
194
+ // leaves depth at 1 after the grant closes and swallows `Read` into the
195
+ // same token — on a subagent that denies a tool the author granted, which
196
+ // is the exact failure this function exists to fix.
197
+ else if (quote === null) {
198
+ if (ch === "(")
199
+ depth++;
200
+ else if (ch === ")")
201
+ depth = Math.max(0, depth - 1);
202
+ }
203
+ // Quotes deliberately do NOT suppress splitting: `allowed-tools: "Read
204
+ // Write Glob"` is a YAML-quoted scalar whose VALUE is a space-separated
205
+ // list, and Claude Code splits it. The quotes come off per token below.
206
+ if (depth === 0 && (ch === "," || /\s/.test(ch))) {
207
+ flush();
208
+ continue;
209
+ }
210
+ token += ch;
211
+ }
212
+ flush();
213
+ return out;
141
214
  }
142
215
  //# sourceMappingURL=frontmatter-read.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigiles",
3
- "version": "27.1.1",
3
+ "version": "27.1.2",
4
4
  "description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
5
5
  "keywords": [
6
6
  "claude-code",