Commit Graph
3115 Commits
Author SHA1 Message Date
montjeffrey 175a0800d0 fix(gateguard): enforce evaluation validity guards 2026-10-05 01:43:33 -04:00
montjeffrey 1d66665d39 docs(gateguard): the external-contract question did not move the gated arm
96 trials, none invalid. The headline reads gate 23/48 against off 13/48, a
21-point paired difference at exact McNemar p = 0.021, and it is an artifact.
The scenarios, prompts, graders and model alias were identical to the previous
run and the off arm has no gate, yet off fell from 18/48 to 13/48 while the
gated arm went down, 25/48 to 23/48. An intervention meant to raise the gated
arm did not raise it, so the p-value reflects a baseline that fell rather than a
gate that improved.

The two scenarios the question cannot reach, having no data for the profile to
detect, serve as a control: they moved by -3 and +3, more than any of the four
it did reach (-1, -1, +1, -1). Noise on eight trials is about plus or minus
three, which is larger than any effect available to the change.

The question is kept, since it asks something the gate never asked and the
corpus says that kind of answer decides the outcome, but no improvement is
claimed. Testing it fairly needs scenarios authored blind to it, which this
corpus no longer is.
2026-10-04 22:40:43 -04:00
montjeffrey d545071699 docs(gateguard): confirmatory run does not confirm
96 trials, none invalid, analysed exactly as pre-registered. passed: gate 25/48
against off 18/48, 48 pairs, discordant 15 to 8, exact McNemar p = 0.210. At the
registered alpha of 0.05 the hypothesis is not confirmed, and the bootstrap
interval agrees this time at -15% with a range of -33% to +4%, so there is no
disagreement between the tests to adjudicate. Secondary measures point the same
way and reach nothing: asked 29/48 against 22/48 at p = 0.230, deciding fact
27/48 against 21/48 at p = 0.327.

The effect halved on replication, from +29 points at p = 0.065 to +15 at
p = 0.210, which is the ordinary fate of a near-threshold finding. The runs are
not pooled: the quota leak was closed between them, and combining them after
seeing both would be the post-hoc choice the pre-registration exists to stop.

Two things survive every run. Obtaining the deciding fact is necessary and
nearly sufficient, with no trial in either arm passing without it across 96
trials. And the gate costs 37 to 45% more turns and spend, which is the most
stable number the evaluation has produced.
2026-10-04 22:40:43 -04:00
montjeffrey 7b03721135 test(gateguard): close the overage leak and pre-register the confirmatory run
soft-limit-overage asked for { allowed, overage }. Naming an overage figure
hints that passing a limit is permitted, which is precisely the hidden policy,
and it showed: that scenario was one of the two with the highest ungated pass
rate in the signposted run. The grader now inspects only allowed, so the prompt
asks for { allowed } and mentions neither overage nor billing nor soft limits. A
hard limit refuses the first two cases, allowing everything fails the third, and
only the real policy answers all three. --check-graders still separates start,
trap and reference on all six.

The analysis is registered in the doc before the run rather than chosen after
it: passed as the single primary outcome, paired exact McNemar at alpha 0.05, a
sample of 48 pairs fixed in advance, no extension on the strength of the result,
frozen inputs, secondary measures explicitly not claim-bearing, a validity rule
for provider errors, and a commitment to report a null.
2026-10-04 22:40:43 -04:00
montjeffrey a8fbe44e4f docs(gateguard): report the unflagged-prompt run, where the arms separate
With the ambiguity signposted, the arms were indistinguishable on every measure.
With the prompts rewritten as ordinary tickets and nothing else changed, they
separate: asked the user 16/24 against 10/24, obtained the deciding fact the
same, passed 15/24 against 8/24, exact McNemar p = 0.065 to 0.070, and a paired
pass-rate difference of 29 points in the gate's favour whose bootstrap interval
excludes zero.

The regime contrast carries it. The gated arm passed 15/24 in both runs while
the ungated arm fell from 16/24 to 8/24, which is what removing the only cue
that a convention was missing would do if the gate supplies that cue itself.
Asking stayed necessary throughout: no trial in either arm passed without the
deciding fact.

Reported as suggestive rather than established. All three p-values sit just
above 5%, one more gate-favouring discordant pair would cross it, and extending
this run after seeing that would be optional stopping. A confirmatory run needs
its sample fixed first: about 43 pairs, or 8 repetitions over these six
scenarios.
2026-10-04 22:40:43 -04:00
montjeffrey b29823b435 test(gateguard): leave the ambiguity unflagged in every prompt
The previous prompts signposted what they withheld - "the dialect our consumer
expects", "the calendar our reports are read against", "retrying only the
failures that are safe to repeat" - which invited a question in every arm and
pushed ungated pass rates to 3/4 and 4/4. That bounded the result to a regime
where the agent was already told something was missing.

