Issue Type
Feature — child slice S3 of EPIC #172. Depends on #176 (S1), #177 (S2). Canonical architecture: docs/adr/0009-mcp-admission-contract.md; see also ADR 0008-filesystem-mcp-bounded-context-grants.md.
Problem Statement
Bounded filesystem context grant recovery has real determinism gaps:
- Denying a required grant burns an execution attempt.
requiresFilesystemGrantApproval returns {blocked:false} for any denied effective phase regardless of blocking capabilities (web/lib/mcps/filesystem-grants.ts:315-322). The package is claimed, then effectiveFilesystemGrant returns empty caps for a denied phase and filesystemRuntimeMetadata throws Filesystem MCP context blocked (web/worker/work-package-executor.ts:~938-944,1049-1062,1515), consuming an attempt for a guaranteed failure — exactly the waste failWorkPackageForFilesystemGrant (work-package-handoff.ts:807-867) was built to prevent.
- Per-task
always_allow skips FAILED siblings; the project endpoint recovers them. The per-task route reconciles siblings only in pending/ready/blocked/needs_rework (web/app/api/tasks/[id]/filesystem-grants/route.ts:418-421); the project route also reconciles failed (web/app/api/projects/[id]/filesystem-grant/route.ts:166-243). The same grant state yields different outcomes depending on which endpoint issued it.
- Handoff's default branch never consults
projectMcpConfig (work-package-handoff.ts:896-899), unlike approval (approve/route.ts:167-187) and the executor — a latent approval-vs-handoff divergence when a package is covered by a live project grant but its persisted effective phase is still not_issued.
Desired Outcome
Denial of a required grant is a recorded, named, recoverable decision that holds execution pre-claim (zero attempts). Recovery is deterministic: the same grant state recovers the same package set regardless of endpoint. Project coverage is consulted consistently at approval and handoff.
User Story
As a Forge operator, when I deny or later approve bounded filesystem context, I want the package held (not crashed) and consistently recovered so retries are predictable and never waste attempts.
Requirements
requiresFilesystemGrantApproval (filesystem-grants.ts:303-339): do NOT blanket-return {blocked:false} for a denied effective phase when blockingCapabilities is non-empty. Return a distinct terminal outcome (e.g. deniedRequired:true) so the handoff gate holds the package pre-claim. Keep non-blocking only for optional/continue_without_mcp denials.
filesystemGrantHandoffBlock (work-package-handoff.ts:876-906): branch on the denied-required outcome to failWorkPackageForFilesystemGrant with a message that names the operator denial ("Filesystem context was denied for required capabilities X; re-approve to proceed") and record it under FILESYSTEM_GRANT_BLOCK_METADATA_KEY so the recovery endpoints can re-open it. Pass project?.mcpConfig into requiresFilesystemGrantApproval in BOTH branches (the default branch at :896-899 currently omits it).
- Per-task
always_allow sibling propagation (tasks/[id]/filesystem-grants/route.ts:418-421): extend selection to include failed grant-blocked siblings (reuse the failed-recovery status transition + task recovery already implemented for the primary package at :455-501), or delegate always_allow reconciliation to the shared routine used by projects/[id]/filesystem-grant/route.ts:166-243. Both endpoints must recover identical package sets.
- Preserve the confirmed-working baseline: filesystem blocks stay
terminalBlock:true with the distinct FILESYSTEM_GRANT_BLOCK_METADATA_KEY (never mcpBroker), so shouldAutoRetryBlockedHandoff (blocked-handoff-retry.ts:67-79) correctly refuses to auto-retry them; both recovery endpoints clear the marker and re-drive via redis lpush forge:approvals.
Acceptance Criteria
Dependencies
Implementation Scope
Large — worker handoff gate, filesystem-grants library, and two recovery API routes.
Issue Type
Feature — child slice S3 of EPIC #172. Depends on #176 (S1), #177 (S2). Canonical architecture:
docs/adr/0009-mcp-admission-contract.md; see also ADR0008-filesystem-mcp-bounded-context-grants.md.Problem Statement
Bounded filesystem context grant recovery has real determinism gaps:
requiresFilesystemGrantApprovalreturns{blocked:false}for anydeniedeffective phase regardless of blocking capabilities (web/lib/mcps/filesystem-grants.ts:315-322). The package is claimed, theneffectiveFilesystemGrantreturns empty caps for a denied phase andfilesystemRuntimeMetadatathrowsFilesystem MCP context blocked(web/worker/work-package-executor.ts:~938-944,1049-1062,1515), consuming an attempt for a guaranteed failure — exactly the wastefailWorkPackageForFilesystemGrant(work-package-handoff.ts:807-867) was built to prevent.always_allowskips FAILED siblings; the project endpoint recovers them. The per-task route reconciles siblings only inpending/ready/blocked/needs_rework(web/app/api/tasks/[id]/filesystem-grants/route.ts:418-421); the project route also reconcilesfailed(web/app/api/projects/[id]/filesystem-grant/route.ts:166-243). The same grant state yields different outcomes depending on which endpoint issued it.projectMcpConfig(work-package-handoff.ts:896-899), unlike approval (approve/route.ts:167-187) and the executor — a latent approval-vs-handoff divergence when a package is covered by a live project grant but its persisted effective phase is stillnot_issued.Desired Outcome
Denial of a required grant is a recorded, named, recoverable decision that holds execution pre-claim (zero attempts). Recovery is deterministic: the same grant state recovers the same package set regardless of endpoint. Project coverage is consulted consistently at approval and handoff.
User Story
As a Forge operator, when I deny or later approve bounded filesystem context, I want the package held (not crashed) and consistently recovered so retries are predictable and never waste attempts.
Requirements
requiresFilesystemGrantApproval(filesystem-grants.ts:303-339): do NOT blanket-return{blocked:false}for adeniedeffective phase whenblockingCapabilitiesis non-empty. Return a distinct terminal outcome (e.g.deniedRequired:true) so the handoff gate holds the package pre-claim. Keep non-blocking only for optional/continue_without_mcpdenials.filesystemGrantHandoffBlock(work-package-handoff.ts:876-906): branch on the denied-required outcome tofailWorkPackageForFilesystemGrantwith a message that names the operator denial ("Filesystem context was denied for required capabilities X; re-approve to proceed") and record it underFILESYSTEM_GRANT_BLOCK_METADATA_KEYso the recovery endpoints can re-open it. Passproject?.mcpConfigintorequiresFilesystemGrantApprovalin BOTH branches (the default branch at:896-899currently omits it).always_allowsibling propagation (tasks/[id]/filesystem-grants/route.ts:418-421): extend selection to includefailedgrant-blocked siblings (reuse the failed-recovery status transition + task recovery already implemented for the primary package at:455-501), or delegatealways_allowreconciliation to the shared routine used byprojects/[id]/filesystem-grant/route.ts:166-243. Both endpoints must recover identical package sets.terminalBlock:truewith the distinctFILESYSTEM_GRANT_BLOCK_METADATA_KEY(nevermcpBroker), soshouldAutoRetryBlockedHandoff(blocked-handoff-retry.ts:67-79) correctly refuses to auto-retry them; both recovery endpoints clear the marker and re-drive viaredis lpush forge:approvals.Acceptance Criteria
Filesystem MCP context blockedthrow at the executor for that case).always_allowgrant recovers the same package set (includingfailedsiblings) whether issued from the per-task or project endpoint.requiresFilesystemGrantApprovalis passedprojectMcpConfigfrom handoff in both branches; approval and handoff agree on project coverage.continue_without_mcpdenials remain non-blocking; terminal non-auto-retry behavior preserved.npm run lint,npx tsc --noEmit,npm test,npm run buildpass.Dependencies
Implementation Scope
Large — worker handoff gate, filesystem-grants library, and two recovery API routes.