30 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 475 | pi-permission-system: extract command enumeration and cwd projection; relocate the bash sub-domain (Phase 6 Step 3) |
Extract command enumeration and cwd projection; relocate the bash sub-domain (born-ready BashProgram + cwd type fix)
Release Recommendation
Release: ship now — batch "bash-program-decomposition" tail (this issue completes the batch)
This is Step 3 of the Phase 6 access-intent roadmap and the tail of the bash-program-decomposition batch (Steps 1 #473, 2 #474, 3 [#475]).
Steps 1 and 2 already landed on main with their releases deferred per the mid-batch marker.
Landing Step 3 completes the batch, so the release-please PR should merge rather than stay open.
Caveat to confirm at ship time: every commit here is refactor: / docs: with no user-facing behavior change (the extension's permission decisions, config surface, and outputs are identical), so release-please will not derive a version bump from the batch alone.
"Ship now" means "nothing is holding the batch back" — if no bumping commit has accumulated, no release is cut, which is correct for an internal-only refactor.
Do not fabricate a fix:/feat: to force a bump.
Problem Statement
After Steps 1 and 2, src/handlers/gates/bash-program.ts is down to ~695 LOC but still mixes three distinct concerns:
- the
BashProgramvalue-object API (parse once, expose typed slices), - command enumeration — chain/substitution/subshell descent that emits each executed command unit, and
- the effective-working-directory
cd-fold projection — the stateful AST walk that tags each path candidate with the working directory in force at its position, plus the per-candidate resolution that turns those tagged candidates into external paths and policy values.
The cd-fold projection is the subtlest region in the package (the home of the #307 and #454 fixes).
It and the command enumeration are each independently testable concerns that do not belong in the value-object file.
The file also still lives under handlers/gates/, which inverts the intended dependency direction: the gates should consume the access-intent engine, not host it.
Two design problems surfaced while planning this relocation, and both are folded into this issue:
BashProgramis not born-ready.parse(command)stores intermediatePathCandidate[]state, thenexternalPaths(cwd)/pathRuleCandidates(cwd)re-supplycwdon every call to finish the resolution lazily. Butcwdis always available at parse time (it istcc.cwd, threaded fromExtensionContext.cwd). An object should be constructed with all the state it needs;parse(command, cwd)can resolve eagerly and hand callers finished answers.- The
cwdtype is wrongly widened.ToolCallContext.cwdis typedstring | undefined, butExtensionContext.cwdisstringin the SDK (non-optional — the same interface marksmodelandsignalas| undefined, socwd's presence is deliberate). The| undefinedis a type-widening error introduced in this package. It spawned deadcwd-undefined branches in five gates that obscure the real invariant: if we are evaluating a tool call,tcc.cwdis astring.
Goals
- Extract command enumeration into
src/access-intent/bash/command-enumeration.ts. - Extract the
cd-fold projection — the walk and the per-candidate resolution — intosrc/access-intent/bash/cwd-projection.ts(Option B: projection owns the whole lifecycle;EffectiveBase/PathCandidatenever leave the module). - Make
BashProgramborn-ready:parse(command, cwd: string)resolves eagerly;commands()/externalPaths()/pathRuleCandidates()become parameter-free getters over stored resolved arrays. - Fix the
cwdtype widening: narrowToolCallContext.cwdtostring, and remove the now-deadcwd-undefined branches and obsolete tests across all five gates. - Relocate the slimmed
BashProgramtosrc/access-intent/bash/program.tsandbash-token-classification.tstosrc/access-intent/bash/token-classification.ts. - Repoint all bash gates and tests at
#src/access-intent/bash/.... - Sharpen the dependency direction:
handlers/gates/depends intoaccess-intent/bash/, never the reverse.
This is not a breaking change to the extension's user-facing surface — no public command, config field, default, schema, or permission output changes.
The changes are internal (module layout, in-package types, computation timing, dead-code removal).
All commits are refactor: / docs:.
Non-Goals
- No change to permission decisions, enumeration semantics, the
cd-fold projection results, classification, or policy resolution — outputs are identical; only when they are computed (eager vs lazy) and the internal API shape change. - No collapse of
BashProgramfrom a class to a function-returning-record. Under eager resolution the class is close to a data holder, but reshaping the value object belongs with Phase 6 Step 4 (#476), which already retypesBashProgram.externalPathsfor theAccessPathvalue object. - No introduction of the
AccessPathvalue object — Step 4. - No collapse of the two external-directory gates — Step 5.
- No new
index.tsbarrel foraccess-intent/bash/— consumers import the relocated modules directly (matchingparser.ts/node-text.ts/token-collection.ts), so fallow does not flag speculative re-exports. - No migration of the package-level path helpers (
path-utils,canonicalize-path) intoaccess-intent/— a later phase.
Background
Relevant existing modules (all under packages/pi-permission-system/src/):
handlers/gates/bash-program.ts— the file being decomposed. ExportsBashProgram(class),BashCommand(interface),BashPathRuleCandidate(interface). Private:EffectiveBase,PathCandidate, the enumeration functions, the projection walk, and the per-candidate resolution helpers.access-intent/bash/parser.ts— lazy tree-sitter-bash parser (getParser,TSNode). Seeded by Step 1.access-intent/bash/node-text.ts—resolveNodeText,SKIP_SUBTREE_TYPES,ARG_NODE_TYPES. Seeded by Steps 1–2.access-intent/bash/token-collection.ts—collectCommandTokens,collectPathCandidateTokens,collectRedirectTokens,extractCommandName. Seeded by Step 2.handlers/gates/bash-token-classification.ts—classifyTokenAsPathCandidate(strict),classifyTokenAsRuleCandidate(broad), sharedrejectNonPathToken.
The cwd invariant (verified during planning):
ExtensionContext.cwd: string(@earendil-works/pi-coding-agent) — non-optional.permission-gate-handler.tsbuildsToolCallContextwithcwd: ctx.cwd, sotcc.cwdis always astringat runtime.- The widened
ToolCallContext.cwd: string | undefinedproduced deadcwd-undefined handling in five gates:bash-external-directory.ts(|| !tcc.cwdguard),external-directory.ts(if (!tcc.cwd) return null),skill-read.ts(if (tcc.cwd === undefined) return null),path.ts(tcc.cwd ? … : filePathternary),tool.ts(tcc.cwd ? … : pathternary). Two of these have dedicated "returns null when no CWD" tests (bash-external-directory.test.ts,external-directory.test.ts) exercising an input that cannot occur.
Consumers of BashProgram (all in handlers/gates/ unless noted):
tool-call-gate-pipeline.ts— callsBashProgram.parse, passes the program to the bash gates.bash-command.ts— imports theBashCommandtype; the handler decomposes viaprogram.commands().bash-path.ts—describeBashPathGatecallsprogram.pathRuleCandidates(tcc.cwd).bash-external-directory.ts—describeBashExternalDirectoryGatecallsprogram.externalPaths(tcc.cwd).bash-path-extractor.ts— thin facadeextractExternalPathsFromBashCommand(command, cwd: string)overBashProgram(already typescwd: string).
AGENTS.md / skill constraints that apply:
package-pi-permission-systemskill: the parser is module-scoped state that persists across same-cwd session switches (earendil-works/pi#5905); this change does not touch that. SKILL.md references the classifiers by name (classifyTokenAsPathCandidate/classifyTokenAsRuleCandidate) but not by file path, so no SKILL.md edit is needed.docs/architecture/architecture.mdcarries a source-tree layout block and a Mermaid roadmap graph that referencebash-program.tsandbash-token-classification.tsby path — those must be updated when the files move.- Deferred-to-tail work from #474: its architecture
Outcome:line still reads "drops below ~670 LOC" against the actual 695. Fold the correction into this plan's doc step.
Design Overview
The design has three parts: the cwd type fix (a self-contained correctness change that lands first), the born-ready eager-resolution model, and the Option-B module split.
The facade-scope question (where the projection lives) is resolved to Option B because born-ready makes it the only coherent choice — once parse() resolves eagerly, there is no call-time orchestration left for the facade to retain, so the projection lives wholly in cwd-projection.ts.
Born-ready data flow — eager resolution at parse time
parse(command, cwd) does all the work; the instance stores only finished answers.
PathCandidate[] and EffectiveBase are local to the projection call and never reach the instance.
flowchart TD
IN["command string + cwd<br/>(cwd: string — always present)"]
IN --> PARSE["BashProgram.parse(command, cwd)<br/>[program.ts]"]
PARSE --> GP["getParser() then parser.parse()<br/>[parser.ts]"]
GP --> TREE["AST tree (rootNode)"]
TREE --> CE["collectCommands(rootNode)<br/>[command-enumeration.ts]"]
TREE --> CP["collectPathCandidates(rootNode)<br/>[cwd-projection.ts walk]"]
CP --> CAND["PathCandidate[] (local, internal to projection)<br/>{token, base: EffectiveBase}"]
CAND --> PE["projectExternalPaths(candidates, cwd)<br/>[cwd-projection.ts]"]
CAND --> PR["projectRuleCandidates(candidates, cwd)<br/>[cwd-projection.ts]"]
CE --> CTOR["new BashProgram(commandUnits,<br/>resolvedExternalPaths, resolvedRuleCandidates)"]
PE --> CTOR
PR --> CTOR
CTOR --> INST["born-ready instance<br/>(holds 3 resolved arrays only)"]
INST --> G1["commands() returns BashCommand[]"]
INST --> G2["pathRuleCandidates() returns BashPathRuleCandidate[]"]
INST --> G3["externalPaths() returns string[]"]
The expensive tree-sitter parse still happens once; the projection (cheap, pure) now also runs once at parse time instead of on every slice call.
Outputs are byte-for-byte identical to the lazy design — the slices were already pure functions of (candidates, cwd), and cwd is fixed for the call.
Module layout — Option B, born-ready
flowchart TD
subgraph dom["src/access-intent/bash/ (the cohesive sub-domain)"]
prog["program.ts — BashProgram (born-ready)<br/>parse(command, cwd) + 3 getters (~110 LOC)<br/>stores resolved arrays only; no classifier/path-utils imports"]
proj["cwd-projection.ts<br/>walk + projectExternalPaths/projectRuleCandidates<br/>owns EffectiveBase, PathCandidate, BashPathRuleCandidate (~420 LOC)"]
enm["command-enumeration.ts<br/>collectCommands + descent, owns BashCommand"]
clsf["token-classification.ts (relocated, unchanged)"]
base["parser.ts, node-text.ts, token-collection.ts (already here)"]
end
prog --> enm
prog --> proj
prog --> base
proj --> clsf
proj --> base
proj --> putils["path-utils, canonicalize-path"]
subgraph gates["src/handlers/gates/ (consumers — depend INTO the domain)"]
pipe["tool-call-gate-pipeline.ts → parse(command, tcc.cwd)"]
bcmd["bash-command.ts → program.commands()"]
bpath["bash-path.ts → program.pathRuleCandidates()"]
bext["bash-external-directory.ts → program.externalPaths()"]
bextr["bash-path-extractor.ts (thin facade)"]
end
pipe --> prog
bcmd --> enm
bpath --> prog
bext --> prog
bextr --> prog
EffectiveBase / PathCandidate / BashPathRuleCandidate live in cwd-projection.ts; BashCommand lives in command-enumeration.ts (its producer); program.ts imports only what its three getters return.
cwd type fix — dead branches removed
Narrowing ToolCallContext.cwd to string makes the type checker enforce the invariant and turns five gate branches into compile errors-if-kept (they read a property that is now always present):
| File | Dead branch removed | Replacement |
|---|---|---|
bash-external-directory.ts |
|| !tcc.cwd in the guard |
if (tcc.toolName !== "bash") return null; |
external-directory.ts |
if (!tcc.cwd) return null; |
removed (guard above it still applies) |
skill-read.ts |
if (tcc.cwd === undefined) return null; |
removed |
path.ts |
tcc.cwd ? normalizePathForComparison(filePath, tcc.cwd) : filePath |
normalizePathForComparison(filePath, tcc.cwd) |
tool.ts |
tcc.cwd ? normalizePathForComparison(path, tcc.cwd) : path |
normalizePathForComparison(path, tcc.cwd) |
The getPolicyValuesForRuleCandidate if (!cwd) { literal-only } branch is likewise dead under born-ready (the projection always has cwd) and is removed with it.
Born-ready BashProgram sketch
export class BashProgram {
private constructor(
private readonly commandUnits: readonly BashCommand[],
private readonly resolvedExternalPaths: readonly string[],
private readonly resolvedRuleCandidates: readonly BashPathRuleCandidate[],
) {}
static async parse(command: string, cwd: string): Promise<BashProgram> {
const parser = await getParser();
const tree = parser.parse(command);
if (!tree) return new BashProgram([], [], []);
try {
const candidates = collectPathCandidates(tree.rootNode);
return new BashProgram(
collectCommands(tree.rootNode),
projectExternalPaths(candidates, cwd),
projectRuleCandidates(candidates, cwd),
);
} finally {
tree.delete();
}
}
commands(): BashCommand[] { return [...this.commandUnits]; }
externalPaths(): string[] { return [...this.resolvedExternalPaths]; }
pathRuleCandidates(): BashPathRuleCandidate[] { return [...this.resolvedRuleCandidates]; }
}
Consumer call sites get simpler (no cwd argument on the getters):
// tool-call-gate-pipeline.ts
const bashProgram =
tcc.toolName === "bash" && command
? await BashProgram.parse(command, tcc.cwd) // tcc.cwd is now string
: null;
// bash-external-directory.ts → program.externalPaths()
// bash-path.ts → program.pathRuleCandidates()
// bash-path-extractor.ts → (await BashProgram.parse(command, cwd)).externalPaths()
Alternatives considered
- Facade scope A (facade retains the projection orchestration) and C (a
resolveCandidateBase()helper) — both kept the per-candidate loop in the facade with the methods takingcwd. Born-ready eager resolution removes all call-time orchestration, so A/C no longer have anything to retain; Option B is the only coherent layout. - Collapse
BashProgramto a function returning a record — under eager resolution the class is close to a data holder, so this is a legitimate direction. Deferred to Step 4 (#476), which already reshapes this value object (retypingexternalPathsforAccessPath); doing it here would pre-empt that step. - Split the
cwdtype fix into its own prerequisite issue — considered; the operator chose to land it in #475 (all-in), since born-ready couples theToolCallContextnarrowing at the pipeline seam and the five-gate cleanup is small and mechanical.
Module-Level Changes
New files (all under src/access-intent/bash/):
command-enumeration.ts—collectCommands,collectCommandsInto,makeUnit,descendCommandChildren,collectSubstitutionCommands, theCOMMAND_ENUM_DESCEND/COMMAND_ENUM_SKIP/NESTED_EXECUTION_CONTEXTStables, and theBashCommandinterface (the type moves to its producer). ImportsTSNodefromparser.tsandBashCommandContextfrom#src/types. ExportscollectCommandsandBashCommand.cwd-projection.ts— the projection walk (collectPathCandidates,walkForCandidates,walkCurrentShellSequence,walkPipeline,foldPipelineFirstStage,foldListExceptTerminal,isBackgrounded,tagTokens,foldCd,cdLiteralTarget,literalTextOf,CWD_BASE,UNKNOWN_BASE), the per-candidate helpers (getPolicyValuesForRuleCandidate,isRelativeCandidate, both takingcwd: string), and the two projection functionsprojectExternalPaths(candidates, cwd: string)/projectRuleCandidates(candidates, cwd: string). Owns theEffectiveBase,PathCandidate, andBashPathRuleCandidatetypes. Drops the deadif (!cwd)literal-only branch fromgetPolicyValuesForRuleCandidate. ImportsTSNodefromparser.ts,ARG_NODE_TYPES/SKIP_SUBTREE_TYPESfromnode-text.ts, the collectors +extractCommandNamefromtoken-collection.ts, the classifiers fromtoken-classification.ts, pluspath-utilsandcanonicalize-path. ExportscollectPathCandidates,projectExternalPaths,projectRuleCandidates, andBashPathRuleCandidate.program.ts— the born-readyBashProgramclass only (see sketch). Private constructor takes the three resolved arrays;parse(command, cwd: string)resolves eagerly; the three getters are parameter-free. ImportsgetParserfromparser.ts,collectCommands+BashCommandfromcommand-enumeration.ts, andcollectPathCandidates+projectExternalPaths+projectRuleCandidates+BashPathRuleCandidatefromcwd-projection.ts.token-classification.ts— relocatedbash-token-classification.ts, content unchanged except the doc-comment phrase "consumed bybash-program.ts" → "consumed bycwd-projection.ts".
Removed files:
src/handlers/gates/bash-program.ts— content distributed across the three new files.src/handlers/gates/bash-token-classification.ts— relocated totoken-classification.ts.
Changed files — cwd type fix (lands first, Step 1):
src/handlers/gates/types.ts—ToolCallContext.cwd: string | undefined→string.src/handlers/gates/bash-external-directory.ts— drop|| !tcc.cwdfrom the guard.src/handlers/gates/external-directory.ts— removeif (!tcc.cwd) return null;.src/handlers/gates/skill-read.ts— removeif (tcc.cwd === undefined) return null;.src/handlers/gates/path.ts— collapse thetcc.cwd ? … : filePathternary.src/handlers/gates/tool.ts— collapse thetcc.cwd ? … : pathternary.test/helpers/gate-fixtures.ts—makeTcccwdoverride no longer acceptsundefined(its default"/test/project"stands).test/handlers/gates/bash-external-directory.test.ts— remove the "returns null when no CWD" test (makeTcc({ cwd: undefined })).test/handlers/gates/external-directory.test.ts— remove the "returns null when no CWD" test.
Grep ToolCallContext object literals before the narrowing — only permission-gate-handler.ts (uses ctx.cwd: string) and makeTcc construct one; both already supply a string.
Changed files — born-ready signatures (Step 3, with the projection extraction):
src/handlers/gates/bash-external-directory.ts—program.externalPaths()(droptcc.cwdarg;tcc.cwdis still read forgetExternalDirectoryPolicyValuesand the descriptorcwdfield).src/handlers/gates/bash-path.ts—program.pathRuleCandidates()(droptcc.cwdarg).src/handlers/gates/bash-path-extractor.ts—(await BashProgram.parse(command, cwd)).externalPaths(); signature unchanged.src/handlers/gates/tool-call-gate-pipeline.ts—BashProgram.parse(command, tcc.cwd).
Changed files — import repoints (Step 4, relocation):
src/handlers/gates/bash-command.ts—BashCommandimport to#src/access-intent/bash/command-enumeration.src/handlers/gates/bash-external-directory.ts,bash-path.ts,bash-path-extractor.ts,tool-call-gate-pipeline.ts—BashProgramimport to#src/access-intent/bash/program.
Test files — relocate and/or repoint (Step 4):
test/handlers/gates/bash-program.test.tstotest/access-intent/bash/program.test.ts; import to#src/access-intent/bash/program.test/handlers/gates/bash-token-classification.test.tstotest/access-intent/bash/token-classification.test.ts; import to#src/access-intent/bash/token-classification.test/handlers/gates/bash-external-directory.test.ts,bash-path.test.ts,bash-command-metamorphic.test.ts,test/handlers/external-directory-symlink-acceptance.test.ts—BashProgramimport repoints.test/handlers/gates/tool-call-gate-pipeline.test.ts— thevi.mock("#src/handlers/gates/bash-program", …)factory path to#src/access-intent/bash/program; the mock's three methods become zero-arg (no signature change to the mock shape — they already return[]).
Born-ready test updates (Step 3): program.test.ts (née bash-program.test.ts) call sites change from BashProgram.parse(cmd) + .externalPaths(cwd) / .pathRuleCandidates(cwd) to BashProgram.parse(cmd, cwd) + parameter-free getters; the "returns the literal token only when no cwd is provided" test is removed (the no-cwd path no longer exists).
extractExternalPathsFromBashCommand(command, cwd) keeps its signature, so the ~90 call sites in test/bash-external-directory.test.ts are untouched.
Doc updates (docs/architecture/architecture.md, Step 5):
- Source-tree layout block: under
access-intent/bash/addcommand-enumeration.ts,cwd-projection.ts,program.ts,token-classification.ts; removebash-program.tsandbash-token-classification.tsfrom thehandlers/gates/block. Update theprogram.tsentry to describe the born-ready value object (parse-time resolution, parameter-free slices). - Inline
ToolCallContextlisting, if present in the doc's copied gate types, updated tocwd: string. - Mark Phase 6 Step 3 complete:
✅on the Step 3 heading and theS3Mermaid roadmap node. - Track A narrative note: update to reflect Step 3 landed.
- Fold in the #474 deferred fix: correct the Step 2
Outcome:"drops below ~670 LOC" line. - Health metrics table: rename the
bash-program.tsLOC / risk rows toprogram.tswith post-Step-3 actuals.
Test Impact Analysis
- New unit tests the extraction enables.
Low value, as in Steps 1–2:
command-enumeration.tsandcwd-projection.tsconsume parse-derivedTSNodetrees andPathCandidate[], so isolated tests would mean hand-building tree/candidate fixtures. The parse-drivenprogram.test.tsexercises the walk + projection end to end. No new isolated unit-test files are required. - Tests that change shape.
program.test.tsparse + slice call sites adopt the born-ready signatures (parse(cmd, cwd)+ parameter-free getters). This is mechanical and touches only that file (plus the removed no-cwd case). - Tests removed (dead inputs).
Three tests assert behavior for
cwd === undefined, which the narrowed type makes impossible:bash-external-directory.test.ts"returns null when no CWD",external-directory.test.ts"returns null when no CWD", andbash-program.test.ts"returns the literal token only when no cwd is provided". Removing them is correct — they document an input the SDK never produces. - Tests that must stay as-is. Every projection / enumeration / classification assertion pins current behavior; the relocation and born-ready change must keep them green (only call-site shape and import paths change).
Invariants at risk
A behavior-preserving move must keep every documented invariant green:
- The
cd-fold projection invariants from #307 (conservative flagging after a non-literalcd) and #454 (folding a leading current-shellcdacross a redirect-then-pipe) — pinned by theexternalPathscases inprogram.test.ts. - The never-weaker nested-command enumeration from #306 — pinned by the
commands()cases. - The lexical-vs-canonical return contract from #418 and cd-aware policy values from #393 — pinned by the
externalPaths/pathRuleCandidatescases.
These live in tests, not just prose; the eager-resolution change must produce identical arrays.
The removed cwd-undefined branches were verified dead against ExtensionContext.cwd: string, so their removal does not change production behavior — only the (impossible) undefined-input tests go.
TDD Order
Each cycle keeps the suite green (behavior-preserving); there is no red phase. Because the born-ready signature change and the type narrowing each break consumers at the type level, those steps fold the source change, all consumer updates, and all consumer-test updates into one commit.
- Fix the
cwdtype widening. NarrowToolCallContext.cwdtostring; remove the deadcwd-undefined branches inbash-external-directory.ts,external-directory.ts,skill-read.ts,path.ts,tool.ts; updatemakeTcc; remove the two "returns null when no CWD" tests. Runpnpm run check+ full suite. Commitrefactor(pi-permission-system): narrow ToolCallContext.cwd to string and drop dead cwd-undefined gate branches. - Extract command enumeration.
Create
command-enumeration.tswith the enumeration functions, tables, andBashCommand; remove them frombash-program.ts, importingcollectCommands+BashCommandback; repointbash-command.ts'sBashCommandimport. Runpnpm run check+ full suite. Commitrefactor(pi-permission-system): extract bash command enumeration to its own module. - Extract the cwd projection and make
BashProgramborn-ready. Createcwd-projection.tswith the walk, the per-candidate helpers,projectExternalPaths/projectRuleCandidates(all takingcwd: string), and theEffectiveBase/PathCandidate/BashPathRuleCandidatetypes; drop the deadif (!cwd)literal branch. RewriteBashProgramto born-ready:parse(command, cwd: string), three-array constructor, parameter-free getters. Update all callers —bash-external-directory.ts,bash-path.ts,bash-path-extractor.ts,tool-call-gate-pipeline.ts— andbash-program.test.ts(born-ready call sites; remove the no-cwd test). Runpnpm run check+ full suite. Commitrefactor(pi-permission-system): extract bash cwd projection and make BashProgram born-ready. - Relocate the facade and classifiers into the sub-domain.
Move
bash-program.tstoaccess-intent/bash/program.tsandbash-token-classification.tstoaccess-intent/bash/token-classification.ts; repointcwd-projection.ts's classifier import and all gate consumers; relocatebash-program.test.tstotest/access-intent/bash/program.test.tsandbash-token-classification.test.tstotest/access-intent/bash/token-classification.test.ts; repoint the remaining test imports and thetool-call-gate-pipeline.test.tsvi.mockpath. Runpnpm run check+ full suite +pnpm run lint+pnpm fallow dead-code. Commitrefactor(pi-permission-system): relocate bash sub-domain under access-intent/bash. - Update the architecture doc.
Apply the layout-block, born-ready
program.tsdescription,ToolCallContexttype note, Step 3✅, Track A, #474-deferredOutcome:fix, and health-metrics edits. Commitdocs(pi-permission-system): record Phase 6 Step 3 bash sub-domain relocation.
Steps 2–4 may merge if a green intermediate state is awkward, but keep Step 1 first (it is independent and unblocks born-ready) and the doc update (Step 5) last.
Risks and Mitigations
- Risk: the born-ready signature change ripples to many call sites.
parsegains a parameter and the three getters lose one, breaking every consumer and test at the type level. Mitigation: fold the projection extraction, all gate-caller updates, andprogram.test.tsupdates into Step 3;tscafter the step confirms none were missed. - Risk: removing a
cwd-undefined branch that is actually reachable. Mitigation: the invariant is verified against the SDK type (ExtensionContext.cwd: string); the narrowedToolCallContext.cwdmakes any reachable use a compile error, sotscproves the branches dead. - Risk: an import cycle between the new modules.
Mitigation: every type lives with its producer (
BashCommand→ enumeration,EffectiveBase/PathCandidate/BashPathRuleCandidate→ projection), so the graph is acyclic (program.ts→ both; both →parser.ts).pnpm run checkafter each step confirms. - Risk: silently dropping a moved symbol during the large block moves.
Mitigation: anchor edits on adjacent unique code lines (not decorative rules), re-read each moved region, rely on Biome
noRedeclare/noUnusedImports+tsc. - Risk: a stale
vi.mockpath silently mocks nothing. Mitigation: repoint thetool-call-gate-pipeline.test.tsvi.mockfactory path in Step 4 and confirm the pipeline tests still pass against the mock.
Open Questions
- None blocking.
Facade scope is resolved to Option B (born-ready forces it); the
cwd-fix scope is resolved to all-in #475; the class→function reshape is deferred to Step 4 (#476). - No follow-up issues are filed by this plan: Step 4 (#476) covers the
AccessPathvalue object and theBashProgramreshape; the external-directory gate collapse is Phase 6 Step 5.