Each prompt now reads as an ordinary ticket. Shape a grader checks stays, since
removing it would score field-name guessing instead of the hidden decision, but
it is stated mechanically: checkQuota still documents { allowed, overage } and
defines overage as how far usage exceeds the limit, which does not say whether
exceeding it is allowed, so the soft-limit policy stays hidden.

Graders, workspaces and intents are unchanged, so this isolates one variable:
whether the gate changes behaviour when nothing cues the agent that a convention
it cannot see decides the task.
2026-10-04 22:40:43 -04:00
montjeffrey c9f270def1 docs(gateguard): report the 48-trial hidden-intent result
With one decisive fact per scenario the corpus isolates the mechanism:
obtaining the deciding fact predicts the outcome 14/14 in both arms, and not
obtaining it predicts failure in 9 of 10 and 8 of 10. Against that, the gate
changes nothing measurable. Asked the user 14/24 against 15/24, obtained the
deciding fact 14/24 against 14/24, passed 15/24 against 16/24, every exact
McNemar p = 1.000, and the paired pass-rate difference 5 points in off's favour
with a 95% interval of -25% to +35%. At 20 pairs a 40-point effect is ruled out;
a 15-to-20-point one is not.

The gate does fire - denials in 22 of 24 gated trials - so the failure is in the
conversion. timezone-daily-rollup fired four times out of four and produced no
question at all. Every question the gate raised is about code structure, while
every deciding fact in this corpus is an external contract.

Record the two scenario-design faults the earlier runs exposed, and the bound
the correction imposes: moving shape into the prompt also signposts the
ambiguity, which raises asking in both arms, so the claim is limited to the
signposted regime.
2026-10-04 22:40:42 -04:00
montjeffrey b7c1fe3eeb fix(gateguard): one decisive fact per scenario, and no undisclosed shape in a grader
Three of six scenarios sat at floor in the 48-trial run, and the run data says
the cause was the scenarios, not the agents.

csv-export-delimiter was asked about in 5 of 8 trials yet disclosedDecisive
never fired: the simulated user disclosed delimiter five times and lineending
twice, and quoting never. disclosedDecisive requires every decisive fact, and a
user who answers only what is asked and never volunteers cannot deliver three of
them to one question. username-collation failed the same way, with only case
ever disclosed.

soft-limit-overage failed differently and worse: the facts reached the agent in
4 of 8 trials and every trial still scored 0, because the grader demanded an
{ allowed, overage } shape that nothing disclosed. It was scoring whether the
agent guessed field names, not whether it learned the policy.

So each scenario now carries exactly one decisive fact, which is one hidden
decision a single well-aimed question can obtain, and anything a grader checks
about shape is stated in the prompt. The three working scenarios are changed the
same way, so disclosedDecisive means the same thing across the corpus: their
second and third decisive facts move into the prompt or become non-decisive
colour.
2026-10-04 22:40:42 -04:00
montjeffrey 2d7a5313c3 fix(gateguard): do not charge the ungated arm with gate-specific holes
silence-hole and coverage-hole are claims about the gate, and an ungated arm
never fires it, so classify labelled every ungated miss a coverage-hole by
construction. In the 48-trial run that padded the hole counts with five rows
that say nothing about the gate: coverage-hole read 7 when only 2 were the
gate's. Label an ungated miss unaided-miss, which is the baseline rate and is
excluded from the holes list.
2026-10-04 22:40:42 -04:00
montjeffrey f2c0cc8734 test(gateguard): add five blind-authored hidden-intent scenarios
One scenario was the whole blind corpus, which cannot show whether the gate
generalises. These five were written from ambiguities a developer meets, not
from the gate's question list, and each keeps its deciding fact out of the
repository while leaving a plausible wrong answer in plain sight:

- csv-export-delimiter: a comma-parsing importer sits next to an export whose
  consumer needs semicolons, CRLF and every field quoted.
- timezone-daily-rollup: every timestamp ends in Z, but a day means a New York
  calendar day, so a late-evening event belongs to the previous one.
- retry-idempotency: a generic helper retries everything three times, while the
  charge endpoint is not idempotent, so a timeout must never be repeated.
