T-WOL2-task-work-preflight-probes-file-mutation-tools
Status: closed/done · Impact: medium · Complexity: small
Spawned from T-M5RS-port-check-entities-to-ts in git@github.com:sksizer/dev.git. The check_entities port run hit a file-mutation permission denial that task-work’s preflight did not catch.
task-work’s Step 3b preflight_permissions.ts only probes Bash tool-family signals, not file-mutation tools (Write/Edit). On the check_entities port run, Write/Edit were denied in the worktree and the implementer had to fall back to Bash heredoc for file creation. Extend the preflight to confirm Write/Edit permission coverage so file-mutation denials surface before implementation rather than mid-flight.
The Step-3b probe scans the task body for Bash(<verb>:*) tool-family
signals and reports missing Bash grants. It has no concept of
file-mutation tools: it never checks whether Write / Edit are
permitted for the worktree path the implementer will create in Step 4.
When those tools are denied (or not allowed) for .sdlc/worktrees/...,
the gap goes undetected until the implementing sub-agent hits the denial
mid-flight and falls back to Bash heredoc — exactly the friction the
probe exists to prevent.
| Location | Role today |
|---|---|
plugin/skills/task-work/preflight_permissions.ts#main | The probe entry point: scans the task body, resolves settings, reports Bash gaps on stdout, exits 0/1/2. Knows nothing about Write/Edit. |
plugin/skills/task-work/preflight_permissions.ts#collectPermissions | Builds the resolved allow/deny sets from the three settings files. Today it only feeds the Bash-verb matcher; the same resolved sets can answer Write/Edit coverage. |
plugin/skills/task-work/preflight_permissions.ts#ResolvedPermissions | Holds the allow/deny sets and the allowsVerb Bash matcher. Needs a sibling check for tool-rule entries like Write(...) / Edit(...). |
plugin/skills/task-work/SKILL.md#3b. | Step 3b’s prose: describes the Bash-signal probe and the AskUserQuestion remediation on exit 1. Must document the new Write/Edit gap shape. |
plugin/skills/task-work/tests/preflight_permissions.test.ts | The bun:test suite (spawnSync against the probe). The new Write/Edit coverage needs cases here. |
plugin/skills/task-work/invariants.yaml#required_phrases | Pins the literal preflight_permissions.ts reference to Step 3b — the prose edit must keep that pin satisfied. |
Proposed
Section titled “Proposed”The Step-3b probe additionally checks whether the Write and Edit
tools are granted for the worktree path the task will use
(.sdlc/worktrees/<basename>/). When either is missing (absent from
allow, or matched by deny), the probe reports it on stdout in a
shape parallel to the Bash gaps (e.g. Write: missing Write(...)),
exits 1, and Step 3b surfaces it through the same AskUserQuestion
remediation. File-mutation denials surface before the worktree is
created rather than mid-implementation.
Approach
Section titled “Approach”- Add a tool-rule matcher to
preflight_permissions.tsalongsideallowsVerb: a method onResolvedPermissions(e.g.allowsTool(tool, targetPath)) that returns true iff a<tool>(...)allow rule coverstargetPathand no<tool>(...)deny rule matches it. Honour the bare-tool form ("Edit"/"Write"with no parens = all paths) and the path-scoped form (Edit(//abs/path/**)/Edit(~/rel/**)), resolving the same~/path semantics Claude Code uses. TreatdefaultMode: acceptEdits/bypassPermissions(read from the same settings files) as blanket allow. - In
main, after the Bash-signal gap collection, always probeWriteandEditagainst the would-be worktree path derived from the task file’s repo root:<repo-root>/.sdlc/worktrees/<task-basename>/. Append any missing tool to the gap list in the shape<tool>: missing <tool>(<worktree-glob>), so the existing exit-1 / one-line-per-gap contract carries the new gaps unchanged. - Update
plugin/skills/task-work/SKILL.mdStep 3b prose to name the Write/Edit probe and its remediation, keeping the literalpreflight_permissions.tsreference (invariant pin) intact. - Extend
plugin/skills/task-work/tests/preflight_permissions.test.ts: Write/Edit denied → exit 1 with the new line; granted (bare or path-scoped to the worktree glob) → no new gap;defaultMode: acceptEdits→ no gap; deny-beats-allow for tools mirrors the existing Bash case. - Refresh the per-skill doc
docs/skills/task-work.mdif its Step-3b description enumerates what the probe checks.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/skills/task-work/preflight_permissions.ts | modify | Add allowsTool tool-rule matcher + Write/Edit worktree-path probe in main; honour defaultMode |
plugin/skills/task-work/SKILL.md | modify | Step 3b prose documents the Write/Edit gap shape and remediation |
plugin/skills/task-work/tests/preflight_permissions.test.ts | modify | Cases for Write/Edit denied / granted / defaultMode / deny-beats-allow |
docs/skills/task-work.md | modify | Step-3b doc reflects the new file-mutation probe (if it enumerates the checks) |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: Run against a task whose resolved settings grant
Bashbut notWrite/Editfor the worktree path,preflight_permissions.tsexits 1 and stdout contains a line naming the missing file-mutation tool (e.g.Write: missing Write(...)and/orEdit: missing Edit(...)). - AC-2: Run against settings that grant
Edit/Write(bare, or path-scoped to.sdlc/worktrees/**) for the worktree path, the probe reports no file-mutation gap; withdefaultMode: acceptEditsit also reports no gap. - AC-3: A
denyentry forEdit(...)/Write(...)matching the worktree path beats a correspondingallow— the probe reports the gap (parallels the existingdeny beats allowBash test). - AC-4: Running
bun testagainstplugin/skills/task-work/tests/preflight_permissions.test.tspasses, including new cases for AC-1 / AC-2 / AC-3; the pre-existing Bash-signal tests still pass unchanged. - AC-5:
plugin/skills/task-work/SKILL.mdStep 3b prose names the Write/Edit probe;bun run plugin/scripts/lint_skill_prose.ts plugin/skills/task-work/SKILL.mdstill finds thepreflight_permissions.tsinvariant pin satisfied (the prose edit does not break the existing pin).
Out of scope
Section titled “Out of scope”- Probing every tool family Claude Code exposes (NotebookEdit, MultiEdit, WebFetch, MCP tools) —
only
WriteandEdit, the two the check_entities run hit. MultiEdit may be added if trivial, but is not required. - Auto-granting the missing permission — the probe reports; remediation stays the existing Step-3b AskUserQuestion flow (grant via /config, proceed anyway, or abort).
- Making the probe block when a gap is found — Step 3b already offers “proceed anyway”; the probe only surfaces, it does not hard-fail.
- Changing how the worktree path is chosen in Step 4 — the probe derives the same path Step 4 will create; it does not alter the worktree layout.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-06-02 UTC from T-M5RS-port-check-entities-to-ts in git@github.com:sksizer/dev.git.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-04. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
bun test plugin/skills/task-work/tests/preflight_permissions.test.ts(“AC-1: Write/Edit denied for the worktree path surfaces (exit 1)”): probe exits 1 withWrite: missing Write(...)/Edit: missing Edit(...)naming the worktree glob. - AC-2: auto — same suite covers bare grants, path-scoped
.sdlc/worktrees/**grants, anddefaultMode: acceptEdits; each reports no file-mutation gap (exit 0). - AC-3: auto — “AC-3: a deny on the worktree path beats an allow” asserts deny-beats-allow parallels the existing Bash case.
- AC-4: auto —
bun testagainst the suite: 18 pass / 0 fail; the pre-existing Bash-signal tests still pass unchanged. - AC-5: auto — SKILL.md Step 3b prose names the Write/Edit probe;
bun run plugin/scripts/lint_skill_prose.ts plugin/skills/task-work/SKILL.mdreports no violations (thepreflight_permissions.tsinvariant pin stays satisfied).
What worked
Section titled “What worked”- The implementation was already present (uncommitted) in the worktree from the paused dispatch;
resume picked it up cleanly.
allowsTool+worktreePathForwere complete and correct against the ACs. tsc --noEmitand the targetedbun testgave a fast, unambiguous green signal independent of the corpus-wide audit noise.
Friction and automation gaps
Section titled “Friction and automation gaps”- The baseline-gated quality gate reported 8
new-driftlines that were all benign- OKaudit-status lines fromaudit_entities.ts, flagged only because the baseline SHA (origin/main) predated the local-main commits the worktree branched from and the main checkout carried uncommitted task-file WIP — the per-entityOKlines should be excluded from the baseline diff (only non-OKaudit lines are real drift signal), or the baseline should be captured against localmain’s tip rather thanorigin/mainwhen they diverge. → T-TWZD-normalize-baseline-diff-nondeterministic-output
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-TWZD-normalize-baseline-diff-nondeterministic-output — linked (existing open/ready tracker
for the baseline-diff false-positive class; the per-entity
- OKaudit-line / divergent-baseline-SHA symptom is the same false-positive dimension it already owns).