Codex workspace-cwd refusal is an ancestor test, not a byte test (#481, finding 2 of 2) - #491
Conversation
The refusal predicate was "a `workspaces` key was substituted and it is not byte-equal to the in-band cwd", so the completely ordinary shape "a session running in a subdirectory of its declared workspace" emitted `plugin.codex.usage_policy_workspace_cwd_refused` at `warn` on every single turn, with both directories clean and no `.hypignore` anywhere. A privacy warn that fires constantly on the common case is read as noise, and the signals beside it are read as noise with it. Narrow it to keys off the in-band cwd's ancestor chain. The justification is not "close enough": when the key is an ancestor, the cwd's `.hypignore` walk passes through the key and every machine-local entry governing the key also governs the cwd, so resolving the cwd is at least as restrictive as resolving the key would have been. The refusal can only tighten, so there is nothing to report. Off the chain the two walks are incomparable, in both directions - a sibling tree, and a key BELOW the cwd whose own walk covers strictly more - and those still warn. The test is the shared `isEqualOrDescendant` (LLP 0069 R8), not a second copy of the path rule. Lexical rather than spelling-agnostic on purpose: `scopeGoverns` buys its extra reach with realpath syscalls this per-exchange seam must not spend (LLP 0049 R6), and the residue errs toward reporting. LLP 0160 records the decision, the ancestor-monotonicity argument, and what is now stale in LLP 0083's #476 limit sentence; LLP 0083 gains the forward-ref. Does NOT address the other finding deferred from PR #477: a row recorded where it used to drop still carries an ignored workspace's identity through the key's surviving enrichment role. That is a privacy-relevant default the corpus does not settle, and it stays open under #481. Co-Authored-By: Claude <noreply@anthropic.com>
The behaviour is kept; the reason given for it was wrong and is replaced. LLP 0160, the `workspaceCoversCwd` JSDoc and the warn-site comment all claimed that when the key is an ancestor of the in-band `cwd`, `resolve(cwd)` is at least as restrictive as `resolve(key)`, so refusing the substitution "can only tighten". That is false on this resolver. Nearest-governs means a declaration between the key and the `cwd` overrides the key's and may be less restrictive. Swept against the real `createUsagePolicyResolver` over `.hypignore` bodies and machine-local list classes at four depths on one ancestor chain: 131 of 576 arrangements resolve the `cwd` LESS restrictively than the key, spanning ignore->local-only, local-only->full and ignore->full. Restated on the ground that actually holds: an ancestor key is not a guess about where the session ran, it is a less specific name for the same tree, and nearest-governs makes the `cwd`'s own declaration authoritative. So the signal reports the location inference, not a verdict change - and the residue (an ancestor key resolving more restrictively than its `cwd`, now silent) is disclosed rather than implicitly denied, and pinned by a test. Also corrected: - the symlink residue cited #479 as an open gap; #479 was closed for the gate by #482/#484. It is knowingly retained here, at this reporting-only predicate, and the converse direction (a lexical descendant that is really a symlink out of the tree reads as covered, so stays silent) was undisclosed. - LLP 0160 and LLP 0083 pointed the undecided enrichment question at #481, which this PR closes. It is split out to #492. Co-Authored-By: Claude <noreply@anthropic.com>
Neutral review - round 1 of 2Verdict: changes made, behaviour kept. The predicate change is right and I did not
Everything labelled executed below was run. Everything labelled reasoned was not. Finding 1 - HIGH - the load-bearing monotonicity claim is false (fixed)The PR, LLP 0160 §decision, and the
This is not true. Both of its two premises are individually correct and jointly
The codebase already knows this. Sweep (executed)
Minimal cases: End-to-end through the real projector (executed)
So there is a real, supported configuration where the substitution is refused, the Disposition: justification replaced, behaviour keptI did not change the predicate. On the evidence the behaviour is still right, What was wrong was the reasoning, and what it concealed. Replaced with the ground that Design note, recorded not acted on: if you do want the verdict-change signal, the Finding 2 - MEDIUM -
|
| mutation | result |
|---|---|
workspaceCoversCwd -> return true |
test 44 fails, plus Codex live projector: the workspace-key substitution can feed the .hypignore gate a directory the session never ran in #476 case c |
make it bidirectional (silences only the key-below-cwd half) |
test 44 fails |
revert to the old pathsEqual byte test |
test 43 fails, alone - reproduces the PR body's evidence exactly |
npm test / npm run typecheck (executed, both worktrees). The 8 failures are
pre-existing: pristine origin/master (73b4618) fails the identical 8 by name,
all in test/core/leave-command.test.js (leave after join ..., leave clears an applied central slot ..., and six more - diffed as sorted name lists, zero
difference). Typecheck clean.
| tree | pass | fail |
|---|---|---|
origin/master 73b4618 |
3028 | 8 |
| this head fe46515 | 3030 | 8 |
| after my fixes 5bfc464 | 3031 | 8 |
(Note for anyone reproducing: there is no lockfile, so npm ci fails; I linked
node_modules from the main checkout. Without deps the suite silently under-collects
to 1760 tests and reports 147 spurious failures.)
What I changed
Pushed 5bfc464 to fix/issue-481. No behaviour
change: the predicate, the gate, the row and the drop path are untouched.
llp/0160-...decision.md- false proof replaced by the justification that holds;
new paragraph disclosing the non-monotonicity with the sweep numbers; symlink residue
restated in both directions with Usage-policy matcher never canonicalizes: a symlinked spelling of an ignored directory escapes its .hypignore (all adapters, hyp purge, query visibility) #479's status; §not-decided repointed to Enrichment from an ignored workspaces key puts an opted-out tree's identity on a clean-cwd row (deferred finding 1 from #481) #492.hypaware-core/plugins-workspace/codex/src/exchange-projector.js- warn-site comment
andworkspaceCoversCwdJSDoc corrected to match.llp/0083-...decision.md-Extended-byrepointed to Enrichment from an ignored workspaces key puts an opted-out tree's identity on a clean-cwd row (deferred finding 1 from #481) #492.test/plugins/codex-exchange-projector.test.js- new test pinning the residue, so
"an ancestor key resolving more restrictively than itscwdis silent" is a checked
decision rather than an accident waiting to surprise someone.
Verified positively in the committed remote tree (not merely by a green suite):
git show origin/fix/issue-481:<file> reports 0 occurrences of "at least as
restrictive" and 0 of "can only tighten" in both the LLP and the projector; 1
occurrence of "131 of 576" in the LLP; 2 of "not monotone" in the projector; 1
of the new test. Remote head confirmed 5bfc464.
Still open for the author
The PR body itself still carries the false claim ("So resolve(cwd) is at least as
restrictive as resolve(key) would have been ... refusing the substitution can only
tighten, never loosen"), and still points finding 1 at #481 without mentioning #492. I
deliberately did not rewrite the narrative - please update those two passages so the
body matches the corrected LLP.
Otherwise this looks good, and the scope is stated honestly: it fixes finding 2, says
so, and the absence of finding 1 is a disclosed deferral, not a gap.
…s round 1 missed, and ground the retained justification by execution Round 1 replaced the false "an ancestor key can only tighten" proof in LLP 0160 §decision and in the `workspaceCoversCwd` JSDoc, but the same claim survived in three other places, one of them LLP 0160's own abstract, where it contradicted the §decision body directly: - `llp/0160-...md:13` - the summary blockquote still said "An ancestor key cannot have changed the `.hypignore` verdict". - `llp/0083-...md:144` - the `Extended-by` block said the same. - `test/plugins/codex-exchange-projector.test.js` - the `@ref LLP 0160#decision [tests]` block said "taking its own `cwd` over an ancestor key can only tighten the verdict". A ref that states a disproved premise is exactly what CLAUDE.md's "keep refs honest" rule is for. All three now state the ground that actually holds, and say explicitly that it is NOT a monotonicity argument. Round 1 also carried the disposition on a reasoned claim: "in every one of the 131 cases the loosening is the user's own nested declaration". Two changes: 1. A better, EXECUTED ground is now stated and pinned by a test. The warn this PR narrows carries no usage class at all. On `origin/master` it fires with an identical field set on a subdirectory turn with no `.hypignore` anywhere, on one where key and `cwd` both resolve `ignore`, and on the loosening arrangement. It could never have distinguished them, so narrowing it removes a constant, not privacy information. The genuine-refusal test now asserts the warn carries no `class`, `declared` or `governed_by`. 2. The provenance claim is corrected where it was too strong. The machine-local list, the only source that reaches an explicit `full`, has exactly two writers, both behind explicit `hyp ignore`/`unignore`/`policy set` verbs, and LLP 0071 §not-central forbids anything central writing one - so the `->full` transitions really are the user's own. But a `.hypignore` is a COMMITTABLE file by design (LLP 0071 §not-dotfiles) and the ancestor walk has no vendored-tree exclusion, so the `ignore->local-only` transition can come from a dependency's own file. Bounded (the walk only goes up, and `.hypignore` cannot express `full`), unchanged by this PR, and now disclosed rather than asserted away. Also notes that the 131/576 count's mixed slice is enumeration-dependent; the two source-pure slices, 50 and 75, reproduce exactly. No behaviour change: predicate, gate, row and drop path untouched. Co-Authored-By: Claude <noreply@anthropic.com>
Neutral review, round 2 of 2 (next rung:
|
| arrangement | resolve(key) |
resolve(cwd) |
loosens | origin/master warns |
this head |
|---|---|---|---|---|---|
no .hypignore anywhere |
full |
full |
no | 1 | 0 |
ignore at key and at cwd |
ignore |
ignore |
no | 1 | 0 |
ignore at key, local-only at cwd |
ignore |
local-only |
yes | 1 | 0 |
Field set on master, identical in all three:
["component","cwd_sha256","error_kind","exchange_id","operation","workspace_sha256"].
No class, no declared, no governed_by.
So on master nobody could ever have told a nested loosening from an ordinary
subdirectory turn by this signal. Narrowing it removes a constant, not privacy
information. And the underlying exposure - a vendored .hypignore local-only
downgrading a tree the user marked ignore - is a property of nearest-governs that
is byte-identical on master and on this head; the row records local-only either
way. It is also bounded twice: the walk only goes up, so a vendored file cannot
reach the parent project, and .hypignore cannot express full
(format.js:13 IMPLEMENTED), so a third party can downgrade ignore to
local-only but can never re-enable forwarding.
Conclusion: keep the predicate. Whether a third-party .hypignore should be
able to loosen a tree the user marked ignore is a real question, but it is a
resolver question for LLP 0049/0070, not a reporting one for this PR, and it is
not made worse here. I did not change the predicate.
Findings
Finding A - MEDIUM - the disproved monotonicity claim survived in three more places, one of them LLP 0160's own abstract (fixed)
Round 1 corrected llp/0160 §decision and the workspaceCoversCwd JSDoc. It
missed three others, which meant the document contradicted itself:
| file:line | text |
|---|---|
llp/0160-...decision.md:13 (the summary blockquote) |
"An ancestor key cannot have changed the .hypignore verdict, so refusing it is not a refusal worth a warn." |
llp/0083-...decision.md:144 (Extended-by) |
"...ancestor chain, because an ancestor key cannot have changed the verdict." |
test/plugins/codex-exchange-projector.test.js:1321 (@ref LLP 0160#decision [tests]) |
"taking its own cwd over an ancestor key can only tighten the verdict" |
The abstract is the worst of the three: 0160:13 asserted exactly what 0160:50
says four lines into §decision is not the justification. And the test comment is
a live @ref stating a disproved premise, which is precisely what CLAUDE.md's
"keep refs honest" rule exists to catch.
Fixed. All three now state the ground that holds and say explicitly that it is
not a monotonicity argument.
Finding B - MEDIUM - the PR body still carried round 1's disproved symlink claim, and its counts were stale (fixed)
Round 1 corrected the symlink residue in the LLP and the JSDoc but not in the body,
which still read:
The residue - a symlinked spelling of the same tree still reads as a refusal - is
the status quo and errs toward reporting, never toward silence.
Round 1 disproved that second direction by execution. Also stale: "Two new
tests" (there are three), "# pass 60 / # fail 0" (61/0), "npm test: 3030 pass
... adds exactly the 2 new passes" (3031, 3), and "LLP 0160 - the decision, the
ancestor-monotonicity argument" (that argument was removed).
Fixed via gh pr edit 491 --body-file. Verified against the live body: 0
occurrences of any of the five stale strings, round-2 correction block present.
Finding C - LOW - the justification's provenance claim, softened not removed (fixed; see the verdict above)
Recorded rather than acted on unilaterally where it touches design: the predicate
is unchanged. What I changed is the prose, so LLP 0160 no longer claims something
executably false. It now (a) leads with the executed ground (the warn has no class
field), (b) splits the provenance question by source with the writer inventory, and
(c) notes the 131 count's mixed slice is enumeration-dependent.
For triage / a human, recorded not acted on: a .hypignore committed into a
dependency can downgrade ignore to local-only for its own subtree, and the
ancestor walk has no vendored-tree exclusion. That predates this PR and is
unchanged by it, but it may deserve its own issue against LLP 0049/0070.
Audit of the round-1 delta fe46515..5bfc464
-
The replaced justification (JSDoc + LLP 0160 §decision): accurate. Both
correctly state nearest-governs is not monotone, both correctly cite
selectGoverning's "a nested loosening". The residual overstatement is Finding C. -
The disclosed non-monotonicity: honest, and the numbers check out (see the
verdict; two of three slices reproduce exactly). -
The corrected symlink residue: verified in both directions. The LLP's new
wording matches what actually happens; the one-directional claim survived only in
the PR body (Finding B). -
#479→ status corrected: right. Usage-policy matcher never canonicalizes: a symlinked spelling of an ignored directory escapes its .hypignore (all adapters, hyp purge, query visibility) #479 is CLOSED; Follow-up: deferred review findings from PR #477 (ignored-workspace identity on newly-recorded rows; refusal warn fires every turn) #481 is OPEN; Enrichment from an ignored workspaces key puts an opted-out tree's identity on a clean-cwd row (deferred finding 1 from #481) #492 is
OPEN and correctly titled for the deferred enrichment fork. Both repointings are
accurate. -
The new test is NOT vacuous (executed mutation testing).
mutation test 45 (the new one) workspaceCoversCwd→return falsefails (also kills 43) workspaceCoversCwd→return truepasses (43/45 pass, 44 fails) walk→ most-restrictive ancestor instead of nearest (i.e. made monotone)fails The third is the important one: it proves the test pins the non-monotonicity
itself, not just the absence of a warn. -
I added one assertion to test 44, so the executed premise the whole
disposition now rests on is checked rather than stated: the refusal warn carries
noclass,declaredorgoverned_by. Mutation-tested - adding aclassfield
to the warn kills it.
Round-2 scope items 3 and 4
- PR body vs code and LLP: now agrees, after Finding B. Nothing else stale: the
Enrichment from an ignored workspaces key puts an opted-out tree's identity on a clean-cwd row (deferred finding 1 from #481) #492 pointer, theFixes #481trailer, theisEqualOrDescendantclaim, the
§not-fixed corpus reading and thehyp purge --subtreenote all check out. - LLP 0160's number is still free. Re-checked after my push against every
refs/remotes/origin/*ref:fix/issue-481is the sole claimant. The repo's own
llp-check.ymlduplicate-numbers command, run verbatim on the tree, returns
empty. LLP 0098, 0099 and 0111 each have two claimants; the renumber-vs-qualified-citation choice is unowned and needs a decision #469 is not aggravated. - Anchors and
@refs resolve.0160defines{#context} {#decision} {#not-decided} {#corrections-0083}; every@ref LLP 0160#...in the tree points
at#decisionor#corrections-0083. The new LLP 0071 links I added
(#not-central,#not-dotfiles) are explicit anchors that exist.- Pre-existing, not this PR: 29
@refs in these two files point at
LLP 0151#.../LLP 0035#...anchors that do not exist (0151 defines no
explicit anchors at all). Identical onorigin/master. Out of scope here,
but someone should file it.
- Pre-existing, not this PR: 29
Checks
| tree | npm test |
typecheck |
|---|---|---|
pristine origin/master 73b4618 |
3028 pass / 8 fail | clean |
| this head 5bfc464 | 3031 pass / 8 fail | clean |
| after my fixes 1685288 | 3031 pass / 8 fail | clean |
The 8 failures are pre-existing, all in test/core/leave-command.test.js
(leave after join ..., leave clears an applied central slot ..., and six more).
Diffed as sorted name lists against pristine origin/master: zero difference.
test/plugins/codex-exchange-projector.test.js: 61 tests, 61 pass.
PR #477's test "a refused workspace substitution still enriches the row from the
workspace key (#476)" passes (test 40) and still means what it did: its key
/work/clean/proj and cwd /work/clean/elsewhere are siblings, so a genuine
refusal under the new predicate too, and its assertions are about enrichment only.
#476 cases a/b/c (tests 37-39) also pass.
Style: no em dashes and no stray semicolons in the delta.
Reproducing note: there is no lockfile, so npm ci fails. Link node_modules
into any fresh worktree first; without it the suite under-collects to ~1760 tests
and reports ~147 spurious failures.
What I changed and how I verified it landed
Pushed 1685288 to fix/issue-481. No behaviour change: the predicate,
the gate, the row and the drop path are untouched.
llp/0160-...decision.md- abstract corrected; executed ground stated; provenance
split by source with the writer inventory; the 131 count qualified.llp/0083-...decision.md-Extended-bysentence corrected.hypaware-core/plugins-workspace/codex/src/exchange-projector.js- JSDoc's
"accepted because" clause regrounded on the executed fact.test/plugins/codex-exchange-projector.test.js- stale@refblock corrected;
three assertions added to test 44 pinning that the warn carries no verdict.
Verified positively against the committed remote tree (git show origin/fix/issue-481:<file>), not a green suite: remote head is 1685288;
"cannot have changed" / "can only tighten" / "at least as restrictive" appear 0
times in both LLPs and in the projector, and once in the test file only as my own
explicit negation ("NOT because taking the cwd can only tighten - it cannot only
tighten"); the new assertion, the executed-ground paragraph, the committable-file
disclosure and the corrected 0083 sentence are each present. PR body re-fetched
after edit: 0 of the five stale strings remain.
Round 2 of 2. The next rung is triage. Nothing here blocks: the predicate is
right, the disposition is now grounded on something executed rather than reasoned,
and the one place I disagreed with round 1 (the provenance claim) is a prose
correction plus a note for a human, not a design change. The open enrichment fork
(#492) still needs a human call and is correctly excluded from this PR's scope.
|
Correction to the round-2 review above. Its closing caveat - that 29 pre-existing Those anchors do exist. Both documents define them as inline Verified by running the repo's own gate rather than by grepping: The reviewer reached the wrong conclusion by applying the Nothing in the round-2 review's substantive findings depends on this caveat; findings A, B and C stand. |
Neutral triage (LLP 0017)Verdict: A — every residual is non-blocking. Both review rounds are read. Nothing here blocks. I re-verified the two things worth independently testing rather than accepting on the record, against the real code (real temp dirs, real Round 2's ground for keeping the predicate — verified trueRound 2's claim: the Independently reproduced end-to-end through the real projector: Field set is byte-identical across all three arrangements on Vendored-
|
One conflict, in `llp/0083`'s `Related:` header line: #462 added LLP 0066 and LLP 0067, this branch added LLP 0160. Resolved as the union, since the two edits are independent additions to a list. Everything else merged textually clean, including `codex/src/exchange-projector.js`, where the two sides turned out to touch disjoint concerns: #462 rewrote the rollout fallback's lookup key (session container to thread id, the new `resolveRolloutCwd`), while this branch changed `refused_workspace_cwd`'s predicate in `resolveCodexContext` and the warn's comment block above it. Checked rather than assumed: - `isEqualOrDescendant` is byte-identical on master and still exported from the usage-policy index, so the shared predicate this branch reuses (LLP 0069 R8) is the same function it was written against. - The two review-established facts survive. Nearest-governs is intact (nearest `.hypignore` walk byte-identical, deepest list entry via `matchDepth`), so the disclosed non-monotonicity is still real and the pinning test still has something to pin. The narrowed warn still carries no `class` / `declared` / `governed_by`, which is the executed ground the disposition rests on. - `scopeGoverns` and `canonicalSpellings` still sit next door after #484, so the deliberate choice of the lexical predicate on this hot path (LLP 0049 R6) remains a live choice rather than a stale one. - LLP 0160 §corrections-0083 says 0083's closing #476 sentence went stale. That bullet is byte-identical between the merge base and master, so the correction still lands where it says it does. - `pathsEqual` is still used by `selectCodexWorkspace`, so it is not orphaned by the predicate swap. Checks on the merge result: `npm test` 3081 pass / 8 fail, `npm run typecheck` clean. The 8 are `test/core/leave-command.test.js`, pre-existing: a pristine `origin/master` (bd7dd0b) worktree fails the identical 8 by name, diffed as sorted name lists with zero difference, at 3078 pass / 8 fail. The merge adds exactly the 3 new #481 passes and no new failures. Co-Authored-By: Claude <noreply@anthropic.com>
Neutral triage (LLP 0017), re-run at head
|
Conflict: `usableInBandCwd`'s "what this does not reach" JSDoc paragraph in `exchange-projector.js`, rewritten by both sides. Resolved by keeping both: this branch's "it bounds the SHAPE, never the provenance; the ranking at the call site is what keeps a guess from deciding" framing, master's "#476 closed, a request that states a `cwd` no longer reaches here through the key" and its `@ref LLP 0160#corrections-0083`. Master's closing clause "the key still outranks the rollout fallback (#480)" is the thing this branch changes, so it now reads as the ranking the demotion established. #491's behaviour is untouched: `workspaceCoversCwd`/`isEqualOrDescendant` and the narrowed warn-site comment merged clean and stand. Two prose reconciliations where each side's text asserted the other's pre-merge state: - `llp/0083` #481 limit said widening the refusal to the rollout was blocked on a frequency objection "already open". #491 settled that objection with the ancestor test, so the limit now says widening is unblocked but must reuse 0160's predicate rather than a byte comparison. - `llp/0160` §corrections-0083 called #480 "the open ranking question". It is answered here, so it points at 0083 §workspace-key-ranks-last. Co-Authored-By: Claude <noreply@anthropic.com>
Issue #481 carries two findings deferred from PR #477's round-2 review. This
PR fixes one of them. The other needs a human decision and is stated below.
Fixed: finding 2, the refusal warn fires on every turn of an ordinary subdirectory session
The predicate was "a
workspaceskey was substituted and it is not byte-equalto the in-band
cwd", so the commonest Codex shape there is - a session runningin a subdirectory of its declared workspace - emitted
plugin.codex.usage_policy_workspace_cwd_refusedatwarnon every turn, withboth directories clean and no
.hypignoreanywhere.It is now an ancestor test.
The predicate was not changed, and the behaviour is still the right one, but
on different grounds: an ancestor key is a less specific name for the same
tree, not a guess about where the session ran, so declining to substitute it
is not a refusal worth reporting.
When the key is an ancestor of the in-band
cwd, these two things do hold:cwd's.hypignoreancestor walk passes through the key and continuesabove it, so every governing file the key would have found, the
cwdfinds;cwd,because membership is equal-or-descendant.
What does not follow is that the resulting verdict is at least as restrictive,
because which of those governing declarations wins is decided by nearness,
not by union. Off the ancestor chain the two walks are incomparable in both
directions - a sibling tree, and a key below the
cwdwhose own walk coversstrictly more - and those still warn. Demoting to
infowas the other option inthe issue; it is rejected in LLP 0160 because it would quiet the interesting case
too, to fix a frequency problem that is really a predicate problem.
Shared predicate, reused, not copied. The test is
isEqualOrDescendantfromsrc/core/usage-policy/matcher.js(LLP 0069 R8), exported through theusage-policy index the projector already imports. It is deliberately the
lexical one rather than the spelling-aware
scopeGoverns/canonicalSpellingspair from #482/#484: those buy their reach with
realpathsyscalls, and thisruns once per exchange on the capture hot path that LLP 0049 R6 keeps free of
unbounded fs work. The residue is that this compares paths, not directories,
in both directions: a symlinked spelling of one tree reads as a refusal (a
spurious
warn), and a lexical descendant that is really a symlink out of thekey's tree reads as covered, so it stays silent. Both were checked against
the real projector. (An earlier version of this paragraph claimed it "errs toward
reporting, never toward silence"; round 1 disproved the second half.) This is the
gap LLP 0083 filed as #479 for the gate, which #482/#484 have since closed
there; it is knowingly retained here, at this reporting-only predicate, for the
hot-path reason above. Nothing about the gate's own verdict depends on it.
Evidence
Three new tests in
test/plugins/codex-exchange-projector.test.js, overreal temp directories and the real shared resolver
(
createUsagePolicyResolver, no injected fs, no mock), matching the bar theissue's own verification used.
Against pristine
origin/master(73b4618) with only the test file applied:On this branch:
Test 44 (the genuine refusals, both directions) passes on master too, on purpose:
it is the guard that the narrowing did not silence the signal it exists for. It
now also asserts the warn carries no
class/declared/governed_by, whichis the executed premise the disposition rests on.
Test 45, added in review, pins the disclosed residue: an ancestor key that
resolves more restrictively than its
cwdis silent, so thenon-monotonicity is a checked decision rather than an accident. Mutation-tested
in round 2: forcing
workspaceCoversCwdtofalsekills it, and making the.hypignorewalk monotone (most-restrictive ancestor instead of nearest) killsit too, so it is not vacuous in either direction.
PR #477's test "a refused workspace substitution still enriches the row from the
workspace key (#476)" uses a clean, unrelated workspace key
(
/work/clean/projagainst acwdof/work/clean/elsewhere, siblings), so itis a genuine refusal under the new predicate too. It is untouched and still
passes, and its assertions are still about enrichment only.
NOT fixed: finding 1, ignored-workspace identity on newly-recorded rows
A human has to decide this one. It is a privacy-relevant default and the LLP
corpus does not settle it, so guessing would be worse than reporting.
The trade, crisply: suppress enrichment sourced from a
workspaceskey thatitself resolves to
ignore- costing a second resolver lookup per exchange (theper-
cwdTTL cache absorbs it) and the graph-bridge identity of genuinelymulti-root sessions (LLP 0032 §capture) - or accept that a row whose
cwdisclean may name an opted-out tree's path, git remote, and head sha (identity only,
never content; the ignored tree's own sessions still drop).
What the corpus actually says, having read it rather than assumed it:
cwd, which makes the currentbehaviour defensible but says nothing about enrichment sources.
Follow-up: deferred review findings from PR #477 (ignored-workspace identity on newly-recorded rows; refusal warn fires every turn) #481 - i.e. it recorded the behaviour and explicitly deferred the decision
rather than taking it.
bridge, which is what suppression costs.
privacy signal; here the signal is perfectly interpretable and the exchange did
not originate in the flagged tree, so it does not reach.
Nothing decides it. LLP 0160 §not-decided records the fork, and it is tracked as
its own issue #492 (labelled
neutral:stuck, awaiting a suppress-or-acceptcall), not under #481 - this PR's
Fixes #481trailer would otherwise retirethe question along with the issue. One point worth weighing that the issue does
not raise: a
cwd-scopedhyp purge --subtreeof the ignored tree would not catch theserows, so accepting also means accepting that the identity is not reachable by the
obvious cleanup verb.
LLP
where the session ran (and explicitly not a monotonicity argument), the
measured non-monotonicity it gives up and on whose declaration that turns, why
not
info, why lexical not spelling-aware, and what it explicitly does notdecide.
Extended-byforward-ref (editorial only; it is Acceptedand immutable, so its prose is not rewritten). The issue's "also for whoever
merges" note - that 0083's
#476limit sentence went stale when Codex live projector: an explicit cwd outranks a substituted workspace key at the .hypignore gate #477 landed -is recorded as LLP 0160 §corrections-0083 for the same reason, and the mirrored
stale comment in
exchange-projector.jsis corrected in place (the residue isthe still-open Codex live projector: a workspaces turn-metadata header preempts the rollout session_meta.cwd, turning a correct .hypignore drop into a record #480, not Codex live projector: the workspace-key substitution can feed the .hypignore gate a directory the session never ran in #476).
Checks
npm test: 3031 pass / 8 fail. The 8 aretest/core/leave-command.test.js,pre-existing: pristine
origin/master(73b4618) fails the identical 8 by name(3028 pass / 8 fail), diffed as sorted name lists with zero difference. This
branch adds exactly the 3 new passes.
npm run typecheck: clean.master, on everyorigin/*ref,and against the tombstones. Re-confirmed in review round 2:
fix/issue-481isthe sole claimant, and the repo's own
llp-check.ymlduplicate-numbers commandrun verbatim on the tree returns empty.
npm cifails. Link or copynode_modulesinto any fresh worktree first; without it the suite silentlyunder-collects to ~1760 tests and reports ~147 spurious failures.
Held, not ready. Do not merge on my say-so.
Fixes #481