diff --git a/.changeset/jolly-yaks-click.md b/.changeset/jolly-yaks-click.md
new file mode 100644
index 000000000..5e28ade3d
--- /dev/null
+++ b/.changeset/jolly-yaks-click.md
@@ -0,0 +1,5 @@
+---
+type: Fixed
+pr: 4006
+---
+**Interrupted executors can be resumed again** — execute-plan deleted `current-agent-id.txt` before the check that read it, so the interrupted-agent detection and its Task `resume` prompt were unreachable after a kill; the id is now captured before the stale marker is cleared. (#3795)
diff --git a/gsd-core/workflows/execute-plan.md b/gsd-core/workflows/execute-plan.md
index da0ad0069..a0d6c6a4b 100644
--- a/gsd-core/workflows/execute-plan.md
+++ b/gsd-core/workflows/execute-plan.md
@@ -155,11 +155,14 @@ Fresh context per subagent preserves peak quality. Main context stays lean.
if [ ! -f .planning/agent-history.json ]; then
echo '{"version":"1.0","max_entries":50,"entries":[]}' > .planning/agent-history.json
fi
-rm -f .planning/current-agent-id.txt
+# #3795: read a SURVIVING id before clearing it — the clear used to run
+# first, so the check below was always false and the resume prompt at this
+# step's tail was never offered.
if [ -f .planning/current-agent-id.txt ]; then
INTERRUPTED_ID=$(cat .planning/current-agent-id.txt)
echo "Found interrupted agent: $INTERRUPTED_ID"
fi
+rm -f .planning/current-agent-id.txt
```
If interrupted: ask user to resume (Task `resume` parameter) or start fresh.
diff --git a/tests/execute-plan-interrupted-agent-guard.test.cjs b/tests/execute-plan-interrupted-agent-guard.test.cjs
new file mode 100644
index 000000000..860fa5e45
--- /dev/null
+++ b/tests/execute-plan-interrupted-agent-guard.test.cjs
@@ -0,0 +1,50 @@
+'use strict';
+
+// ─────────────────────────────────────────────────────────────────────────────
+// #3795 — execute-plan's interrupted-agent detection must be REACHABLE.
+//
+// The init_agent_tracking step cleared `.planning/current-agent-id.txt`
+// BEFORE the existence check that read it, so the interrupted-agent branch
+// and the Task `resume` prompt it exists to offer could never execute: a
+// kill -9 mid-executor left the file, and the very next run deleted it
+// before looking. The read must precede the clear.
+// ─────────────────────────────────────────────────────────────────────────────
+
+const { test } = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const EXECUTE_PLAN_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'execute-plan.md');
+
+// execute-plan.md is shipped workflow text — the bytes ARE what the runtime
+// loads; a structural scan over the step's own fenced block tests the
+// deployed contract (same shape as tests/config-get-raw-guard.test.cjs).
+function initAgentTrackingBlock() {
+ const md = fs.readFileSync(EXECUTE_PLAN_MD, 'utf-8');
+ const step = md.indexOf('');
+ assert.ok(step > 0, 'execute-plan.md must contain the init_agent_tracking step');
+ const end = md.indexOf('', step);
+ assert.ok(end > step, 'init_agent_tracking step must close');
+ return md.slice(step, end);
+}
+
+test('#3795: the interrupted-agent read must precede the stale-id clear', () => {
+ const block = initAgentTrackingBlock();
+ const readCheck = block.indexOf('[ -f .planning/current-agent-id.txt ]');
+ const clear = block.indexOf('rm -f .planning/current-agent-id.txt');
+ assert.ok(readCheck > 0, 'the step must probe for a surviving current-agent-id.txt');
+ assert.ok(clear > 0, 'the step must clear the stale id for the fresh run');
+ assert.ok(
+ readCheck < clear,
+ '#3795: the existence check must run BEFORE the rm — delete-first made the interrupted-agent branch and its Task resume prompt unreachable',
+ );
+ assert.ok(
+ /INTERRUPTED_ID=\$\(/.test(block),
+ 'the surviving id must be captured while the file still exists',
+ );
+ assert.ok(
+ /resume/.test(block),
+ 'the resume-or-fresh prompt the read enables must stay present',
+ );
+});
diff --git a/tests/no-bare-gsd-tools-command-position.test.cjs b/tests/no-bare-gsd-tools-command-position.test.cjs
index 1826a0405..0816c4035 100644
--- a/tests/no-bare-gsd-tools-command-position.test.cjs
+++ b/tests/no-bare-gsd-tools-command-position.test.cjs
@@ -107,7 +107,7 @@ const PROSE_ALLOWLIST = [
{ file: 'agents/gsd-phase-researcher.md', line: 33, reason: 'package-legitimacy provenance rule names the command as the source of an OK verdict; descriptive' },
{ file: 'agents/gsd-roadmapper.md', line: 642, reason: 'parenthetical "e.g." naming SDK queries a user *could* run; not an agent instruction' },
{ file: 'agents/gsd-intel-updater.md', line: 40, reason: 'cross-platform note names the `gsd-tools intel ` CLI surface descriptively ("CLI invocations go through..."); not an agent instruction' },
- { file: 'gsd-core/workflows/execute-plan.md', line: 416, reason: 'describes the downstream SDK validation step (`validated downstream by ...`); names the mechanism, does not instruct the agent to type it' },
+ { file: 'gsd-core/workflows/execute-plan.md', line: 419, reason: 'describes the downstream SDK validation step (`validated downstream by ...`); names the mechanism, does not instruct the agent to type it' },
];
// Resolver-snippet definition lines / probes that must never be flagged. A line