Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 20 additions & 14 deletions IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,26 @@ bulldoze priorities.
- [x] Flag-value path classification table (`git -C` is path; `curl -d`
is body data; `docker -v` is single literal IsPath=false per #8)

### 8. cd-in-compound propagation (SPEC §9)

- [ ] First-clause `cd` sets attributed cwd for the compound
- [ ] Subsequent clauses receive synthetic `Arg` with
`IsCwdAttribution=true`
- [ ] Subsequent `cd` in the same compound replaces attribution
- [ ] Subshell boundaries reset attribution

### 9. Subshell + bash -c surfacing (SPEC §10)

- [ ] Flatten subshell clauses into parent's `Clauses` with
`IsSubshell=true`
- [ ] Surface `bash -c` inner clauses inline with `IsBashCWrapped=true`
- [ ] Recursion-depth cap
### 8. cd-in-compound propagation (SPEC §9) — PR 5, complete

- [x] First-clause `cd`/`chdir` sets attributed cwd; only those two verbs
propagate (interp #5)
- [x] Subsequent clauses receive synthetic `Arg` with
`IsCwdAttribution=true`; `Kind=Literal, IsPath=true` when cd
target resolved, `Kind=DynamicSkip, IsPath=false` when dynamic
(interp #6)
- [x] Subsequent `cd` in the same compound replaces attribution
- [x] Subshell boundaries isolate attribution via push/pop stack

### 9. Subshell + bash -c surfacing (SPEC §10) — PR 5, complete

- [x] Flatten subshell clauses into parent's `Clauses` with
`IsSubshell=true`; sibling subshells handled via SubshellStack IDs
- [x] Surface `bash -c "..."` / `sh -c "..."` inner clauses inline with
`IsBashCWrapped=true`; outer cd attribution doesn't leak into
inner shell (v0.1 decision)
- [x] Recursion depth cap at 5 → outer
`ParsedCommand.IsUnparseable=true` (interp #4)

### 10. Hand-authored corpus (SPEC §13 — minimum 105 entries)

Expand Down
57 changes: 43 additions & 14 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -680,8 +680,15 @@ The agent's natural idiom is `cd /target && cmd1 && cmd2`. Bash semantics:

### Rules

1. **First clause is a `cd`-family verb** (per §6.2): the cd target becomes
the **attributed cwd** for subsequent clauses in the same compound.
1. **First clause is a `cd` or `chdir` verb**: the cd target becomes the
**attributed cwd** for subsequent clauses in the same compound. **Only
`cd` and `chdir`** propagate attribution per locked interpretation #5.
`pushd`, `popd`, `push-location`, and `set-location` are still listed
in `CwdVerbs` so their first non-flag positional is path-classified
(the target shows up as `IsPath=true`), but they do **not** add a
synthetic attribution arg to subsequent clauses. A future v0.1.x or
v0.2 with PowerShell support may model `pushd`/`popd` as a proper
directory stack.
2. **Subsequent clauses inherit the attributed cwd** as if it were
prepended with `-C` semantics. Specifically: a synthetic `Arg` with
`IsPath=true`, `Resolved=<cd target>`, and `Kind=Literal` is added to
Expand All @@ -693,17 +700,40 @@ The agent's natural idiom is `cd /target && cmd1 && cmd2`. Bash semantics:

3. **A subsequent `cd`** in the same compound **replaces** the attributed
cwd for clauses after it. (`cd /a && cmd1 && cd /b && cmd2` → cmd1
inherits `/a`, cmd2 inherits `/b`.)
inherits `/a`, cmd2 inherits `/b`.) The replacing `cd /b` itself still
*receives* `/a` as a synthetic attribution arg (rule 2) before becoming
the new source — additive semantics per rule 5.

4. **Subshell boundaries reset attribution.** `cd /a && (cd /b && cmd1) && cmd2`:
cmd1 (inside subshell) inherits `/b`; cmd2 (outside subshell) inherits
`/a` (the subshell's `cd /b` does not leak out).
`/a` (the subshell's `cd /b` does not leak out). A subshell *inherits*
outer attribution on entry (so `cd /a && (cmd)` still attributes cmd
to /a) but its own cd changes stay isolated.

5. **Attribution does not change the clause's verb or original args.**
The attribution is purely additive — the `cd` clause itself is still
parsed normally, and subsequent clauses retain everything the user
typed, plus the synthetic Arg.

### Dynamic-cd attribution (locked interpretation #6)

When the cd target itself is `Kind=DynamicSkip` (e.g. `cd $REPO`), we
statically don't know the resolved cwd. To preserve the cwd-uncertainty
signal for subsequent clauses:

- A synthetic `Arg { Raw="<dynamic-cwd>", Resolved=null, Kind=DynamicSkip,
IsPath=false, IsCwdAttribution=true }` is appended to each subsequent
clause (instead of the literal-cd flavor).
- Relative path args in subsequent clauses are not re-resolved against a
fall-back cwd; they surface as `Kind=DynamicSkip, IsPath=false,
Resolved=null` so consumers route to safe-fail rather than trust a
guessed working directory.

Consumers that iterate `IsPath=true` args won't see the synthetic
attribution arg; consumers that specifically check `IsCwdAttribution`
can detect "this clause's cwd context is unknown" and elevate to
user-prompt instead of treating it like a default-cwd command.

### Example

Input: `cd /target && git -C /other log && cat file.txt`
Expand Down Expand Up @@ -739,15 +769,11 @@ already see the resolved path in another arg.
### Subshells

Subshells are clauses wrapped in parens: `(cd /a && cmd)`. The parser
recognizes the parens, parses the inner command, and emits a single
clause with:

- `Verb` = empty (a subshell has no verb of its own)
- `Args` = empty
- `IsSubshell = true`
- A nested `ParsedCommand` field — **but** since the AST should be flat
for consumer convenience, instead we **flatten** the subshell into the
parent's `Clauses` list with each inner clause's `IsSubshell=true`.
recognizes the parens and **flattens** the subshell's inner clauses into
the parent's `Clauses` list, marking each with `IsSubshell=true` so
consumers can distinguish them from outer-compound clauses. A subshell
*inherits* the outer compound's cd attribution on entry but its own cd
changes stay isolated to the subshell (rule 4 above).

Specifically: `(cd /b && cmd) && cmd2` produces three clauses:

Expand Down Expand Up @@ -779,7 +805,10 @@ by the recursion. Consumers that care that this came from a wrapper can
inspect `IsBashCWrapped` on the surfaced clauses.

**Recursion limit:** parse `bash -c "bash -c ..."` chains up to depth 5.
Deeper nesting → mark the deepest clause as `IsUnparseable = true`.
Deeper nesting → set the outer `ParsedCommand.IsUnparseable = true` with
reason `"bash -c recursion depth exceeded (>5)"` per locked interpretation
#4. (`Clause` has no `IsUnparseable` field; we surface the overflow on the
top-level ParsedCommand so consumers safe-fail per §11.)

---

Expand Down
72 changes: 55 additions & 17 deletions openspec/changes/v0.1-locked-interpretations/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,23 +161,61 @@ the SPEC.md sections that get updated alongside the implementation.

## 5. PR 5 — cd attribution + subshells + bash -c (interpretations #4, #5, #6)

- [ ] 5.1 `Internal/Bash/Parsing/CdAttributionContext.cs` (parser-internal
mutable; output AST stays immutable)
- [ ] 5.2 Only `cd`/`chdir` propagate (interpretation #5); pushd/popd
parse but don't propagate
- [ ] 5.3 Synthetic `Arg{ IsCwdAttribution=true, … }` appended to
subsequent clauses; `Kind=DynamicSkip` when cd target is dynamic
(interpretation #6)
- [ ] 5.4 Subshell `(...)` parsing with attribution stack push/pop
- [ ] 5.5 `bash -c "..."` / `sh -c "..."` recursion (cap at 5)
- [ ] 5.6 Recursion overflow → outer `ParsedCommand.IsUnparseable=true`
(interpretation #4)
- [ ] 5.7 Update `SPEC.md` §9 (clarify which CwdVerbs propagate; add
dynamic-cd attribution subsection); §10 (replace "mark the deepest
clause" wording with "set ParsedCommand.IsUnparseable=true")
- [ ] 5.8 File GitHub issue: "Model pushd/popd directory stack semantics"
- [ ] 5.9 Open OpenSpec change `cd-attribution-clarifications` for the
§9/§10 deltas
- [x] 5.1 `Internal/Bash/Parsing/CdAttributionContext.cs` — parser-internal
mutable; SubshellStack of monotonic IDs (handles sibling subshells
cleanly); SetLiteralAttribution / SetDynamicAttribution; HasAttribution
- [x] 5.2 Only `cd`/`chdir` propagate (interpretation #5); pushd/popd
parse as CwdVerbs but don't propagate. Test
`Pushd_does_not_propagate_attribution` pins this.
- [x] 5.3 Synthetic `Arg{ IsCwdAttribution=true, … }` appended to
subsequent clauses. `Kind=Literal, IsPath=true, Resolved=<cwd>`
when cd target resolved; `Kind=DynamicSkip, IsPath=false,
Resolved=null, Raw="<dynamic-cwd>"` when cd target was DynamicSkip
per interpretation #6.
- [x] 5.4 Subshell `(...)` parsing with attribution stack push/pop.
`cd /a && (cd /b && cmd1) && cmd2` — cmd1 sees /b attribution;
cmd2 sees /a (subshell's /b doesn't leak out). `IsSubshell=true`
set on every clause inside a subshell.
- [x] 5.5 `bash -c "..."` / `sh -c "..."` recursion replaces PR 3's
single-clause framework. Outer `bash -c` consumed; inner clauses
surfaced inline with `IsBashCWrapped=true`. Outer cd attribution
does NOT leak into inner shell (v0.1 decision).
- [x] 5.6 Recursion depth cap at 5 → outer
`ParsedCommand.IsUnparseable=true` with reason `"bash -c recursion
depth exceeded (>5)"` per interpretation #4.
- [x] 5.7 SPEC §9 updated (rule 3 explicit on which verbs propagate;
dynamic-cd attribution subsection added per interp #6); §10
updated (subshell flattening rephrased; bash -c recursion-limit
replaced with "set ParsedCommand.IsUnparseable" per interp #4).
- [x] 5.8 BashResolver internal overload `Resolve(raw, treatAsPath,
options, workingDirectoryUnknown)` lets the propagator force
DynamicSkip on relative-path args under dynamic-cd (interp #6)
without polluting the public `BashParserOptions` surface.
- [x] 5.9 30 new corpus entries (71-100): 10 cd-in-compound + 10
subshell + 10 bash -c. 5 PR 3-4 corpus entries refreshed
(25, 28, 34, 35, 52).
- [x] 5.10 8 new parser tests covering attribution propagation, subshell
isolation, sequential cd, pushd non-propagation, dynamic cd,
sibling subshells, bash -c depth-2 success, depth-6 overflow.
- [x] 5.11 **337/337 tests passing** (was 296 at PR 4 baseline). Public
API surface unchanged.
- [ ] 5.12 File GitHub issue: "Model pushd/popd directory stack semantics"
(interp #5 option-C upgrade)

### PR 5 follow-ups (tracked for PR 6)

- Possible SPEC clarification: pin the sentinel `Raw` value for
dynamic-cd synthetic attribution args (current implementation uses
`"<dynamic-cwd>"` but SPEC is silent).
- Outer cd attribution into bash -c inner clauses (v0.1: doesn't
propagate; v0.1.x or v0.2 may revisit).
- `cd -` (jump-to-previous-dir): currently treated as "no attribution
change" because `-` is detected as a flag (no non-flag positional).
v0.1.x may want explicit handling.
- Existing PR 3 corpus entries 21-24, 26-27, 29-33 (the compound
entries that had no `cd` prefix) likely don't need updates; but PR 6
should audit all 100 corpus entries for AST drift now that PRs 1-5
have all landed.

## 6. PR 6 — Corpus completeness + PII audit (interpretation #7)

Expand Down
Loading
Loading