code-yeongyu / code-yeongyu/lazycodex

Fix start-work retries on conclusive external blockers

Open
#144 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
3.5k
Forks
216
PR merge metrics
No merged PRs in 30d

Description

## 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 ``, the released hook exited 0 but returned:

```text
{"decision":"block", ...}
```

Failing-first regression:

```text
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
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 `` 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*(?:\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*(?:\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: [
+ "",
+ "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](https://github.com/code-yeongyu/lazycodex).
Tag: lazycodex-generated

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with plugins/omo/components/start-work-continuation/src/codex-hook.ts and the adjacent test/codex-hook.test.ts regression. Run npx vitest --run test/codex-hook.test.ts, then check the directive and dist/cli.js artifacts. Done means the marked external-blocker handoff produces empty output while unmarked active work still continues, with the component checks passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cli, developer-experience
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.