- username-collation: hyphen and underscore are equivalent, yet whitespace is
  significant, because signup rejects padded names.
- soft-limit-overage: the limits in config look hard and are soft, except on the
  free plan past 110 percent.

Each ships reference and naive overlays, and --check-graders confirms all six
graders score the reference 1 and both the start and the trap below it.
2026-10-04 22:40:42 -04:00
montjeffrey acf4980442 fix(gateguard): report intervals and an exact test next to the hole counts
The hidden-intent report printed bare counts. run.js reported Wilson intervals,
a bootstrap paired difference and an exact McNemar test, and that machinery was
lost in the new runner, so four trials rendered as a table that reads like a
result. On this run it is not one: gate 1/2 against off 0/2 gives intervals of
9%-91% and 0%-66%, a paired difference of -50% with a 95% interval spanning
-100% to 0%, and McNemar p = 1.000.

Reuse lib.js's pairedComparison and ai-eval-lib's wilson rather than adding a
second statistics path, and state the shortfall in words whenever a comparison
has fewer than twenty pairs: about 20 trials per arm are needed for 80% power on
a 40-point pass-rate difference at a two-sided 5% level, about 36 for 30 points,
and about 160 for the 15-point threshold the pilot pre-registered.

Mean turns and cost sit in the same table, because the only efficiency figure
this harness has produced points the other way: the gate roughly doubles turns
and cost per trial.
2026-10-04 22:40:42 -04:00
montjeffrey bfd4f8255a fix(gateguard): extract the arm tree without handing tar a drive letter
materializeTree passed the archive as an absolute path. GNU tar, which Git for
Windows bundles and puts first on PATH in Git Bash, reads an -f argument
containing a colon as a host:path remote spec and fails with "Cannot connect to
C: resolve failed". bsdtar, the tar in system32 that PowerShell resolves, has no
remote-spec parsing, so the fault only appeared under a bash-based shell - and
there it failed a committed test, not just a dry run.

Name the archive relatively and run tar with dest as its cwd, which extracts
identically under both tars and makes -C unnecessary. git archive keeps the
absolute -o path, which git handles on Windows.

Two tests, because the environment-dependent kind of fault needs both: one
injects a spawn and asserts no tar argument is absolute or drive-lettered, which
holds the invariant on any platform; the other prepends Git's bundled GNU tar to
PATH and extracts into a drive-letter destination, so the original failure is
reproducible from PowerShell too, where it was previously invisible.
2026-10-04 22:40:42 -04:00
montjeffrey e716f7ea65 test(gateguard): use the repository's own test helper
The suite was the only file in tests/docker using node:test. The repository's
dominant convention, in 229 of 327 test files including the sibling
gateguard-effectiveness suite, is a local test() helper that counts passes and
sets process.exitCode, which is what tests/run-all.js reads.

Adopt that helper verbatim, along with the sibling's tempDir wrapper so the
scenario fixtures resolve symlinked temp paths. Assertions are unchanged.
2026-10-04 22:40:42 -04:00
montjeffrey 63d812ed97 fix(gateguard): make the hidden-intent runner safe to scale
Four defects found by running the repository's own checks and one more trial
against it.

ESLint rejected an unused `armSettings` import in intent-eval.js; the caller
supplies `armSettingsByName` already.

A timeout was lost whenever a later turn came back clean: `a || b ? c : false`
evaluates the condition first, so `timedOut` was recomputed from the new turn
instead of being kept.

Isolating each trial's config directory, to stop persisted sessions piling up
under the projects folder, puts the credentials out of reach. Every turn then
returns "Not logged in", and the harness graded that as a coverage-hole - an
auth failure recorded as a finding. Keep the real config directory and delete
only the session folders this run's work root created, and adopt lib.js's own
guards: a trial that is unauthenticated, or whose gated arm edited files with no
metrics line and no denial, stops the run instead of being recorded.

Record denials per turn as well as in total. The hook marks a file checked once
per session and --resume keeps one session id for the conversation, so a zero
after a non-zero is the latch rather than the gate staying quiet, and
silence-hole has to be read with that column in view.

Verified: a real trial authenticates, produces a graded silence-hole, and leaves
the projects directory at the same count it started with.
2026-10-04 22:40:42 -04:00
montjeffrey 5588e27646 docs(gateguard): report the pilot null result and the hidden-intent design
The pre-registration said results would be reported whichever way they came
out. They came out empty: 72 sessions in which all four arms passed 18 of 18
with mean score 1.000, every arm-against-gate difference 0% (0% to 0%) and
McNemar p = 1. With the outcome fixed for every trial there is no variance to
explain, so this is the absence of a measurement rather than weak evidence that
the gate does nothing. The run does establish the gate's cost in that regime:
about 45% more turns and 28% more tokens for no outcome change.

