32 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 694 | pi-permission-system: Bash path gates miss three variable-expanded external path forms |
Resolve plain $HOME / $PWD expansions in bash path tokens
Release Recommendation
Release: ship independently
Issue #694 is not a numbered step in docs/architecture/architecture.md's improvement roadmap, so it carries no Release: batch tag.
It is a fail-open security fix on the external_directory gate and lands as a breaking change (fix!:), so it cuts a major release on its own.
Problem Statement
Issue #694 (filed by ThreeIce, a third party) reports three bash tokens whose expanded form escapes the external_directory gate that an equivalent literal spelling triggers.
Reproduced against main at 2073c0af with a session cwd outside $HOME:
| Command | externalPaths today |
pathRuleCandidates today |
|---|---|---|
touch "$HOME/nonexistent" |
(empty) | /Users/chris/nonexistent |
ls "$HOME" (target exists) |
/Users/chris |
/Users/chris |
ls "${HOME}" |
(empty) | (empty) |
ls "${HOME}/somewhere" |
(empty) | <cwd>/${HOME}/somewhere |
CURRENT="$HOME"; ls "$CURRENT" |
(empty) | (empty) |
Three distinct defects are visible in that table.
First, $HOME/… is expanded by the rule-candidate projection but is invisible to the external-path projection.
normalizePathPolicyLiteral calls expandHomePath, so AccessPath resolves $HOME/x to the real home path — but classifyTokenAsPathCandidate does not recognize the $HOME shape, so the strict gate never accepts the token.
It reaches external_directory only when the #645 existence probe rescues it, which requires the target to already exist.
A new output path (touch, mkdir, a download destination, a copy destination) therefore produces no externalPaths entry at all.
Second, ${HOME} is not expanded anywhere.
expandHomePath handles ~, ~/, $HOME, and $HOME/ but not the braced form, so ls "${HOME}" yields nothing on either surface and ls "${HOME}/somewhere" yields a fabricated in-project candidate <cwd>/${HOME}/somewhere — a path that names nothing, displays wrongly in prompts and logs, and can match an in-project path rule.
Third, a literal assignment followed by a reference (CURRENT="$HOME"; ls "$CURRENT") is invisible to both projections.
The framing constraint is docs/decisions/0009-bash-path-projection-completeness-contract.md, which lists "Computed paths ($VAR, $(cmd), "$HOME/x")" as an accepted residual, not a bug.
Defects one and two sit awkwardly inside that residual: the package already resolves $HOME, just inconsistently across its two projections.
Defect three is squarely inside it.
Goals
- Resolve a plain
$HOME/${HOME}reference in a bash argument token to the OS home directory before token classification, so$HOME/xreceives the samepathandexternal_directorydecision as~/xand as the literal absolute spelling — regardless of whether the target exists. - Resolve a plain
$PWD/${PWD}reference to the base-relative form (.), so$PWD/xreceives the same decision as./xand resolves against thecd-folded effective base rather than fabricating<base>/$PWD/x. - Expand
${HOME}alongside$HOMEinexpandHomePath, so the braced form works identically everywhere the unbraced one already does: config rule patterns,piInfrastructureReadPaths, and path policy literals. - This is a breaking change.
On upgrade, a bash command referencing
$HOME/${HOME}newly triggersexternal_directorywhere it previously did not, and the token shown in prompts, logs, and derived session-approval patterns becomes the expanded path. Commit asfix(pi-permission-system)!:with aBREAKING CHANGE:footer. - Amend ADR 0009 (and its ADR 0003 reconciliation sentence) so
$HOMEand$PWDare named, bounded exceptions to the computed-path residual rather than an undocumented inconsistency.
Non-Goals
- Defect three — assignment dataflow.
CURRENT="$HOME"; ls "$CURRENT"stays unresolved. Confirmed with the operator: the bounded same-program literal-assignment tracker was considered and declined. It reaches a measured 1.6% of real commands (45 of 2767 unique bash commands in the permission review log) at the cost of new stateful dataflow in the AST walk, and ADR 0009 already exists to hold exactly this kind of residual. No follow-up issue is filed; the ADR amendment is the durable record. - Conservative flooring of unresolved expansions.
Flooring any command carrying an unresolved-expansion path operand to
askwas considered and declined: it would newly prompt on a measured 7.0% of real commands (194 of 2767), the prompt-firehose outcome ADR 0009 explicitly rejects for the bare-token case. - Widening the resolvable variable set beyond
HOMEandPWD. No configurable environment-variable allowlist.$TMPDIR,$USER,$XDG_*, and every other name keep their literal text. - Parameter expansion with operators.
${HOME:-/tmp},${#HOME},${HOME%/*},${HOME/a/b}stay unresolved — only a plain reference is resolved. cdtarget folding for expansions.cd "$HOME" && cat xkeeps yielding an unknown base. This is parity with the already-conservativecd ~, whichliteralTextOfalso rejects, and an unknown base is the fail-closed direction (relative tokens are then flagged conservatively).literalTextOfinbash-path-resolver.tsis untouched.- Command substitution.
$(cmd)and backticks stay unresolved. - Tool-input paths.
$PWDhandling is bash-token-scoped; aread/writetool input of$PWD/xis unaffected (expandHomePathhas no base to resolve$PWDagainst, and adding one would widen a shared pure helper for no reported need).
Background
The bash path projection walks the tree-sitter AST once (BashPathResolver.resolve) and emits two slices: externalPaths (strict shape gate → external_directory) and ruleCandidates (broad shape gate → the path surface).
Tokens reach both from token-collection.ts, which resolves each argument node's "shell value" via resolveNodeText (access-intent/bash/node-text.ts).
resolveNodeText today returns node.text verbatim for simple_expansion ($HOME) and expansion (${HOME}) nodes, so the expansion's literal spelling is what every downstream consumer sees.
Expansion resolution then happens — inconsistently — much later, inside normalizePathPolicyLiteral → expandHomePath, which is a string-prefix matcher operating on the already-classified token.
That split is the root cause: classification runs on the unexpanded string, resolution runs on the expanded one.
Verified AST shapes (spiked against the real parser):
$HOME simple_expansion → [ "$" (anon), variable_name "HOME" ]
${HOME} expansion → [ "${" (anon), variable_name "HOME", "}" (anon) ]
${HOME:-/tmp} expansion → [ "${", variable_name "HOME", ":-" (anon), word "/tmp", "}" ]
${#HOME} expansion → [ "${", "#" (anon), variable_name "HOME", "}" ]
$HOME/sub concatenation → [ simple_expansion "$HOME", word "/sub" ]
"$HOME/sub" string → [ '"', simple_expansion "$HOME", string_content "/sub", '"' ]
The plain-reference cases are structurally distinguishable from the operator cases: a plain reference has exactly one variable_name child and every other child is a pure delimiter ($, ${, }).
${HOME:-/tmp} carries a :- and a word; ${#HOME} carries a #.
This is a structural test, not a string-prefix test, so it cannot misfire on an operator form.
Constraints from AGENTS.md and the package skill that apply:
- The four path layers compose most-restrictive-wins; a
pathallow cannot suppress anexternal_directoryask. This is why defect one is a real fail-open: thepathsurface seeing the expanded value does not compensate forexternal_directorynot seeing it. - Do not read
process.platforminsidesrc/(ESLint-guarded). The new module reads neither the platform nor the path flavor —homedir()is platform-agnostic and"."is platform-free. docs/decisions/anddocs/architecture/are not in the packagefilesallowlist. A link to ADR 0009 added from the shippeddocs/configuration.mdmust be an absolute GitHub URL, not a relative path.pnpm fallow dead-codegates CI, so the new module must be wired into a consumer in the same commit that introduces it.
Design Overview
Resolve expansions at collection, not at classification
The fix moves expansion resolution upstream of classification, into the one place that already turns an AST node into its shell value.
This is deliberately the same shape as the --opt=value split that ADR 0009 describes as "token preprocessing, not classification": the resolved token is then handed to the ordinary shape classifiers, existence probe, and AccessPath machinery, all unchanged.
The consequence is that neither classifier needs a $HOME branch.
By the time classifyTokenAsPathCandidate sees the token it is already /Users/chris/nonexistent, which its existing startsWith("/") branch accepts.
token-classification.ts is not edited at all, which keeps its documented "pure shape function, policy-free" contract intact and avoids re-encoding the home-prefix vocabulary in a third place.
The new collaborator
// src/access-intent/bash/shell-variable-expansion.ts
/**
* The value of a plain `$NAME` / `${NAME}` reference the path projection
* resolves, or `null` when the node is not a plain reference or names a
* variable outside the resolvable set.
*/
export function resolvePlainVariableExpansion(node: TSNode): string | null;
The module owns two facts and nothing else: which expansion node shapes count as a plain reference, and which variable names resolve to what.
const RESOLVABLE_VARIABLES: ReadonlyMap<string, () => string> = new Map([
// The OS home directory, matching what `expandHomePath` already resolves
// for `~` and `$HOME` in config patterns and path literals.
["HOME", homedir],
// The shell's working directory === the projection's effective base, so the
// base-relative marker resolves it correctly after any `cd` folding, with no
// base parameter and no platform branch.
["PWD", () => "."],
]);
const PLAIN_REFERENCE_DELIMITERS: ReadonlySet<string> = new Set(["$", "${", "}"]);
Call site (node-text.ts), the only consumer:
case "simple_expansion":
case "expansion":
return resolvePlainVariableExpansion(node) ?? node.text;
Tell-Don't-Ask holds: the caller hands over the node and receives the answer, never walking children itself or asking "is this a $HOME?"
and then acting.
Law of Demeter holds: node-text.ts gains no node.child(1).text reach-through.
Parameter type is TSNode, the package's own minimal AST projection (six members); the module reads type, childCount, child(), and a child's type/text.
Narrowing further would fragment the one AST abstraction the bash modules share, so TSNode is the right ISP granularity here.
Why $PWD resolves to "." and not to a directory
$PWD in a bash program is the shell's current directory at that point, which after a current-shell cd is not the session cwd.
The projection already models exactly this as EffectiveBase, and already resolves relative tokens against it via forBashToken(token, { resolveBase }).
Rewriting $PWD to . therefore lands the token in the existing machinery with the correct semantics for free, keeps the resolver a pure function of the node (no base parameter threaded into resolveNodeText), and inherits the #393 unknown-base conservatism when the base is unresolvable.
$PWD/sub → ./sub, bare $PWD → ., and cd /etc && ls "$PWD/x" → /etc/x.
This also means $PWD/… is correctly not a strict external candidate (it is base-relative, like ./x), reaching external_directory through the same route ./x does.
${HOME} in expand-home.ts
expandHomePath is a separate, string-prefix concern serving config rule patterns (wildcard-matcher.ts), piInfrastructureReadPaths (path/pi-infrastructure-read.ts), and path policy literals (access-intent/path-normalization.ts).
Adding the ${HOME} prefix there is pure widening — the braced form previously matched nothing — and is what makes "${HOME}/.cargo/*": "allow" work as a config pattern.
// added forms
// `${HOME}` → homedir()
// `${HOME}/path` → homedir()/path
// `${HOME}\path` → homedir()\path (Windows)
$PWD is deliberately not added there: expandHomePath has no base to resolve it against, and widening a shared pure helper beyond its name is the kind of drift this plan is fixing.
Windows
homedir() on win32 returns a native path (C:\Users\x), which classifyTokenAsPathCandidate accepts via WINDOWS_DRIVE_PATH_PATTERN and PathNormalizer.forBashToken routes through the plain branch.
Git Bash's own $HOME is the MSYS spelling (/c/Users/x), which the drive-mount branch translates to the same C:\Users\x, so the two agree on the location.
$PWD → . is platform-free.
No new process.platform read, no PathFlavor parameter.
Determinism
ADR 0009 states the invariant as same policy + same filesystem state + same command → same decision, and its closing paragraph excludes ambient host state including environment variables, per ADR 0003.
This change makes $HOME and $PWD named, bounded exceptions, which the ADR amendment must state explicitly:
$HOMEresolves viaos.homedir(), which the package already treats as a resolvable input inexpandHomePathfor~and$HOMEin config patterns and path literals. This change removes an inconsistency rather than adding a concession.$PWDresolves to the projection's own effective base and reads no environment at all, so it is strictly more deterministic than$HOME.
Every other variable keeps its literal text, so ADR 0003's rejection of cygpath shell-outs and MSYS environment detection is untouched.
Edge cases
${HOME:-/tmp},${#HOME},${HOME%/*}— operator forms, structurally rejected, keep literal text.$HOMEDIR,$CURRENT,$PATH— outside the resolvable set, keep literal text.FOO=$HOME/bar cmd— avariable_assignmentnode, already skipped at collection and by the command enumerator's prefix stripping.$HOMEinside a heredoc or comment —SKIP_SUBTREE_TYPESalready prunes the subtree.grep "$HOME" file—grepis aPATTERN_FIRST_COMMANDSentry, so the first positional is skipped as a pattern.--prefix=$HOME/.local—collectEmbeddedOptionValuesreads throughresolveNodeText, so the embedded value is the expanded path.- A project whose
cwdis under$HOME— the expanded path is inside the working directory, so noexternal_directoryprompt fires at all. The measured blast radius below assumes the worse case of acwdoutside home.
Predicted effect
Measured on main at 2073c0af with cwd outside $HOME; the "after" column is the predicted post-change projection.
| Command | externalPaths before |
externalPaths after |
|---|---|---|
touch "$HOME/nonexistent" |
(empty) | /Users/chris/nonexistent |
ls "$HOME" |
/Users/chris |
/Users/chris (unchanged, no longer existence-dependent) |
ls "${HOME}" |
(empty) | /Users/chris |
ls "${HOME}/somewhere" |
(empty) | /Users/chris/somewhere |
cat $HOME/.ssh/id_rsa (absent) |
(empty) | /Users/chris/.ssh/id_rsa |
echo hi > $HOME/out.txt |
(empty) | /Users/chris/out.txt |
CURRENT="$HOME"; ls "$CURRENT" |
(empty) | (empty) (declined) |
Upgrade blast radius, measured over 2767 unique real bash commands from the permission review log (~/.pi/agent/extensions/pi-permission-system/logs/pi-permission-system-permission-review.jsonl):
- 15 commands (0.5%) contain
$HOMEor${HOME}— the upper bound on newly-prompting commands, before subtracting those whosecwdis under home. - 1 command (0.04%) contains the braced
${HOME}. - 194 commands (7.0%) contain some
$VAR/${VAR}; all but the 15 keep today's behavior exactly.
Cost: the change adds no filesystem call and no parse. It adds one map lookup and a bounded child scan per expansion node, on a tree the walk already visits.
Module-Level Changes
Added
packages/pi-permission-system/src/access-intent/bash/shell-variable-expansion.ts— new. ExportsresolvePlainVariableExpansion(node: TSNode): string | null. Owns the plain-reference structural test and theHOME/PWDresolvable-variable map.packages/pi-permission-system/test/access-intent/bash/shell-variable-expansion.test.ts— new unit tests for the pure module.
Changed — source
packages/pi-permission-system/src/access-intent/bash/node-text.ts— thesimple_expansion/expansionarm ofresolveNodeTextdelegates toresolvePlainVariableExpansion, falling back tonode.text. Update the function's doc comment, which currently documents expansion nodes as returning.text.packages/pi-permission-system/src/expand-home.ts—expandHomePathgains${HOME},${HOME}/…, and${HOME}\…. Update the supported-forms doc comment.
Verified by grep that no export is removed or renamed, so no consumer sweep is required beyond the two files above.
resolveNodeText's only importer is token-collection.ts (unchanged signature); expandHomePath's importers are wildcard-matcher.ts, access-intent/path-normalization.ts, and path/pi-infrastructure-read.ts (unchanged signature).
token-classification.ts and bash-path-resolver.ts are not edited.
Changed — tests
packages/pi-permission-system/test/access-intent/bash/node-text.test.ts— the existing assertionresolveNodeText(makeNode("simple_expansion", "$HOME")) === "$HOME"inverts. False-green hazard:makeNodedefaults to zero children, and a childless node fails the plain-reference test and falls back tonode.text, so the existing assertion would keep passing while testing nothing. Rebuild these cases with realistic children (makeNode("simple_expansion", "$HOME", [makeNode("$", "$"), makeNode("variable_name", "HOME")])) and add an operator-form case.packages/pi-permission-system/test/expand-home.test.ts— add${HOME}cases mirroring the existing$HOMEones, plus a negative for${HOMEDIR}.packages/pi-permission-system/test/access-intent/bash/program.test.ts— add the issue's three repro commands as end-to-end projection assertions (two fixed, one recorded as the declined residual).packages/pi-permission-system/test/handlers/gates/bash-external-directory.test.ts— add thetouch "$HOME/<nonexistent>"case asserting the gate prompts underexternal_directory: { "*": "ask" }.
Changed — docs
packages/pi-permission-system/docs/decisions/0009-bash-path-projection-completeness-contract.md— amend three sections:- "What the projection guarantees" gains a bullet for a resolved plain
$HOME/${HOME}/$PWD/${PWD}reference. - The "Computed paths" residual (line 77) narrows to
$VARoutside the resolvable set,$(cmd), and assignment-then-reference, and states the declined alternatives with their measured reach (1.6% / 7.0%). - "Determinism and the filesystem" (line 97) gains the two named exceptions and their rationale.
- "What the projection guarantees" gains a bullet for a resolved plain
packages/pi-permission-system/docs/configuration.mdline 592 — the sentence "This is a best-effort heuristic — variable expansion and escaped quotes are not parsed, and relative paths inside subshells are not yet resolved against a per-subshell working directory" is now doubly stale (cdfolding shipped in#454/#393). Rewrite to state what is resolved (~,$HOME,${HOME},$PWD,${PWD},cd-folded relative tokens) and what is not (other variables, command substitution, assignment references). Any ADR 0009 citation added here must be an absolute GitHub URL —docs/decisions/is not in the packagefilesallowlist.packages/pi-permission-system/docs/configuration.mdlines 109, 139, 647, 664 — extend the~/$HOMEpattern-expansion prose to name${HOME}.packages/pi-permission-system/docs/opencode-compatibility.mdline 23 — the "Home directory expansion" row names~/and$HOME/; add${HOME}/.packages/pi-permission-system/docs/architecture/architecture.md:- Line 702 —
expand-home.tstree entry:~/$HOME/${HOME}. - Line 727 —
node-text.tstree entry:resolveNodeTextis no longer purely lexical; it resolves plainHOME/PWDexpansions via the new module. - Add a tree entry for
shell-variable-expansion.tsalongside its siblings. - Line 291 — the ADR 0009 summary sentence gains the resolved-expansion guarantee.
- Per the architecture-doc convention, cite
#694in a tree entry only on the constraint that must not drift (the resolvable-variable set is bounded toHOME/PWDby ADR 0009); keep provenance out of the tree otherwise.
- Line 702 —
.pi/skills/package-pi-permission-system/SKILL.mdlines 268–269 — the paragraph enumerating whatclassifyTokenAsPathCandidateaccepts. It describes the mechanism this change reworks and carries no removed symbol, so it will not surface in asrc/grep. Add that a plain$HOME/${HOME}reference is expanded before classification (so it is an absolute-shaped token) and a plain$PWD/${PWD}reference becomes base-relative.
Grep sweep performed: \$HOME|variable expansion|environment variable across packages/pi-permission-system/docs/, README.md, and .pi/skills/package-pi-permission-system/SKILL.md; node-text|token-classification|token-collection|expand-home across docs/architecture/architecture.md.
packages/pi-permission-system/README.md names no bash-expansion behavior, so it needs no edit.
No file listed here is claimed unchanged in Non-Goals.
Test Impact Analysis
New tests the change enables.
shell-variable-expansion.test.ts unit-tests the plain-reference discrimination directly — previously this logic did not exist as a seam, and the operator-form rejection (${HOME:-/tmp}, ${#HOME}) could only have been asserted through a full parse.
The module is pure and node-shaped, so each AST form is a one-line case.
Tests that become redundant. None. The change adds behavior to a previously identity-mapped branch; no existing assertion is subsumed.
Tests that must stay as-is.
test/path-normalization.test.ts$HOMEcases andtest/wildcard-matcher.test.ts$HOME-pattern cases exerciseexpandHomePath's string-prefix path, which serves config patterns and tool-input literals — a surface the AST resolver never touches. They must keep passing unchanged, and they are the regression guard that the${HOME}addition did not perturb the unbraced forms.test/permission-manager-unified.test.ts$HOME/*pattern-matching cases pin that a rule pattern spelled$HOME/…still matches, which is the opposite direction from token resolution and must not drift.- The
#645existence-probe cases inprogram.test.tsmust stay green: the probe remains the mechanism for bare tokens, and$HOME/…merely stops depending on it.
Invariants at Risk
| Invariant | Source | Pinning test | Risk |
|---|---|---|---|
A promoted token matching no explicit rule stays unrestricted (matchedPattern === undefined) |
ADR 0009 | program.test.ts / bash-external-directory.test.ts probe cases |
Low — the change routes $HOME/… through the strict shape gate rather than the probe, leaving the probe's guard untouched. Verify ls "$HOME" still yields exactly one deduplicated external path, not two. |
| Classifiers are pure shape functions that consult no ruleset | ADR 0009, token-classification.ts header |
token-classification.test.ts |
None — the file is not edited. The new module consults no ruleset either. |
No process.platform read inside src/ outside index.ts |
ESLint no-restricted-syntax |
Lint gate | None — no platform read added. |
#393 unknown-base conservatism: a relative token after a non-literal cd stays literal-only |
bash-path-resolver.ts |
program.test.ts unknown-base cases |
Low — $HOME/x expands to an absolute and is base-independent; $PWD/x becomes relative and inherits the existing conservatism. Add a case for each under an unknown base. |
#533 win32 non-mount POSIX absolutes stay literal-only |
ADR 0003 | path-normalizer.test.ts, program.test.ts win32 cases |
Low — homedir() on win32 is a drive path, not a POSIX absolute, so the literal-only branch is not entered. Add a win32-flavor case for $HOME/x. |
| Cost of the projection stays negligible relative to the tree-sitter parse (ADR 0009 measured the probe at ~19% of parse) | ADR 0009 | none | None — the change adds zero filesystem calls and zero parses; it adds a bounded child scan on nodes already visited. No new number is claimed. |
TDD Order
-
${HOME}inexpandHomePath. Red: extendtest/expand-home.test.tswith${HOME},${HOME}/dev/project,${HOME}/dev/*,${HOME}\dev\project, and a negative for${HOMEDIR}/${HOME:-/tmp}. Green: add the three prefix branches toexpand-home.tsand update its doc comment. Non-breaking pure widening — the braced form previously matched nothing. Commit:feat(pi-permission-system): expand ${HOME} alongside $HOME in path patterns -
Plain-expansion resolution in bash tokens. Red, in one cycle:
test/access-intent/bash/shell-variable-expansion.test.ts—$HOME/${HOME}→homedir(),$PWD/${PWD}→.,$HOMEDIR/$CURRENT/${HOME:-/tmp}/${#HOME}/ a childless node →null.test/access-intent/bash/node-text.test.ts— rebuild thesimple_expansioncases with realistic children (see the false-green hazard above) and add anexpansioncase.test/access-intent/bash/program.test.ts— the issue's repros:touch "$HOME/<nonexistent>"andls "${HOME}/somewhere"produce the expanded external path;ls "$HOME"still produces exactly one;CURRENT="$HOME"; ls "$CURRENT"still produces none;cd /etc && ls "$PWD/x"resolves to/etc/x; a win32-flavor$HOME/xcase; an unknown-base case for each of$HOME/xand$PWD/x.test/handlers/gates/bash-external-directory.test.ts—touch "$HOME/<nonexistent>"prompts underexternal_directory: { "*": "ask" }.
Green: add
src/access-intent/bash/shell-variable-expansion.tsand delegate fromresolveNodeText. The module and its wiring land in one commit: an unwired export would trip the CIpnpm fallow dead-codegate. Commit:fix(pi-permission-system)!: resolve $HOME and $PWD expansions in bash path tokens, with aBREAKING CHANGE:footer naming the remediation — anexternal_directoryallow rule for the home-anchored path (e.g."~/.cargo/registry/*": "allow"), the mechanismdocs/configuration.mdalready documents. -
Docs and ADR amendment. ADR 0009 (three sections),
docs/configuration.md(line 592 rewrite plus the four~/$HOMEprose sites),docs/opencode-compatibility.mdline 23,docs/architecture/architecture.md(lines 291, 702, 727 plus the new tree entry), and.pi/skills/package-pi-permission-system/SKILL.mdlines 268–269. Commit:docs(pi-permission-system): record resolved shell expansions in ADR 0009 and user docs
Risks and Mitigations
- New prompts on upgrade.
Measured upper bound is 15 of 2767 real commands (0.5%), and lower still for a project under
$HOME. Mitigated by theBREAKING CHANGE:footer naming theexternal_directoryallow-rule remediation, and framed by ADR 0009's "over-suppression is unrecoverable, over-surfacing is recoverable". - False green in
node-text.test.ts. A childless fake node silently falls back tonode.text, so the inverted assertion would pass without exercising the new code. Mitigated by rebuilding those cases with realistic children as an explicit red step, and by theprogram.test.tsend-to-end cases which go through the real parser. - Displayed token changes.
Prompts, review-log entries, and derived session-approval patterns for a
$HOMEtoken now show the expanded path. This is an improvement —deriveApprovalPatternalready derives fromAccessPath.value()(the expanded form), so today the prompt says$HOME/xwhile the rule written says/Users/chris/x*. The change makes the two agree. Call it out in theBREAKING CHANGE:footer. - Over-resolving an operator form.
Mitigated by the structural plain-reference test (exactly one
variable_namechild, all others pure delimiters) rather than a string-prefix match, with explicit negative cases for${HOME:-/tmp}and${#HOME}. - ADR drift.
Amending ADR 0009's residual list is load-bearing: without it, the next "the gate missed my
$VAR" report has no triage answer and the patch-per-report cycle the ADR was written to end resumes. Step 3 is not optional. - Third-party issue, partial resolution. Defect three is declined. The ship comment must say so explicitly and cite the amended ADR, rather than closing #694 as if all three were fixed.
Open Questions
- None blocking.
The scope ladder and the resolvable-variable set were both settled with the operator before planning: home-parity only,
HOME+PWD.