Extends scripts/ci/validate-skills.js to also scan docs/{locale}/skills/
translated mirrors, not just curated skills/. Adds detection for the
YAML defect classes from #2630 without a parser dependency: unquoted
values containing ": " (glued next key / dropped quoting), values
starting with the reserved '@'/'`' indicators, and missing frontmatter
blocks entirely (required only for docs mirrors; curated skills/ keeps
its existing tolerant behavior).
Greptile flagged that the env-access guard in
gateguard-env-documented.test.js could be bypassed via reflective reads
of process.env (Reflect.get/has/set/deleteProperty/defineProperty/
getOwnPropertyDescriptor/ownKeys), since none of the existing
UNSUPPORTED_ACCESS patterns matched that form.
Add a rule that rejects Reflect.get/has/set/deleteProperty/
defineProperty/getOwnPropertyDescriptor/ownKeys(process.env, ...) and
three self-check fixture cases (Reflect.get, Reflect.has,
Reflect.ownKeys) so the guard is pinned against silently missing them
again.
Negative control: commenting out only the new rule reproduces exactly
the reported gap (the 3 new fixture cases fail with "access guard
missed: Reflect.get, Reflect.has, Reflect.ownKeys"); restoring it goes
back to 10/10.
The documentation surface test scanned the hook's raw source with two
regexes. That had two holes, both confirmed against the shipped parser:
- a GATEGUARD_* name appearing only in a comment or a string was counted
as a real read, and
- destructured, aliased and computed reads were invisible, so an
undocumented knob added in one of those forms would pass silently.
Blank comments, string literals, template-literal text and regex literals
before scanning, so only real code contributes. Blanking preserves length,
so `process.env[...]` keys are located in the blanked code and read back
from the raw source at the same offset.
Rather than chase every possible access form with regexes, the supported
forms are now enforced: destructuring, aliasing, spreading, enumerating
and computed keys fail the guard with instructions to either keep the
convention or extend the parser. Six self-checks cover the blanker and
the guard, including a regex literal containing a slash.
Refs #2573
CodeRabbit review on #2611, all four findings:
- GATEGUARD_DISABLED sat in a table introduced as 'these do not disable the gate'. Moved to its own full-disable section with ECC_GATEGUARD, and corrected the accepted values against ECC_DISABLE_VALUES (0/false/off/disabled/disable - the earlier draft would have implied 'no' works, which it does not).
- Documented that a leading **/ compiles to .*/ and so needs a preceding separator: verified by reproducing the hook's glob->regex translation, **/tests/** matches /repo/tests/foo.js but not a bare relative tests/foo.js. Docs now say so and the example carries both forms. Matcher behaviour deliberately unchanged - widening it is a behaviour change, not a docs fix.
- Reverse-drift check now compares documented names against the parsed env reads instead of hookSource.includes(), so a name surviving only in a comment or error string no longer satisfies it.
- readGateguardEnvNames builds one Set from collected matches instead of mutating via Set#add, per the repo's no-in-place-mutation guideline.
GateGuard reads five GATEGUARD_* environment variables that were absent
from skills/gateguard/SKILL.md, so the only discoverable escape hatch was
ECC_GATEGUARD=off - disabling the load-bearing destructive-Bash gate
along with the noisy ones (#2573).
Documented, with defaults and exact accepted values read from the hook:
- GATEGUARD_BASH_ROUTINE_DISABLED (was undocumented everywhere)
- GATEGUARD_EXEMPT_GLOBS (previously only in a 2.1.0 release note)
- GATEGUARD_BASH_EXTRA_DESTRUCTIVE (was undocumented)
- GATEGUARD_DISABLED (was undocumented)
- GATEGUARD_STATE_DIR (was undocumented; named in a runtime warning)
- GATEGUARD_FACT_FORCE_FULL_DENIALS (already documented; folded into the
same table for one lookup point)
Adds tests/ci/gateguard-env-documented.test.js, which asserts every
GATEGUARD_* variable the hook reads appears in the skill doc, and that the
doc names no variable the hook has stopped reading. That surface test is
what found the three knobs beyond the two the issue reported.
Docs and test only; no hook behaviour changes.
Refs #2573
The context signal always rendered "N% of <window> window", including when
the window size was the assumed 200k default rather than a detected value.
On a 1M session whose transcript carries no [1m] marker, that produced
lines like:
[StrategicCompact] Context ~194k tokens (97% of 200k window)
while actual usage was ~19%. The user compacts on a false alarm, loses
context, and the resulting quality drop reads as a model regression.
The gap is structural: the context threshold defaults to 80% of the
window (160k on 200k), so the signal fires precisely in the 160k-200k
band where the size cannot be determined — above 200k the observed-tokens
fallback correctly infers 1M, and below 160k nothing fires.
Model id alone cannot close this. A tier may ship both a 200k and a 1M
variant under one id, so neither the known-family table nor a new entry
can distinguish them, and the transcript records no window field.
So stop asserting what isn't known: resolveContextWindow() now reports
whether the size was detected (env override, [1m] marker, known family,
or observed tokens > 200k) or assumed, and the hook omits the percentage
and window label when it was assumed. The token count, threshold, and
firing behaviour are unchanged.
resolveContextWindowTokens() keeps its existing signature and semantics.
Note: 3 pre-existing failures in tests/hooks/suggest-compact.test.js
reproduce identically on unmodified main and are untouched here.
Remove the redundant JSON-RPC initialize fallback from the consolidated MCP health-check batch. A routed 404 already proves the endpoint is reachable, and the real authenticated MCP call remains authoritative. Avoiding the fallback also prevents a stalled GET plus stalled POST from consuming twice the configured hook timeout.
Prevent stale /tmp/harness-cost cache files from affecting Sonnet 5, dated,
near-miss, and cache-rate pricing tests by using Date.now() in each session ID.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The mcp-health-check preflight probes HTTP MCP servers with a bare GET.
Some Streamable HTTP servers route only POST /mcp and answer a bare GET
with 404 (Paper Desktop 0.5.3 is one). The probe scored that as down and
blocked every tool call for the server indefinitely, since the 30s
backoff just re-probes and re-fails.
A routed HTTP response of any status proves the endpoint is reachable,
which is all this preflight claims to check -- 400/401/403/405/406 are
already treated this way for the same reason. Add 404 to the set and let
the real MCP client validate the endpoint.
Adds a regression test that stands up a POST-only server (404 on GET,
200 on POST /mcp); it fails on the current code and passes with the fix.
The preflight probe in mcp-health-check only ever sent a bare GET to the
server URL. Some Streamable HTTP MCP servers route POST exclusively and
answer any GET with 404 — api.telnyx.com/v2/mcp is one — so the probe
failed permanently against a perfectly healthy server.
404 is not in HEALTHY_HTTP_CODES, so every probe failed, the backoff
compounded to the 10-minute ceiling, and the hook blocked every tool call
for that server before it left the machine while `claude mcp list` still
reported it Connected.
Replay a failed GET as a real JSON-RPC initialize POST and accept that as
proof of life. Whitelisting 404 was the alternative, but it would mask
genuine outages on every other server.
Adds a regression test with a POST-only server that 404s all GETs and
validates the initialize body; it fails without this change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cost-estimate.js carries its own copy of the stale Opus/Haiku/Sonnet rate
table already reported in #2574, but grepping every .js/.json/.md file
outside node_modules turns up zero callers besides its own test. It was
added in 940135e alongside the statusline observability hooks and never
wired into any of them.
The maintainer's comment on #2656 named two acceptable outcomes: remove
the unused duplicate, or share one rate source with the live tracker.
cost-tracker.js's own fix (#2574) has not landed yet, so sharing its
table now would import numbers that are still wrong. Removing the dead
file is the smaller, immediately-correct step.
Fixes#2656
Artifacts written by `evolve --generate` are inert: Claude Code (and every
spec-compliant Agent Skills client) injects only `name` + `description` at
startup and will not load an artifact missing them.
Today the generator writes:
- skills: `# {name}` with no frontmatter block at all
- commands: `# {cmd_name}` with no frontmatter block at all
- agents: `model`/`tools` only, no `name`, no `description`
So the whole evolve pipeline terminates in files that can never load. I hit
this on a real install: 12 generated artifacts across two projects, none of
which Claude Code had ever seen.
This adds a `_evolved_description()` helper and emits proper frontmatter for
all three artifact kinds. The description is sanitised for the two things that
break loaders: `: ` in an unquoted scalar (rejected by strict YAML parsers)
and `<`/`>` (system-prompt injection risk).
Adds two tests to tests/scripts/instinct-cli-evolve-generate.test.js. Both
fail against current main and pass with this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>