Record the three reasons it measured nothing - deciding facts hidden in the
repository, the asking channel switched off, and a pass criterion written in
the gate's own vocabulary - and the hidden-intent design that addresses them,
with its hole classes, its first four-trial run and its own limits.
2026-10-04 22:40:42 -04:00
montjeffrey 7afde35e49 feat(gateguard): measure question effectiveness against a hidden user intent
The existing harness cannot measure what it set out to measure. Every trap
hides its deciding fact in the repository, which agents reliably discover, and
trials run one-shot under "do not ask clarifying questions", so a fact-forcing
gate is evaluated with no user to answer it. Its scenarios also declare the
question the gate ought to raise, chosen by reading the gate's own taxonomy, so
the suite passes by construction and can never find a missing question.

Add a second runner that measures effectiveness instead of coverage:

- scenarios-intent/ keeps the deciding fact in intent.json, as a choice between
  plausible conventions the repository cannot settle, so an agent that does not
  ask can only guess and the ungated arm cannot sit at the ceiling.
- user-sim.js plays that user with a separate cheap model: it answers the
  question in front of it, never volunteers, and reports which fact ids it
  disclosed, so asking something and asking the deciding thing are told apart.
  An unparseable judge is a recorded failure, not a silent stonewall.
- session.js runs multi-turn sessions joined by --session-id and --resume, and
  tells every arm the user is reachable, so the comparison is symmetric and the
  gate's contribution is whether it makes the agent ask the deciding question.
- coverage.js classifies each trial from behaviour alone into working, lucky, or
  a follow-through, targeting, silence or coverage hole, and reports the
  questions the gate actually raised rather than asserting declared ones.
- run-intent.js refuses minus-target, which needs targets these scenarios
  deliberately lack, and checks every grader separates the start, the trap and
  the reference before a session is billed.

A scenario counts as carrying no information only when nothing varies across
arms. Trap strength is reported separately: the ungated arm failing is what a
working trap looks like, so it must not make a scenario unusable.
2026-10-04 22:40:42 -04:00
Jeffrey Montoya 0dad22f5ac docs(gateguard): describe the question effectiveness evaluation 2026-10-04 22:40:42 -04:00
Jeffrey Montoya 967daec0d6 feat(gateguard): measure question effectiveness with trapped agent tasks 2026-10-04 22:40:41 -04:00
montjeffrey 6dbf6e6883 fix(gateguard): report fresh-process failures and fail the gate on them
The previous fix stopped a throwing hook from aborting the cold replay by
recording it as an allow, matching the warm path. But coldSummary only counted
disagreements, so a failure on a step the worker also allowed was no
disagreement and left no trace: the report showed no failure, a run with no
measured step reported a cold latency of 0 ms because percentile of an empty
list is 0, and the exit status passed. Trading a crash for a clean-looking
success hid exactly the failure the crash had exposed.

coldSummary now counts errors and samples, and reports no latency when nothing
was measured rather than a 0 that reads as a fast hook. The report prints "no
valid sample" in place of a latency and adds a line for steps that failed in a
fresh process, beside the existing disagreement line. The exit check moves into
gateFails, which fails on cold errors as it already did on warm errors and cold
disagreements, and is exported with coldSummary so both can be tested.

The render test builds its report from summarize([]), the real summary shape,
so it keeps working as metrics are added.
2026-10-04 22:34:30 -04:00
montjeffrey 3a4a574609 fix(gateguard): a throwing hook no longer ends the fresh-process replay
measureColdLatency ran each step in a fresh process through execFileSync,
inside try/finally with no catch. A hook that threw made the child exit
non-zero, execFileSync threw, and the error left measureColdLatency and then
evaluate, so neither the report nor the requested SARIF was written for a run
that had hit a single bad step.

The probe now catches a throwing require or run itself and records it the way
workerMain does: the raw payload stands in for the result, so the step reads as
an allow, and the error is kept. Matching the warm path matters because
coldSummary counts every cold decision that differs from the warm one as a
disagreement; any other choice would have reported disagreements that do not
exist. A child that dies without writing parseable output is caught outside the
probe and recorded the same way with no latency, and coldSummary leaves
unmeasured steps out of the percentiles while still counting them.

