Codex live projector reads the cwd predicate from core, not a copy - #513
Conversation
) `usableInBandCwd` inlined its own `trim() && isAbsolute()` copy of the rule core exports as `sessionMetaCwd`. PR #474 added it deliberately and temporarily, while LLP 0150 was unmerged; #466 landed that predicate, so the copy now has an owner to defer to. Behaviour is unchanged. Only a non-empty string reaches the in-band seam (`readStringKey` and `firstString` refuse the rest upstream), and over that whole domain the two copies already agreed byte for byte. What changes is that there is one place left for the rule to drift from, which is the whole reason LLP 0150 exists: this exact rule, stated twice, shipped the wrong answer twice (#453, #459). The wrapper survives only for the refusal diagnosis: `error_kind` needs blank told apart from relative, and the shared predicate's single `undefined` cannot carry that. It now decides nothing. Docs: LLP 0083's bullet said the checks are restated locally and that 0150's predicate is not borrowed; LLP 0150's consequences listed the in-band path as "Still outstanding ... no such predicate", which #474 already falsified. Both now describe the shared predicate, and 0150 keeps its scoping caveat: sharing the rule was LLP 0083's call, not an invariant 0150 imposed. Co-Authored-By: Claude <noreply@anthropic.com>
…st LLP 0083 provenance Two review findings on PR #513, both in the round's own scope. R1 (test hygiene): the new equivalence test was the only case in `codex-exchange-projector.test.js` that built a projector with no injected resolver, so the shared matcher walked the REAL ancestors of `/work/repo` looking for a `.hypignore`. A file above the checkout on the machine running the suite would turn `projection.cwd` into a `USAGE_POLICY_DROP` sentinel and redden an assertion that has nothing to do with the gate. Inject a resolver whose fs holds no list anywhere, matching how every neighbouring case in the file works, and hoist the projector out of the loop. Re-checked the mutation after the change: loosening `usableInBandCwd` back to `cwd.trim().length > 0` still reddens the test on the "repo" case, so the hermetic resolver did not make it vacuous. R2 (doc accuracy): the LLP 0083 bullet claimed the local copy was written "while 0150 was still unmerged". It was not. LLP 0150 / `sessionMetaCwd` merged in #466 on 2026-07-29; the copy landed in #474 on 2026-07-30, and that commit's own docstring cited LLP 0150 `#usable-cwd` by anchor while explicitly declining to borrow it, on the scoping argument the bullet already states. Attributing the copy to timing rewrites the recorded rationale. Say what actually changed instead: the weight given to drift, not the scoping. Co-Authored-By: Claude <noreply@anthropic.com>
🤖 neutral review round 1 (head
|
| mutation | site | result |
|---|---|---|
| (none) | origin/master copy |
ok 1, passes before the fix, as claimed |
drop the isAbsolute conjunct (if (cwd.trim().length > 0) return cwd) |
origin/master copy |
not ok on "repo", the author's claim, confirmed |
sessionMetaCwd(cwd) → cwd.trim().length > 0 ? cwd : undefined |
this branch | not ok on "repo" |
| strip a trailing slash off the surviving value | this branch | not ok on "/work/repo/" |
So the test is not vacuous in either direction: it catches the fail-open drift
shape from #453/#459 and it pins byte-identity of the accepted value, which
the loosening mutation alone would not have shown. Re-ran the loosening mutation
after my own change to confirm the hermetic resolver did not defang it.
One structural limit, by design and worth naming: the test compares the projector
against sessionMetaCwd itself, so it can only catch drift in the projector,
never in the shared predicate. That is the right split (test/core/codex-rollout-session-meta.test.js owns the predicate), but it means this test is an
agreement pin, not a specification of the rule.
Findings
F1 (minor, test hygiene, fixed) test/plugins/codex-exchange-projector.test.js:257
The new test was the only case in the file that built createCodexExchangeProjector()
with no injected resolver. Every neighbouring case injects one
(ignoringResolver / clampingResolver, :14-45). Without it the real shared
matcher runs: existsSync/readFileSync walk the actual ancestors of
/work/repo (/work/repo, /work, /) hunting a .hypignore, plus a real
realpathSync and volume case probe. A .hypignore of class ignore anywhere
above the checkout makes project() return the USAGE_POLICY_DROP sentinel,
whose .cwd is undefined, reddening an assertion that has nothing to do with
the gate. It passes here, but it is environment-coupled.
Fixed: added a governsNothingResolver() helper beside the other two (a real
resolver over an fs that holds no list anywhere) and hoisted the projector out of
the loop. Mutation re-verified after the change.
F2 (minor, doc accuracy, fixed) llp/0083-codex-live-cwd-from-rollout.decision.md:85
The new bullet said the local copy was written "while 0150 was still unmerged".
That is not what happened, and the PR body repeats it ("because #466 had not
landed yet").
- LLP 0150 /
sessionMetaCwdmerged in One reader for the Codex session_meta header, in core (#465) #466 on 2026-07-29 (1555f13). - The copy landed in Codex live projector: an unusable in-band cwd is a miss, not a path (#471) #474 on 2026-07-30 (
05581df), a day later. - Codex live projector: an unusable in-band cwd is a miss, not a path (#471) #474's own docstring cited
LLP 0150 #usable-cwdby anchor and argued the
copy on scoping grounds: "They are restated here rather than borrowed from
there: LLP 0150 scopes the in-band path out of its own mandate." - Issue Codex live projector's usableInBandCwd should reuse sessionMetaCwd now that #466/LLP 0150 landed #478's own triage comment says it plainly: "
sessionMetaCwdis on
masternow (it landed with PR One reader for the Codex session_meta header, in core (#465) #466).usableInBandCwdis not."
So the copy was a deliberate scoping call made with the owner already available,
not a timing artefact. Attributing it to timing rewrites the recorded rationale,
which is the one thing an LLP is for. Rewritten to say what actually changed: the
weight given to drift, not the scoping. (I did not edit the PR body; the same
sentence there is now the only place the claim survives.)
Everything else checked, no findings
The surviving wrapper. Correct. The trim() at :1271 is reached only after
sessionMetaCwd already returned undefined, i.e. only when the value is blank
or non-absolute, and the refusal is settled by the return above it. It
decides nothing; it classifies. error_kind is right for every input class,
including the interesting one: ' /work/repo' trims non-empty, so
cwd_not_absolute, which is exactly what it is. The retained inline comment says
this and is honest.
LLP edits.
0150#usable-cwdexists (llp/0150...:115,{#usable-cwd}) and
0083#decisionexists (## Decision,:55). Both new@refs resolve; both
attach with no intervening blank line; the[tests]ref in the test file is
correctly placed above thetest(construct.- The 0083 bullet now matches the code on all three limbs, verified by reading:
rollout-cwd.js→readRolloutSessionMeta→parseRolloutSessionMeta, which
appliessessionMetaCwd(payload?.cwd)atrollout_session_meta.js:85. So
"reads the rollout through it" is true, not aspirational. - 0150's "Still outstanding" correction is right: Codex live projector: an unusable in-band cwd is a miss, not a path (#471) #474 did falsify it, and the
replacement keeps the scoping caveat rather than retconning 0150's mandate.
"All three sites" checks out (backfill.js:6,rollout-cwd.js:6,
exchange-projector.js:3). - Conventions permit the edits. 0150 is
Active; 0083 isAccepted, and
this repo does use "Accepted and immutable" language elsewhere (LLP 0133 about
LLP 0115). But that is not repo-wide practice: 0083 has been edited by five
merged PRs (Codex live cwd from session rollout: close .hypignore leak for subscription route (LLP 0083) #266, Codex rollout cwd keys on the thread, not the session container (#459) #462, Codex lineage reads the durable body client_metadata, not header names Codex never sends (#464) #467, Codex live projector: an unusable in-band cwd is a miss, not a path (#471) #474, Codex live projector: an explicit cwd outranks a substituted workspace key at the .hypignore gate #477), including this exact bullet by Codex live projector: an unusable in-band cwd is a miss, not a path (#471) #474
and Codex live projector: an explicit cwd outranks a substituted workspace key at the .hypignore gate #477, and CLAUDE.md's living-docs rule directs the doc edit into the same
commit as the code. No status flip is owed.
Scope vs #480 / PR #515. Clean. This diff's only code hunks are the import
block and usableInBandCwd (:1206, :1244). It does not touch
resolveCodexContext, readCodexTurnMetadata, selectCodexWorkspace, or the
gate expression at :134, the :723 / :130-131 lines PR #515 rewrites. Note
for whoever merges second: both PRs edit llp/0083, and #515's fix is expected
to trim the #476 "one limit of the rule" sentence that this PR reflows, so a
textual conflict in that bullet is likely. Code-wise they are disjoint.
House rules (CLAUDE.md). No semicolons, no U+2014 anywhere in the diff, JSDoc
only, no @typedef, no inline import('...') types. The new import
'../../../../src/core/codex/rollout_session_meta.js' is the repo-root-anchored
.js form and is byte-identical to how backfill.js:6 already imports the same
symbol. The node:path import is fully removed with no orphaned isAbsolute
reference left behind.
Checks
npm teston521cf88: 3079 pass / 8 fail, all in
test/core/leave-command.test.js(not ok 892-894, 896-900). Same 8 and no
others, matching the knownorigin/masterbaseline tracked as test/core/leave-command.test.js: 8 of 11 tests fail on master (exit 1 from hyp leave), red since the tests were introduced #512.npm testafter my fixes (d4255de): 3079 pass / same 8 fail. No new failures.npm run typecheck: clean, before and after.npm run smoke -- gateway_codex_capture: ok.
Pushed as d4255de. Not merging, not labelling, not touching the PR body.
# Conflicts: # hypaware-core/plugins-workspace/codex/src/exchange-projector.js
Review round 2 (merged head
|
🤖 neutral: #513 and #515 both edit
|
Root cause
usableInBandCwdinhypaware-core/plugins-workspace/codex/src/exchange-projector.jscarried its own copy of the usable-cwd rule:
That is the same rule core exports as
sessionMetaCwd(
src/core/codex/rollout_session_meta.js, LLP 0150#usable-cwd). PR #474 addedthe copy deliberately and temporarily, because #466 had not landed yet. #466 has
since merged, so the copy now has an owner to defer to.
This is a no-behaviour-change refactor, and here is the proof
Stating it plainly, as the review discipline asks: no test can distinguish the
two implementations, so none was written to fail. Only a non-empty string can
reach
usableInBandCwd(readStringKeyrequirestypeof value === 'string' && value.length > 0, andfirstStringrequires the same), and over that entiredomain the two predicates compute the same conjuncts in the same order and return
the surviving value byte-identical. The one place they differ, a non-string
input, where
sessionMetaCwdreturnsundefinedand the old copy would throw on.trim(), is unreachable through the typed call site.So the test proves equivalence instead, driving one table through both paths:
the shared predicate directly, and the projector's in-band seam via
project().The table is the whole of what the seam can be asked: absolute, absolute with a
trailing slash, unnormalized absolute, bare relative,
./,../, whitespace-only,tabs/newlines, and the one case the trim alone would wave through (
' /work/repo',non-blank but not absolute).
It passes before the fix, which is the point
And it is not vacuous: mutation-checked
A test that passes both ways proves nothing unless it would catch the drift it
exists to prevent. Dropping the
isAbsoluteconjunct from the old local copy,which is exactly the shape of the drift that shipped wrong in #453 and #459,
reddens it:
After the fix
What changed
usableInBandCwdcallssessionMetaCwdand trusts its return value as theverdict. The
node:pathisAbsoluteimport is gone; the core import replaces it.error_kindsplit (cwd_blankvscwd_not_absolute) needs blank told apartfrom relative, and a single
undefinedcannot carry that. Thetrim()in thelog line now decides nothing, and a comment says so, since the refusal is
already settled above it.
LLP 0150's predicate is "not borrowed".
@ref LLP 0150#usable-cwd [implements]added alongside the existing@ref LLP 0083#decision.usableInBandCwd. Now describes the shared predicate, and keeps the scopingcaveat, since sharing it was LLP 0083's call, not something 0150 imposed.
reaches the same
resolver.resolvewith no such predicate". PR Codex live projector: an unusable in-band cwd is a miss, not a path (#471) #474 alreadyfalsified that; this makes it honest. The scoping caveat is kept there too:
0150 still does not claim the in-band path as its mandate.
Checks
npm test: 3079 pass, 8 fail. The 8 are pre-existing onorigin/master(verified by stashing this diff and re-running: same count, same
leave/attachtests, unrelated to codex or cwd).
npm run typecheckclean.npm run smoke -- gateway_codex_captureok.Scoped tightly per the issue: no other behaviour touched, and nothing in the
turn-metadata header area that #480 is working in.
Fixes #478