Problem Situation
The start-work Stop hook forced automatic continuation after the agent had already identified a conclusive external blocker such as missing hardware or authorization. The injected directive required three same-failure cycles and reviewer escalation, so the agent repeated an unchanged external-state check instead of reporting the blocker and stopping.
Reproduction Logs
With active Boulder work, one unchecked plan item, and the first line of last_assistant_message set to <start-work-blocked-external>, the released hook exited 0 but returned:
{"decision":"block", ...}
Failing-first regression:
Test Files 1 failed (1)
Tests 1 failed | 10 passed (11)
AssertionError: expected decision=block output to be empty
Root Cause
runStopHook ignored last_assistant_message and returned decision: "block" for every active tracked plan. The directive separately applied its three-cycle reviewer rule to all failures, without distinguishing agent-controllable remediation failures from immutable external blockers.
Verified Fix
The directive now requires a one-check user handoff for conclusive external blockers and reserves reviewer escalation for three materially different agent-controllable remediation attempts. The hook consumes a structural first-line marker and allows that turn to stop, while unmarked active work continues normally.
diff --git a/plugins/omo/components/start-work-continuation/directive.md b/plugins/omo/components/start-work-continuation/directive.md
index 518d6a6..bc35d43 100644
--- a/plugins/omo/components/start-work-continuation/directive.md
+++ b/plugins/omo/components/start-work-continuation/directive.md
@@ -45,7 +45,8 @@ Before completion, satisfy all five `review-work` lanes and a `debugging` runtim
# Stop conditions for THIS turn
- A top-level checkbox flipped to `- [x]` after the 5-phase QA gate (Phase 1 read, Phase 2 automated, Phase 3 channel scenario, Phase 4 adversarial-class probing, Phase 5 gate decision). Then the Stop hook will re-evaluate; if more checkboxes remain you will be continued again.
-- 3 same-failure cycles on one sub-task → escalate via `multi_agent_v1.spawn_agent({"message":"TASK: act as a rigorous reviewer. DELIVERABLE: diagnose the repeated sub-task failure and recommend the next safe action. VERIFY: cite the failing evidence.","fork_context":false})` and stop dispatch.
+- A conclusive external blocker that only the user or external state can change (missing hardware, authorization, credential, permission, or unavailable service) → after one authoritative check, stop all retries and reviewer dispatch. Start the user-facing handoff with `<start-work-blocked-external>` on its own first line, then state the exact blocker and observable resume condition. The Stop hook recognizes this marker and lets the turn end immediately.
+- 3 materially different failed remediation approaches on an agent-controllable sub-task → escalate via `multi_agent_v1.spawn_agent({"message":"TASK: act as a rigorous reviewer. DELIVERABLE: diagnose the repeated sub-task failure and recommend the next safe action. VERIFY: cite the failing evidence.","fork_context":false})` and stop dispatch.
- Safety boundary (destructive command, secret exfiltration, production write) → stop and surface a safe substitute.
- All top-level checkboxes `- [x]` AND the Global Review and Debugging Gate passed → print the ORCHESTRATION COMPLETE block and end.
diff --git a/plugins/omo/components/start-work-continuation/dist/cli.js b/plugins/omo/components/start-work-continuation/dist/cli.js
index f33460c..a4d7e40 100755
--- a/plugins/omo/components/start-work-continuation/dist/cli.js
+++ b/plugins/omo/components/start-work-continuation/dist/cli.js
@@ -302,11 +302,14 @@ import { readFileSync as readFileSync3 } from "node:fs";
var START_WORK_CONTINUATION_DIRECTIVE = readFileSync3(new URL("../directive.md", import.meta.url), "utf8");
// components/start-work-continuation/src/codex-hook.ts
+var EXTERNAL_BLOCKER_HANDOFF_PATTERN = /^\s*<start-work-blocked-external>(?:\r?\n|$)/;
function runStopHook(input, fs) {
if (!isStopInput(input))
return "";
if (input.stop_hook_active)
return "";
+ if (EXTERNAL_BLOCKER_HANDOFF_PATTERN.test(input.last_assistant_message ?? ""))
+ return "";
if (transcriptHasContextPressureMarker(input.transcript_path, fs))
return "";
const state = readContinuationState(input.cwd, input.session_id);
diff --git a/plugins/omo/components/start-work-continuation/src/codex-hook.ts b/plugins/omo/components/start-work-continuation/src/codex-hook.ts
index f794047..f6b93e3 100644
--- a/plugins/omo/components/start-work-continuation/src/codex-hook.ts
+++ b/plugins/omo/components/start-work-continuation/src/codex-hook.ts
@@ -3,9 +3,12 @@ import { readContinuationState } from "./boulder-reader.js";
import { START_WORK_CONTINUATION_DIRECTIVE } from "./directive.js";
import type { ReadonlyFileSystem, StopHookEventName, StopHookOutput, StopInput } from "./types.js";
+const EXTERNAL_BLOCKER_HANDOFF_PATTERN = /^\s*<start-work-blocked-external>(?:\r?\n|$)/;
+
export function runStopHook(input: unknown, fs: ReadonlyFileSystem): string {
if (!isStopInput(input)) return "";
if (input.stop_hook_active) return "";
+ if (EXTERNAL_BLOCKER_HANDOFF_PATTERN.test(input.last_assistant_message ?? "")) return "";
if (transcriptHasContextPressureMarker(input.transcript_path, fs)) return "";
const state = readContinuationState(input.cwd, input.session_id);
if (state === null) return "";
diff --git a/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts b/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts
index 14b4a24..b1ac870 100644
--- a/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts
+++ b/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts
@@ -90,6 +90,29 @@ describe("start-work Stop hook", () => {
expect(parsed.reason).toContain("re-read the ledger record and verify the exact lane/SHA pair");
});
+ it("#given an external blocker handoff #when hook runs #then it allows the turn to stop immediately", () => {
+ // given
+ const workspace = createWorkspace({
+ boulderJson: createBoulderJson({ sessionIds: ["codex:sess_abc"], status: "active" }),
+ planMarkdown: ["# Plan", "", "## TODOs", "- [ ] 1. Run device QA"].join("\n"),
+ });
+ const fs = createMemoryFs();
+ const input = {
+ ...createStopInput(workspace),
+ last_assistant_message: [
+ "<start-work-blocked-external>",
+ "Blocker: no authorized Android device.",
+ "Resume when adb reports one device.",
+ ].join("\n"),
+ };
+
+ // when
+ const output = runStopHook(input, fs);
+
+ // then
+ expect(output).toBe("");
+ });
+
it("#given context-window pressure in transcript #when hook runs #then it does not inject continuation text", () => {
// given
const transcriptPath = "/repo/transcript.jsonl";
Verification
- RED:
npx vitest --run test/codex-hook.test.ts → 1 failed, 10 passed before the fix.
- GREEN: the same command → 11 passed.
- Full component suite:
npx vitest --run → 38 passed.
- Typecheck:
npx tsc --noEmit → exit 0.
- Lint:
npx biome check . → exit 0.
- Build: aggregate-path Bun bundle → exit 0.
- Manual CLI QA against the built hook: marked handoff → exit 0 and 0 output bytes; identical unmarked active work →
decision=block.
This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated
Problem Situation
The start-work Stop hook forced automatic continuation after the agent had already identified a conclusive external blocker such as missing hardware or authorization. The injected directive required three same-failure cycles and reviewer escalation, so the agent repeated an unchanged external-state check instead of reporting the blocker and stopping.
Reproduction Logs
With active Boulder work, one unchecked plan item, and the first line of
last_assistant_messageset to<start-work-blocked-external>, the released hook exited 0 but returned:Failing-first regression:
Root Cause
runStopHookignoredlast_assistant_messageand returneddecision: "block"for every active tracked plan. The directive separately applied its three-cycle reviewer rule to all failures, without distinguishing agent-controllable remediation failures from immutable external blockers.Verified Fix
The directive now requires a one-check user handoff for conclusive external blockers and reserves reviewer escalation for three materially different agent-controllable remediation attempts. The hook consumes a structural first-line marker and allows that turn to stop, while unmarked active work continues normally.
Verification
npx vitest --run test/codex-hook.test.ts→ 1 failed, 10 passed before the fix.npx vitest --run→ 38 passed.npx tsc --noEmit→ exit 0.npx biome check .→ exit 0.decision=block.This fix was debugged, implemented, and verified with LazyCodex.
Tag: lazycodex-generated