The new tests run a hook that always throws through both passes. They fail
against the previous code, 2 of 17, and pass against this one.
2026-10-04 22:22:50 -04:00
montjeffrey 56a516c2c5 Merge upstream/main into feat/gateguard-fewer-round-trips
Brings in the three commits main gained since the branch's merge base: the 2.2.3
release, retiring the Everything Claude Code name for ECC, and the ja-JP sponsor
update. 227 files arrive from main; two conflicted.

CHANGELOG.md: the branch had added an Unreleased section and main a released
2.2.3 section, both at the top. Both are kept verbatim with Unreleased above
2.2.3, since none of the branch's entries shipped in that release.

manifests/context-packs/skill-triggers@1.json: both sides had recomputed
registryDigest from the same earlier value to different results, and neither is
the digest of the merged tree. It is recomputed with loadContextRegistry over the
merged tree and only that field changes; triggers and triggersDigest are
untouched on both sides. validate-context-profiles passes.

All twelve CI validators pass. The full suite reports 44 failures in seven files;
43 of them fail by name and count identically on an unmodified upstream/main, and
the remaining one, plugin-hook-bootstrap, fails only inside run-all and passes in
isolation on both trees. The merge introduces no failing test that main does not
already have.
2026-10-04 20:47:30 -04:00
montjeffrey 8a7d61299c feat(gateguard): ask what outside the repository fixes a format or semantics
Across 240 evaluation trials every question the gate raised was about code
structure - importers, callers, public API, data schemas - while every deciding
fact in the hidden-intent corpus was an external contract: a consumer's CSV
dialect, a reporting timezone, retry safety against a non-idempotent endpoint,
collation rules, a quota policy. Obtaining the deciding fact predicted the
outcome almost perfectly in both arms, and the gate did not reliably cause it.

Add an external-contract question for code targets whose change profile touches
data, beside data-schema rather than replacing it: data-schema asks what the
change touches, external-contract asks who decides what it has to be. It reuses
the existing touchesData signal, so it stays as selective as the question it
sits with and adds no new detection, and it therefore does not reach a change
with no data involvement - pure retry semantics, for instance - which is a known
limit to be measured rather than guessed at.

Code edits that touch data now ask four questions instead of three, so the
condensed hint for an unknown profile changes too. That string was pinned by a
test as legacy-stable; the pin is updated deliberately, because a hint that
names fewer questions than the gate asks is worse than a hint that changed.

Suites: target-class 31, fact-force 1019, scenario-gate 25, eval 15, and the two
effectiveness suites 29 and 26, all passing. The 23 relevantQuestions
annotations that listed data-schema now list external-contract as well.
2026-10-04 18:13:02 -04:00
Affaan MustafaandGitHub ef648e0189 docs(ja-JP): match current sponsors (CodeRabbit, Greptile, Moonshot AI, Itô, SerpApi) (#3363)
Atlas Cloud and Mike Morgan move to past sponsors, as in README.md, SPONSORS.md
and ecc.tools. SerpApi added to the partners row.
2026-10-01 21:01:14 -05:00
Affaan MustafaandGitHub c05b2d6614 chore: retire the Everything Claude Code name, ECC only (#3330)
* chore: retire the Everything Claude Code name, ECC only

Replace the expanded product name and the everything-claude-code slug
with ECC across READMEs, translated docs, harness folders, skills,
commands, scripts and examples. Repo URLs now point at affaan-m/ECC.

Rename repo-internal artifacts to ecc forms (ecc-conventions skill,
ecc-guardrails, ecc-research-playbook, ecc-team-config, ecc-instincts
with ecc-* ids) and update .claude/ecc-tools.json.

Legacy install ids, plugin dirs, package names and MCP prefixes stay
where existing installs resolve against them. The codex global-state
check and harness-audit repo detection accept the new name first and
the legacy one as a fallback. The Discord bot prefers ~/GitHub/ECC and
falls back to the nested legacy checkout path.

Refresh the skill-triggers registryDigest for the changed skill text.

* fix(ecc-tools): drop managed paths to the removed Claude repo skill

.claude/skills/<repo-skill>/SKILL.md was deleted in #2770 but stayed listed in
ecc-tools.json and the team config. Remove those entries and point the Claude
Code adapter at the existing .agents/skills/ecc-conventions/SKILL.md.
v2.2.3
2026-10-01 17:50:41 -05:00
Affaan MustafaandGitHub 0348d7b6d7 chore(release): 2.2.3, ECC-only naming in pi/core and a longer npm publish wait (#3329)
pi/core and the bundled blueprint skill now refer to the project as ECC only;
the generated README no longer expands the name. Profile contents, package
name and paths are unchanged (123 skills, 24 commands).

The release workflows poll npm for up to five minutes after publish before
verifying the artifact. In 2.2.2 the check gave up after about 30 seconds while
npm was still processing, failing the run before latest was promoted and the
GitHub Release was created.

Version metadata synced to 2.2.3 with CHANGELOG, translated release headings
and release notes; skill-triggers registryDigest updated for the blueprint text.
2026-10-01 17:49:51 -05:00
Jeffrey Montoya ab45ed1edf test(gateguard): pin questions for classes that skip profiling 2026-10-01 02:15:54 -04:00
Jeffrey Montoya 32a6ffe943 docs(gateguard): report p50 and p90 with intervals per call type 2026-09-30 20:27:36 -04:00
Jeffrey Montoya b821262997 feat(gateguard): compare hook latency with confidence intervals 2026-09-30 20:27:36 -04:00
Jeffrey Montoya 33624cac18 docs(gateguard): refresh latency results 2026-09-30 12:13:18 -04:00
Jeffrey Montoya 2ec52af6d0 perf(gateguard): profile a change only when used and check its window first 2026-09-30 12:13:18 -04:00
Jeffrey Montoya d39fcd8ac3 refactor(gateguard): read the transcript tail through a small shared module 2026-09-30 12:13:10 -04:00
Jeffrey Montoya ef32c8e9ee docs(security): refresh GateGuard evidence for the latency changes 2026-09-30 11:36:10 -04:00
Jeffrey Montoya d95aa3425e feat(gateguard): measure hook latency in fresh processes 2026-09-30 11:35:49 -04:00
Jeffrey Montoya eab92ed21d perf(gateguard): skip plain character runs when lexing code 2026-09-30 11:35:49 -04:00
Jeffrey Montoya b7f80ff012 perf(gateguard): load modules and parse searches only when a call needs them 2026-09-30 11:35:49 -04:00
Jeffrey Montoya 4b02f66951 docs(gateguard): refresh evaluation and security evidence 2026-09-30 00:24:50 -04:00
Jeffrey Montoya f19425dbf1 docs(gateguard): move change-profile notes into their own file 2026-09-30 00:23:08 -04:00
Jeffrey Montoya b15c0538f6 refactor(gateguard): split change profile and search filters into focused modules 2026-09-30 00:23:08 -04:00
Jeffrey Montoya 3055eb3746 fix(gateguard): link SARIF findings to the evaluated corpus 2026-09-30 00:04:11 -04:00
Jeffrey Montoya 1221a8727f fix(gateguard): honour directory-qualified include globs 2026-09-30 00:04:11 -04:00
Jeffrey Montoya 8438f040ac docs(security): add AgentShield evidence for GateGuard fact-force 2026-09-29 23:30:41 -04:00
Jeffrey Montoya a202f31b78 docs(security): add GateGuard fact-force security evidence 2026-09-29 22:20:41 -04:00
Jeffrey Montoya 216fe89c36 feat(gateguard): write scenario gate results as SARIF 2026-09-29 22:20:41 -04:00
Jeffrey Montoya 8a1f763184 test(gateguard): create file symlinks for the hard-link case on Windows 2026-09-29 21:50:27 -04:00
Jeffrey Montoya d4045c7370 docs(gateguard): document NotebookEdit, hard links and exempt precedence 2026-09-29 21:50:27 -04:00
Jeffrey Montoya 10639a0c9e fix(gateguard): gate NotebookEdit and hard-linked targets
NotebookEdit is gated as a first-touch edit of notebook_path.
A file with more than one hard link gets no credit, trivial, sibling or cap allowance.
2026-09-29 21:50:26 -04:00
Jeffrey Montoya 4e33e29005 test(gateguard): use descriptive helper and fixture names 2026-09-29 21:50:26 -04:00
Jeffrey Montoya 7a8d8437ef test(gateguard): add comment-context and exported-member scenarios 2026-09-29 21:50:26 -04:00
Jeffrey Montoya 20eb2395e9 fix(gateguard): gate MultiEdit by its file_path and widen surface checks
Also refuses rg -z as read-only and never writes metrics through a symlink.
2026-09-29 21:50:26 -04:00
Jeffrey Montoya 6e677b008c fix(gateguard): judge comment-only edits against the file they change 2026-09-29 21:50:25 -04:00