From ae0266e5ad06c8881f2bbc21f77421f98fb42729 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Thu, 19 Feb 2026 12:09:44 -0800 Subject: [PATCH 01/41] default tries 100 --- check-completion.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/check-completion.sh b/check-completion.sh index 4fa09db..1cd849b 100755 --- a/check-completion.sh +++ b/check-completion.sh @@ -6,7 +6,7 @@ # TASKMASTER_DONE:: # # Optional env vars: -# TASKMASTER_MAX Max number of blocks before allowing stop (default: 0 = infinite) +# TASKMASTER_MAX Max number of blocks before allowing stop (default: 100) # set -euo pipefail @@ -35,7 +35,7 @@ fi COUNTER_DIR="${TMPDIR:-/tmp}/taskmaster" mkdir -p "$COUNTER_DIR" COUNTER_FILE="${COUNTER_DIR}/${SESSION_ID}" -MAX=${TASKMASTER_MAX:-0} +MAX=${TASKMASTER_MAX:-100} COUNT=0 if [ -f "$COUNTER_FILE" ]; then @@ -92,7 +92,7 @@ fi NEXT=$((COUNT + 1)) echo "$NEXT" > "$COUNTER_FILE" -# Optional escape hatch. Default is infinite (0) so hook keeps firing. +# Optional escape hatch after MAX continuations. if [ "$MAX" -gt 0 ] && [ "$NEXT" -ge "$MAX" ]; then rm -f "$COUNTER_FILE" exit 0 From 67bd2c42902f43289abf46ed1cdb9bac466ecb72 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Thu, 19 Feb 2026 12:11:16 -0800 Subject: [PATCH 02/41] docs: add session summary (default-tries-100) --- .../2026-02-19-121116-default-tries-100.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/session-summaries/2026-02-19-121116-default-tries-100.md diff --git a/docs/session-summaries/2026-02-19-121116-default-tries-100.md b/docs/session-summaries/2026-02-19-121116-default-tries-100.md new file mode 100644 index 0000000..28517a4 --- /dev/null +++ b/docs/session-summaries/2026-02-19-121116-default-tries-100.md @@ -0,0 +1,27 @@ +# Session Summary + +**Date:** 2026-02-19 +**Time:** 12:11 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 1 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `25bf291` - default tries 100 + +## Key Changes + +### Files Modified +[Review git diff for details] + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From a7542fe80b83e0b750f48c5b72f13cec4a9ac7ca Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Thu, 19 Feb 2026 14:28:57 -0800 Subject: [PATCH 03/41] docs: add session summary (make-installsh-posix-portable-) --- ...9-142857-make-installsh-posix-portable-.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/session-summaries/2026-02-19-142857-make-installsh-posix-portable-.md diff --git a/docs/session-summaries/2026-02-19-142857-make-installsh-posix-portable-.md b/docs/session-summaries/2026-02-19-142857-make-installsh-posix-portable-.md new file mode 100644 index 0000000..dc116d7 --- /dev/null +++ b/docs/session-summaries/2026-02-19-142857-make-installsh-posix-portable-.md @@ -0,0 +1,27 @@ +# Session Summary + +**Date:** 2026-02-19 +**Time:** 14:28 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 1 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `cbff59c` - Make install.sh POSIX-portable (sh shebang, portable pipefail) + +## Key Changes + +### Files Modified +[Review git diff for details] + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From 689d83016a83d5d0ced720acd1da4ef224561953 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Mon, 23 Feb 2026 14:53:41 -0800 Subject: [PATCH 04/41] docs: add session summary (docs-add-session-summary-make-) --- ...3-145341-docs-add-session-summary-make-.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/session-summaries/2026-02-23-145341-docs-add-session-summary-make-.md diff --git a/docs/session-summaries/2026-02-23-145341-docs-add-session-summary-make-.md b/docs/session-summaries/2026-02-23-145341-docs-add-session-summary-make-.md new file mode 100644 index 0000000..e31fb6c --- /dev/null +++ b/docs/session-summaries/2026-02-23-145341-docs-add-session-summary-make-.md @@ -0,0 +1,30 @@ +# Session Summary + +**Date:** 2026-02-23 +**Time:** 14:53 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 4 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `fde5036` - docs: add session summary (make-installsh-posix-portable-) +- `46f6a44` - Make install.sh POSIX-portable (sh shebang, portable pipefail) +- `31694ca` - docs: add session summary (default-tries-100) +- `0940f36` - default tries 100 + +## Key Changes + +### Files Modified +[Review git diff for details] + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From 6a4bd8af7f6e71aee3a4d099b83e7e14602850cd Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 04:34:07 -0800 Subject: [PATCH 05/41] docs: add session summary (hide-verbose-checklist-from-us) --- ...5-043407-hide-verbose-checklist-from-us.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/session-summaries/2026-02-25-043407-hide-verbose-checklist-from-us.md diff --git a/docs/session-summaries/2026-02-25-043407-hide-verbose-checklist-from-us.md b/docs/session-summaries/2026-02-25-043407-hide-verbose-checklist-from-us.md new file mode 100644 index 0000000..79f2973 --- /dev/null +++ b/docs/session-summaries/2026-02-25-043407-hide-verbose-checklist-from-us.md @@ -0,0 +1,34 @@ +# Session Summary + +**Date:** 2026-02-25 +**Time:** 04:34 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 1 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `9c25e5d` - hide verbose checklist from user output, use TASKMASTER_DONE signal detection + +## Key Changes + +### Files Modified +- `SKILL.md` +- `check-completion.sh` +- `docs/SPEC.md` +- `docs/session-summaries/2026-02-19-121116-default-tries-100.md` +- `docs/session-summaries/2026-02-19-142857-make-installsh-posix-portable-.md` +- `docs/session-summaries/2026-02-23-145341-docs-add-session-summary-make-.md` +- `hooks/check-completion.sh` +- `install.sh` + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From 8eb006c106f8bd26a12eb0cb91ededf7669552d3 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 04:38:14 -0800 Subject: [PATCH 06/41] release v2.3.0: minimal hook output, TASKMASTER_DONE signal detection --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a86ca32 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +All notable changes to Taskmaster are documented here. + +## [2.3.0] - 2026-02-25 + +### Changed +- Hook `reason` field now contains only the TASKMASTER_DONE signal token instead + of the full completion checklist. This keeps user-visible terminal output + minimal — one collapsed line rather than a wall of text. +- Full completion checklist lives exclusively in SKILL.md, which is always + loaded as system context. The agent already has all instructions; the reason + field no longer needs to duplicate them. +- Added `last_assistant_message` as the primary done-signal detection path + (faster, no transcript file parsing required). Transcript-based check is + retained as fallback. +- Removed `HAS_RECENT_ERRORS` / `stop_hook_active` escape-hatch logic in favor + of the explicit TASKMASTER_DONE signal protocol. +- `hooks/check-completion.sh` brought in sync with root-level canonical source. + +## [2.2.0] - 2026-02-19 + +### Changed +- Default `TASKMASTER_MAX` set to 100 (previously 0 / infinite). +- Moved full completion checklist from hook `reason` into SKILL.md system + context (first pass; reason still contained a short prompt). +- `install.sh` made POSIX-portable (`sh` shebang, conditional `pipefail`). + +### Fixed +- Resolved infinite loop caused by `set -euo pipefail` in sh-sourced contexts. + +## [2.1.0] + +### Added +- Session-scoped counter with configurable `TASKMASTER_MAX` escape hatch. +- Subagent skip: transcripts shorter than 20 lines are ignored. +- `TASKMASTER_DONE_PREFIX` env var for customising the done token prefix. + +## [2.0.0] + +### Added +- TASKMASTER_DONE signal protocol: stop is allowed only after the agent emits + `TASKMASTER_DONE::` in its response. +- Transcript-based done-signal detection. + +## [1.0.0] + +### Added +- Initial release: stop hook that blocks agent from stopping prematurely. +- Completion checklist injected via hook `reason` field. +- `TASKMASTER_MAX` loop guard. From 9a17abf8f2b0f3a66f3f6628753b80a3fbdf20a9 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 05:11:08 -0800 Subject: [PATCH 07/41] Add blog post: taskmaster hook cleanup (3 versions) --- ...02-25-taskmaster-hook-cleanup-humanized.md | 95 +++++++++++++++++++ .../2026-02-25-taskmaster-hook-cleanup-sws.md | 95 +++++++++++++++++++ .../2026-02-25-taskmaster-hook-cleanup.md | 93 ++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md create mode 100644 docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md create mode 100644 docs/blog/2026-02-25-taskmaster-hook-cleanup.md diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md new file mode 100644 index 0000000..6243b8c --- /dev/null +++ b/docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md @@ -0,0 +1,95 @@ +# Cleaning up taskmaster's terminal output + +**2026-02-25** + +I built [taskmaster](https://github.com/micahstubbs/taskmaster) a few months ago to stop Claude Code from quitting early. The stop [hook](https://github.com/micahstubbs/taskmaster/blob/main/check-completion.sh) fires every time the agent tries to stop and blocks it until it emits an explicit `TASKMASTER_DONE::` token — a parseable signal that confirms the agent actually finished. + +It works. The terminal output, though, was a mess. + +#### The problem + +Every time the hook blocked a stop attempt, Claude Code dumped the full completion checklist into the terminal: + +``` +● Ran 9 stop hooks (ctrl+o to expand) + ⎿ Stop hook error: TASKMASTER (1/100): Verify that all work is truly complete + before stopping. + + Before stopping, do each of these checks: + + 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete request... + 2. CHECK THE TASK LIST. Review every task. Any task not marked completed?... + 3. CHECK THE PLAN. Walk through each step... + 4. CHECK FOR ERRORS. Did any tool call, build, test, or lint fail?... + 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder code... +``` + +Fifteen lines, every time, accumulating across a long session. The checklist is instructions *for the AI* — I never needed to read it. + +#### How the `reason` field works + +Claude Code stop hooks return JSON when they want to block a stop: + +```json +{ "decision": "block", "reason": "..." } +``` + +The `reason` field does two things at once: + +1. **User-visible output** — shown in the terminal as a "Stop hook error" +2. **AI context** — injected back into the conversation so the agent knows what to do next + +I was putting the full checklist in `reason` so the agent had its instructions. Which meant I was also printing the full checklist to my terminal. Every single stop attempt. + +#### What I was missing + +The AI already has the checklist. Every Claude Code [skill file](https://github.com/micahstubbs/taskmaster/blob/main/SKILL.md) loads into system context at session start. The agent doesn't need instructions repeated in the hook reason — it just needs to know the specific token to emit. + +So I stripped the reason down to exactly that: + +```bash +DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" + +jq -n --arg reason "$DONE_SIGNAL" '{ decision: "block", reason: $reason }' +``` + +Now the terminal shows one collapsed line: + +``` +● Ran N stop hooks (ctrl+o to expand) + ⎿ Stop hook error: TASKMASTER_DONE::abc123xyz +``` + +The agent sees the signal it needs. I see almost nothing. Both of us get what we need from the same field. + +#### Faster signal detection too + +While I was in there I also changed how the hook detects the done signal. The old version opened the transcript file and scanned potentially hundreds of lines of JSON on every stop attempt. + +The Claude Code hook API passes `last_assistant_message` directly in the hook's input JSON. Checking that first skips the file read in the common case: + +```bash +LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""') +if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; then + HAS_DONE_SIGNAL=true +fi + +# Only scan the transcript if the message check didn't match +if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then + if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then + HAS_DONE_SIGNAL=true + fi +fi +``` + +When the agent just emitted the done signal in its last message — the normal case — no transcript parsing happens. + +#### The lesson + +Hook reasons and system context have different jobs. System context (skill files, `CLAUDE.md`) carries persistent instructions that shape behavior across a whole session. Hook reasons carry transient, stop-specific information — the minimum the agent needs right now. + +Here that's: "emit `TASKMASTER_DONE::abc123` and you're done." + +The checklist still runs. The enforcement is unchanged. It just doesn't print to my terminal anymore. + +These changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md new file mode 100644 index 0000000..d9f331b --- /dev/null +++ b/docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md @@ -0,0 +1,95 @@ +# Cleaning up taskmaster's terminal output + +**2026-02-25** + +I built [taskmaster](https://github.com/micahstubbs/taskmaster) a few months ago to stop Claude Code from quitting early. The stop [hook](https://github.com/micahstubbs/taskmaster/blob/main/check-completion.sh) fires every time the agent tries to stop and blocks it until it emits an explicit `TASKMASTER_DONE::` token — a parseable signal that lets external tooling know the agent genuinely finished. + +It works well. The terminal output, though, had gotten out of hand. + +#### The problem: a wall of text on every stop attempt + +Every time the hook blocked a stop attempt, Claude Code would dump the full completion checklist into the terminal: + +``` +● Ran 9 stop hooks (ctrl+o to expand) + ⎿ Stop hook error: TASKMASTER (1/100): Verify that all work is truly complete + before stopping. + + Before stopping, do each of these checks: + + 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete request... + 2. CHECK THE TASK LIST. Review every task. Any task not marked completed?... + 3. CHECK THE PLAN. Walk through each step... + 4. CHECK FOR ERRORS. Did any tool call, build, test, or lint fail?... + 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder code... +``` + +Fifteen lines of text, every time. In a long session with multiple blocked stops, that accumulates fast. The checklist is instructions *for the AI* — the user never needed to see it. + +#### The dual-use trap in Claude Code hook reasons + +Claude Code stop hooks return a JSON object when they want to block a stop: + +```json +{ "decision": "block", "reason": "..." } +``` + +The `reason` field does two things at once: + +1. **User-visible output** — displayed in the terminal as a "Stop hook error" +2. **AI context** — injected back into the conversation so the agent knows why it was blocked and what to do next + +That dual-use created the problem. To give the AI its instructions, I was putting the full checklist in `reason`. Which meant the user was also seeing the full checklist. Every. Single. Time. + +#### The fix: skill files are already system context + +Here's what I was missing: the AI already has the full completion checklist. Every Claude Code [skill file](https://github.com/micahstubbs/taskmaster/blob/main/SKILL.md) is loaded into system context at session start. The agent doesn't need the checklist repeated in the hook reason — it's already there. + +The hook reason only needs to communicate one thing: the specific done signal the agent must emit to satisfy the hook. + +```bash +DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" + +jq -n --arg reason "$DONE_SIGNAL" '{ decision: "block", reason: $reason }' +``` + +Now the terminal shows a single collapsed line: + +``` +● Ran N stop hooks (ctrl+o to expand) + ⎿ Stop hook error: TASKMASTER_DONE::abc123xyz +``` + +The agent sees the exact signal it needs to emit. The user sees almost nothing. Both get what they need from the same field. + +#### Also improved: done-signal detection + +While I was in there I also improved how the hook detects the done signal. The old version parsed the transcript file every time — opening and scanning potentially hundreds of lines of JSON on every stop attempt. + +The Claude Code hook API exposes `last_assistant_message` directly in the hook's input JSON. Checking that first is much faster: + +```bash +LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""') +if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; then + HAS_DONE_SIGNAL=true +fi + +# Fallback to transcript scan only if needed +if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then + if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then + HAS_DONE_SIGNAL=true + fi +fi +``` + +In the common case — where the agent just emitted the done signal in its last message — no transcript parsing happens at all. + +#### Separating user output from AI instructions + +The broader lesson for hook design: user-visible output and AI instructions have different lifetimes and audiences. System context (skill files, `CLAUDE.md`) is the right home for persistent instructions that shape the agent's behavior across a whole session. Hook reasons are for transient, stop-specific signals — the minimum information the agent needs right now to know what to do next. + +In this case that's: "emit `TASKMASTER_DONE::abc123` and you can stop." That's the whole message. + +The checklist still runs. The enforcement is still there. It's just not printing to your terminal anymore. + +These changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md new file mode 100644 index 0000000..98ec081 --- /dev/null +++ b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md @@ -0,0 +1,93 @@ +# Cleaning up taskmaster's terminal output + +**2026-02-25** + +I built [taskmaster](https://github.com/micahstubbs/taskmaster) a few months ago to solve a real problem: Claude Code would sometimes stop working before actually finishing a task. The stop hook forces the agent to keep going until it emits an explicit `TASKMASTER_DONE::` signal — a parseable token that gives external tooling a deterministic completion marker. + +It works. But the terminal output was a mess. + +## The problem + +Every time the hook blocked a stop, Claude Code would display the full completion checklist in the user-visible terminal output: + +``` +● Ran 9 stop hooks (ctrl+o to expand) + ⎿ Stop hook error: TASKMASTER (1/100): Verify that all work is truly complete + before stopping. + + Before stopping, do each of these checks: + + 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete request... + 2. CHECK THE TASK LIST. Review every task. Any task not marked completed?... + 3. CHECK THE PLAN. Walk through each step... + 4. CHECK FOR ERRORS. Did any tool call, build, test, or lint fail?... + 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder code... +``` + +That's a 15-line wall of text every time the hook fires. In a long session with multiple stop attempts, this pollution accumulates. The checklist is instructions *for the AI*, not the user — it doesn't need to be on screen. + +## How Claude Code hook reasons work + +Claude Code stop hooks return a JSON object when they want to block: + +```json +{ "decision": "block", "reason": "..." } +``` + +The `reason` field serves two purposes simultaneously: + +1. **User-visible terminal output** — shown in the UI as a "Stop hook error" +2. **AI context** — injected back into the conversation so the agent knows why it was blocked + +This dual-use is the root of the problem. If you put the full instructions in `reason` so the AI has them, the user sees a wall of text. But you need the AI to know what to do. + +## The fix: separate instructions from signal + +The key insight: the AI already has the full completion checklist in system context via `SKILL.md`. Every Claude Code skill file is loaded at session start — the agent knows what to do when blocked without being told again in the hook reason. + +So the hook reason only needs to contain one thing: the done signal token the agent must emit to satisfy the hook. + +```bash +DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" + +jq -n --arg reason "$DONE_SIGNAL" '{ decision: "block", reason: $reason }' +``` + +Now the terminal shows at most one collapsed line: + +``` +● Ran N stop hooks (ctrl+o to expand) + ⎿ Stop hook error: TASKMASTER_DONE::abc123xyz +``` + +The agent sees the done signal it needs to emit. The user sees almost nothing. Both get what they need. + +## Improving the done-signal detection + +While I was in the hook, I also upgraded how it detects the done signal. The old version parsed the transcript file — opening and scanning potentially hundreds of lines of JSON on every stop attempt. + +The newer Claude Code API exposes `last_assistant_message` directly in the hook's JSON input. Checking that first is much faster and avoids the transcript entirely in the happy path: + +```bash +LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""') +if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; then + HAS_DONE_SIGNAL=true +fi + +# Fallback to transcript only if needed +if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then + if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then + HAS_DONE_SIGNAL=true + fi +fi +``` + +## The broader lesson + +When designing hooks and other automation that wraps AI agents, it helps to keep user-visible output and AI-context separate. System context (skills, CLAUDE.md) is the right place for persistent instructions. Hook reasons are for transient signals — the specific thing the agent needs right now to unblock itself. + +In this case: "emit `TASKMASTER_DONE::abc123` to stop." That's it. + +The full completion checklist is still there, still enforced, still directing the agent's behavior. It's just not cluttering the terminal anymore. + +The changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). From daeed44113e4f7e979c38663dae6f98dd7e069b1 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 05:33:21 -0800 Subject: [PATCH 08/41] manual blog post edits --- ...02-25-taskmaster-hook-cleanup-humanized.md | 95 ----------------- .../2026-02-25-taskmaster-hook-cleanup-sws.md | 95 ----------------- .../2026-02-25-taskmaster-hook-cleanup.md | 100 +++++++++++------- 3 files changed, 64 insertions(+), 226 deletions(-) delete mode 100644 docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md delete mode 100644 docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md deleted file mode 100644 index 6243b8c..0000000 --- a/docs/blog/2026-02-25-taskmaster-hook-cleanup-humanized.md +++ /dev/null @@ -1,95 +0,0 @@ -# Cleaning up taskmaster's terminal output - -**2026-02-25** - -I built [taskmaster](https://github.com/micahstubbs/taskmaster) a few months ago to stop Claude Code from quitting early. The stop [hook](https://github.com/micahstubbs/taskmaster/blob/main/check-completion.sh) fires every time the agent tries to stop and blocks it until it emits an explicit `TASKMASTER_DONE::` token — a parseable signal that confirms the agent actually finished. - -It works. The terminal output, though, was a mess. - -#### The problem - -Every time the hook blocked a stop attempt, Claude Code dumped the full completion checklist into the terminal: - -``` -● Ran 9 stop hooks (ctrl+o to expand) - ⎿ Stop hook error: TASKMASTER (1/100): Verify that all work is truly complete - before stopping. - - Before stopping, do each of these checks: - - 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete request... - 2. CHECK THE TASK LIST. Review every task. Any task not marked completed?... - 3. CHECK THE PLAN. Walk through each step... - 4. CHECK FOR ERRORS. Did any tool call, build, test, or lint fail?... - 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder code... -``` - -Fifteen lines, every time, accumulating across a long session. The checklist is instructions *for the AI* — I never needed to read it. - -#### How the `reason` field works - -Claude Code stop hooks return JSON when they want to block a stop: - -```json -{ "decision": "block", "reason": "..." } -``` - -The `reason` field does two things at once: - -1. **User-visible output** — shown in the terminal as a "Stop hook error" -2. **AI context** — injected back into the conversation so the agent knows what to do next - -I was putting the full checklist in `reason` so the agent had its instructions. Which meant I was also printing the full checklist to my terminal. Every single stop attempt. - -#### What I was missing - -The AI already has the checklist. Every Claude Code [skill file](https://github.com/micahstubbs/taskmaster/blob/main/SKILL.md) loads into system context at session start. The agent doesn't need instructions repeated in the hook reason — it just needs to know the specific token to emit. - -So I stripped the reason down to exactly that: - -```bash -DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" - -jq -n --arg reason "$DONE_SIGNAL" '{ decision: "block", reason: $reason }' -``` - -Now the terminal shows one collapsed line: - -``` -● Ran N stop hooks (ctrl+o to expand) - ⎿ Stop hook error: TASKMASTER_DONE::abc123xyz -``` - -The agent sees the signal it needs. I see almost nothing. Both of us get what we need from the same field. - -#### Faster signal detection too - -While I was in there I also changed how the hook detects the done signal. The old version opened the transcript file and scanned potentially hundreds of lines of JSON on every stop attempt. - -The Claude Code hook API passes `last_assistant_message` directly in the hook's input JSON. Checking that first skips the file read in the common case: - -```bash -LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""') -if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; then - HAS_DONE_SIGNAL=true -fi - -# Only scan the transcript if the message check didn't match -if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then - if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then - HAS_DONE_SIGNAL=true - fi -fi -``` - -When the agent just emitted the done signal in its last message — the normal case — no transcript parsing happens. - -#### The lesson - -Hook reasons and system context have different jobs. System context (skill files, `CLAUDE.md`) carries persistent instructions that shape behavior across a whole session. Hook reasons carry transient, stop-specific information — the minimum the agent needs right now. - -Here that's: "emit `TASKMASTER_DONE::abc123` and you're done." - -The checklist still runs. The enforcement is unchanged. It just doesn't print to my terminal anymore. - -These changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md deleted file mode 100644 index d9f331b..0000000 --- a/docs/blog/2026-02-25-taskmaster-hook-cleanup-sws.md +++ /dev/null @@ -1,95 +0,0 @@ -# Cleaning up taskmaster's terminal output - -**2026-02-25** - -I built [taskmaster](https://github.com/micahstubbs/taskmaster) a few months ago to stop Claude Code from quitting early. The stop [hook](https://github.com/micahstubbs/taskmaster/blob/main/check-completion.sh) fires every time the agent tries to stop and blocks it until it emits an explicit `TASKMASTER_DONE::` token — a parseable signal that lets external tooling know the agent genuinely finished. - -It works well. The terminal output, though, had gotten out of hand. - -#### The problem: a wall of text on every stop attempt - -Every time the hook blocked a stop attempt, Claude Code would dump the full completion checklist into the terminal: - -``` -● Ran 9 stop hooks (ctrl+o to expand) - ⎿ Stop hook error: TASKMASTER (1/100): Verify that all work is truly complete - before stopping. - - Before stopping, do each of these checks: - - 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete request... - 2. CHECK THE TASK LIST. Review every task. Any task not marked completed?... - 3. CHECK THE PLAN. Walk through each step... - 4. CHECK FOR ERRORS. Did any tool call, build, test, or lint fail?... - 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder code... -``` - -Fifteen lines of text, every time. In a long session with multiple blocked stops, that accumulates fast. The checklist is instructions *for the AI* — the user never needed to see it. - -#### The dual-use trap in Claude Code hook reasons - -Claude Code stop hooks return a JSON object when they want to block a stop: - -```json -{ "decision": "block", "reason": "..." } -``` - -The `reason` field does two things at once: - -1. **User-visible output** — displayed in the terminal as a "Stop hook error" -2. **AI context** — injected back into the conversation so the agent knows why it was blocked and what to do next - -That dual-use created the problem. To give the AI its instructions, I was putting the full checklist in `reason`. Which meant the user was also seeing the full checklist. Every. Single. Time. - -#### The fix: skill files are already system context - -Here's what I was missing: the AI already has the full completion checklist. Every Claude Code [skill file](https://github.com/micahstubbs/taskmaster/blob/main/SKILL.md) is loaded into system context at session start. The agent doesn't need the checklist repeated in the hook reason — it's already there. - -The hook reason only needs to communicate one thing: the specific done signal the agent must emit to satisfy the hook. - -```bash -DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" - -jq -n --arg reason "$DONE_SIGNAL" '{ decision: "block", reason: $reason }' -``` - -Now the terminal shows a single collapsed line: - -``` -● Ran N stop hooks (ctrl+o to expand) - ⎿ Stop hook error: TASKMASTER_DONE::abc123xyz -``` - -The agent sees the exact signal it needs to emit. The user sees almost nothing. Both get what they need from the same field. - -#### Also improved: done-signal detection - -While I was in there I also improved how the hook detects the done signal. The old version parsed the transcript file every time — opening and scanning potentially hundreds of lines of JSON on every stop attempt. - -The Claude Code hook API exposes `last_assistant_message` directly in the hook's input JSON. Checking that first is much faster: - -```bash -LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""') -if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; then - HAS_DONE_SIGNAL=true -fi - -# Fallback to transcript scan only if needed -if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then - if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then - HAS_DONE_SIGNAL=true - fi -fi -``` - -In the common case — where the agent just emitted the done signal in its last message — no transcript parsing happens at all. - -#### Separating user output from AI instructions - -The broader lesson for hook design: user-visible output and AI instructions have different lifetimes and audiences. System context (skill files, `CLAUDE.md`) is the right home for persistent instructions that shape the agent's behavior across a whole session. Hook reasons are for transient, stop-specific signals — the minimum information the agent needs right now to know what to do next. - -In this case that's: "emit `TASKMASTER_DONE::abc123` and you can stop." That's the whole message. - -The checklist still runs. The enforcement is still there. It's just not printing to your terminal anymore. - -These changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md index 98ec081..ba627ef 100644 --- a/docs/blog/2026-02-25-taskmaster-hook-cleanup.md +++ b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md @@ -2,50 +2,76 @@ **2026-02-25** -I built [taskmaster](https://github.com/micahstubbs/taskmaster) a few months ago to solve a real problem: Claude Code would sometimes stop working before actually finishing a task. The stop hook forces the agent to keep going until it emits an explicit `TASKMASTER_DONE::` signal — a parseable token that gives external tooling a deterministic completion marker. +I forked [taskmaster](https://github.com/micahstubbs/taskmaster) a few recently to stop Claude Code from quitting early. The stop [hook](https://github.com/micahstubbs/taskmaster/blob/main/check-completion.sh) fires every time the agent tries to stop and blocks it until it emits an explicit `TASKMASTER_DONE::` token — a parseable signal that confirms the agent actually finished. -It works. But the terminal output was a mess. +It works. The terminal output, though, was a way too much. -## The problem +#### The problem -Every time the hook blocked a stop, Claude Code would display the full completion checklist in the user-visible terminal output: +Every time the hook blocked a stop attempt, Claude Code dumped the full completion checklist into the terminal: ``` -● Ran 9 stop hooks (ctrl+o to expand) - ⎿ Stop hook error: TASKMASTER (1/100): Verify that all work is truly complete - before stopping. - - Before stopping, do each of these checks: - - 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete request... - 2. CHECK THE TASK LIST. Review every task. Any task not marked completed?... - 3. CHECK THE PLAN. Walk through each step... - 4. CHECK FOR ERRORS. Did any tool call, build, test, or lint fail?... - 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder code... + Ran 9 stop hooks (ctrl+o to expand) + ⎿  Stop hook error: TASKMASTER (1/100): Verify that + all work is truly complete before stopping. + + Before stopping, do each of these checks: + + 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete + request or acceptance criterion. For each one, confirm it + is fully addressed — not just started, FULLY done. If the + user explicitly changed their mind, withdrew a request, or + told you to stop or skip something, treat that item as + resolved and do NOT continue working on it. + + 2. CHECK THE TASK LIST. Review every task. Any task not + marked completed? Do it now — unless the user indicated it + is no longer wanted. + + 3. CHECK THE PLAN. Walk through each step. Any step + skipped or partially done? Finish it — unless the user + redirected or deprioritized it. + + 4. CHECK FOR ERRORS. Did any tool call, build, test, or + lint fail? Fix it. + + 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder + code, missing tests, or follow-ups noted but not acted on? + + IMPORTANT: The user's latest instructions always take + priority. If the user said to stop, move on, or skip + something, respect that — do not force completion of work + the user no longer wants. + + If after this review everything is genuinely 100% done (or + explicitly deprioritized by the user), briefly confirm + completion for each user request. Otherwise, immediately + continue working on whatever remains — do not just + describe what is left, ACTUALLY DO IT. ``` -That's a 15-line wall of text every time the hook fires. In a long session with multiple stop attempts, this pollution accumulates. The checklist is instructions *for the AI*, not the user — it doesn't need to be on screen. +Many lines, every time, accumulating across a long session. The checklist is instructions _for the AI_ — I never needed to read it. -## How Claude Code hook reasons work +#### How the `reason` field works -Claude Code stop hooks return a JSON object when they want to block: +Claude Code stop hooks return JSON when they want to block a stop: ```json { "decision": "block", "reason": "..." } ``` -The `reason` field serves two purposes simultaneously: +The `reason` field does two things at once: -1. **User-visible terminal output** — shown in the UI as a "Stop hook error" -2. **AI context** — injected back into the conversation so the agent knows why it was blocked +1. **User-visible output** — shown in the terminal as a "Stop hook error" +2. **AI context** — injected back into the conversation so the agent knows what to do next -This dual-use is the root of the problem. If you put the full instructions in `reason` so the AI has them, the user sees a wall of text. But you need the AI to know what to do. +Before, taskmaster was putting the full checklist in `reason`, to ensure that Claude got the instructions. However, this meant taskmaster was also printing the full checklist to my terminal. Every single stop attempt. -## The fix: separate instructions from signal +#### What I was missing -The key insight: the AI already has the full completion checklist in system context via `SKILL.md`. Every Claude Code skill file is loaded at session start — the agent knows what to do when blocked without being told again in the hook reason. +The Claude already has the checklist. He usually has [beads](https://github.com/Dicklesworthstone/beads_rust) too. Every Claude Code [skill file](https://github.com/micahstubbs/taskmaster/blob/main/SKILL.md) loads into system context at session start. The agent doesn't need instructions repeated in the hook reason — it just needs to know the specific token to emit. -So the hook reason only needs to contain one thing: the done signal token the agent must emit to satisfy the hook. +So I stripped the reason down to exactly that: ```bash DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" @@ -53,20 +79,20 @@ DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" jq -n --arg reason "$DONE_SIGNAL" '{ decision: "block", reason: $reason }' ``` -Now the terminal shows at most one collapsed line: +Now the terminal shows one collapsed line: ``` ● Ran N stop hooks (ctrl+o to expand) ⎿ Stop hook error: TASKMASTER_DONE::abc123xyz ``` -The agent sees the done signal it needs to emit. The user sees almost nothing. Both get what they need. +The agent sees the signal it needs. I see almost nothing. Both of us get what we need from the same field. -## Improving the done-signal detection +#### Faster signal detection too -While I was in the hook, I also upgraded how it detects the done signal. The old version parsed the transcript file — opening and scanning potentially hundreds of lines of JSON on every stop attempt. +While I was in there I also changed how the hook detects the done signal. The old version opened the transcript file and scanned potentially hundreds of lines of JSON on every stop attempt. -The newer Claude Code API exposes `last_assistant_message` directly in the hook's JSON input. Checking that first is much faster and avoids the transcript entirely in the happy path: +The Claude Code hook API passes `last_assistant_message` directly in the hook's input JSON. Checking that first skips the file read in the common case: ```bash LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""') @@ -74,7 +100,7 @@ if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; HAS_DONE_SIGNAL=true fi -# Fallback to transcript only if needed +# Only scan the transcript if the message check didn't match if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then HAS_DONE_SIGNAL=true @@ -82,12 +108,14 @@ if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then fi ``` -## The broader lesson +When the agent just emitted the done signal in its last message — the normal case — no transcript parsing happens. + +#### The lesson -When designing hooks and other automation that wraps AI agents, it helps to keep user-visible output and AI-context separate. System context (skills, CLAUDE.md) is the right place for persistent instructions. Hook reasons are for transient signals — the specific thing the agent needs right now to unblock itself. +Hook reasons and system context have different jobs. System context (skill files, `CLAUDE.md`) carries persistent instructions that shape behavior across a whole session. Hook reasons carry transient, stop-specific information — the minimum the agent needs right now. -In this case: "emit `TASKMASTER_DONE::abc123` to stop." That's it. +Here that's: "emit `TASKMASTER_DONE::abc123` and you're done." -The full completion checklist is still there, still enforced, still directing the agent's behavior. It's just not cluttering the terminal anymore. +The checklist still runs. The skill enforcement is unchanged. It just doesn't output the skill prompt to my terminal anymore. -The changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). +These changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). From 566d7804992c685bced2e2f0c972657bdbe86392 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 05:38:10 -0800 Subject: [PATCH 09/41] add docs link at the end --- docs/blog/2026-02-25-taskmaster-hook-cleanup.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md index ba627ef..1f138df 100644 --- a/docs/blog/2026-02-25-taskmaster-hook-cleanup.md +++ b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md @@ -119,3 +119,5 @@ Here that's: "emit `TASKMASTER_DONE::abc123` and you're done." The checklist still runs. The skill enforcement is unchanged. It just doesn't output the skill prompt to my terminal anymore. These changes shipped as [v2.3.0](https://github.com/micahstubbs/taskmaster/releases/tag/v2.3.0). + +Read more about how stop decision control and the `reason` field works in the [Claude Code Hooks docs](https://code.claude.com/docs/en/hooks#stop-decision-control). From bdf29fb0d0c5fc11c13cba80e612f31f4973fa43 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 05:44:51 -0800 Subject: [PATCH 10/41] further edits. the agent --> Claude; it --> he --- .../2026-02-25-taskmaster-hook-cleanup.md | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/docs/blog/2026-02-25-taskmaster-hook-cleanup.md b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md index 1f138df..18dea51 100644 --- a/docs/blog/2026-02-25-taskmaster-hook-cleanup.md +++ b/docs/blog/2026-02-25-taskmaster-hook-cleanup.md @@ -2,7 +2,7 @@ **2026-02-25** -I forked [taskmaster](https://github.com/micahstubbs/taskmaster) a few recently to stop Claude Code from quitting early. The stop [hook](https://github.com/micahstubbs/taskmaster/blob/main/check-completion.sh) fires every time the agent tries to stop and blocks it until it emits an explicit `TASKMASTER_DONE::` token — a parseable signal that confirms the agent actually finished. +I forked [taskmaster](https://github.com/micahstubbs/taskmaster) a few recently to stop Claude from quitting early when working in a Claude Code session. The stop [hook](https://github.com/micahstubbs/taskmaster/blob/main/check-completion.sh) fires every time Claude tries to stop and blocks it until he emits an explicit `TASKMASTER_DONE::` token — a parseable signal that confirms Claude is actually finished. It works. The terminal output, though, was a way too much. @@ -17,37 +17,19 @@ Every time the hook blocked a stop attempt, Claude Code dumped the full completi Before stopping, do each of these checks: - 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete - request or acceptance criterion. For each one, confirm it - is fully addressed — not just started, FULLY done. If the - user explicitly changed their mind, withdrew a request, or - told you to stop or skip something, treat that item as - resolved and do NOT continue working on it. - - 2. CHECK THE TASK LIST. Review every task. Any task not - marked completed? Do it now — unless the user indicated it - is no longer wanted. - - 3. CHECK THE PLAN. Walk through each step. Any step - skipped or partially done? Finish it — unless the user - redirected or deprioritized it. - - 4. CHECK FOR ERRORS. Did any tool call, build, test, or - lint fail? Fix it. - - 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder - code, missing tests, or follow-ups noted but not acted on? - - IMPORTANT: The user's latest instructions always take - priority. If the user said to stop, move on, or skip - something, respect that — do not force completion of work - the user no longer wants. - - If after this review everything is genuinely 100% done (or - explicitly deprioritized by the user), briefly confirm - completion for each user request. Otherwise, immediately - continue working on whatever remains — do not just - describe what is left, ACTUALLY DO IT. + 1. RE-READ THE ORIGINAL USER MESSAGE(S). List every discrete request or acceptance criterion. For each one, confirm it is fully addressed — not just started, FULLY done. If the user explicitly changed their mind, withdrew a request, or told you to stop or skip something, treat that item as resolved and do NOT continue working on it. + + 2. CHECK THE TASK LIST. Review every task. Any task not marked completed? Do it now — unless the user indicated it is no longer wanted. + + 3. CHECK THE PLAN. Walk through each step. Any step skipped or partially done? Finish it — unless the user redirected or deprioritized it. + + 4. CHECK FOR ERRORS. Did any tool call, build, test, or lint fail? Fix it. + + 5. CHECK FOR LOOSE ENDS. Any TODO comments, placeholder code, missing tests, or follow-ups noted but not acted on? + + IMPORTANT: The user's latest instructions always take priority. If the user said to stop, move on, or skip something, respect that — do not force completion of work the user no longer wants. + + If after this review everything is genuinely 100% done (or explicitly deprioritized by the user), briefly confirm completion for each user request. Otherwise, immediately continue working on whatever remains — do not just describe what is left, ACTUALLY DO IT. ``` Many lines, every time, accumulating across a long session. The checklist is instructions _for the AI_ — I never needed to read it. @@ -63,13 +45,13 @@ Claude Code stop hooks return JSON when they want to block a stop: The `reason` field does two things at once: 1. **User-visible output** — shown in the terminal as a "Stop hook error" -2. **AI context** — injected back into the conversation so the agent knows what to do next +2. **AI context** — injected back into the conversation so that Claude knows what to do next Before, taskmaster was putting the full checklist in `reason`, to ensure that Claude got the instructions. However, this meant taskmaster was also printing the full checklist to my terminal. Every single stop attempt. #### What I was missing -The Claude already has the checklist. He usually has [beads](https://github.com/Dicklesworthstone/beads_rust) too. Every Claude Code [skill file](https://github.com/micahstubbs/taskmaster/blob/main/SKILL.md) loads into system context at session start. The agent doesn't need instructions repeated in the hook reason — it just needs to know the specific token to emit. +Claude already has the checklist from the taskmaster [skill file](https://github.com/micahstubbs/taskmaster/blob/main/SKILL.md). Every Claude Code `SKILL.md` file loads into system context at session start. Claude doesn't need instructions repeated in the hook reason — it just needs to know the specific token to emit. So I stripped the reason down to exactly that: @@ -86,7 +68,7 @@ Now the terminal shows one collapsed line: ⎿ Stop hook error: TASKMASTER_DONE::abc123xyz ``` -The agent sees the signal it needs. I see almost nothing. Both of us get what we need from the same field. +Claude sees the signal he needs. I see almost nothing. Both of us get what we need from the same field. #### Faster signal detection too @@ -108,11 +90,11 @@ if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then fi ``` -When the agent just emitted the done signal in its last message — the normal case — no transcript parsing happens. +When Claude just emitted the done signal in his last message — the normal case — no transcript parsing happens. #### The lesson -Hook reasons and system context have different jobs. System context (skill files, `CLAUDE.md`) carries persistent instructions that shape behavior across a whole session. Hook reasons carry transient, stop-specific information — the minimum the agent needs right now. +Hook reasons and system context have different jobs. System context (skill files, `CLAUDE.md`) carries persistent instructions that shape behavior across a whole session. Hook reasons carry transient, stop-specific information — the minimum Claude needs right now. Here that's: "emit `TASKMASTER_DONE::abc123` and you're done." From 9997f36f1470b64750c32ad27adc6e8032165066 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 06:18:23 -0800 Subject: [PATCH 11/41] docs(lessons): hook reason dual-use, last_assistant_message detection --- LESSONS.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 LESSONS.md diff --git a/LESSONS.md b/LESSONS.md new file mode 100644 index 0000000..abb2a7d --- /dev/null +++ b/LESSONS.md @@ -0,0 +1,64 @@ +# Lessons Learned + +Append-only log of debugging insights and non-obvious solutions. + +--- + +## 2026-02-25T14:00 - Claude Code hook `reason` is dual-use: user-visible AND AI context + +**Problem**: The taskmaster stop hook embedded a full 5-item completion checklist in the `reason` field of `{ "decision": "block", "reason": "..." }`. Every stop attempt printed the entire checklist to the user's terminal. + +**Root Cause**: Claude Code's stop hook `reason` field serves two purposes simultaneously — it is displayed to the user in the terminal UI ("Stop hook error: ...") AND injected back into the AI conversation as context. Putting verbose instructions in `reason` to inform the AI caused them to also appear as user-visible output. + +**Lesson**: The `reason` field is not a private AI channel. Anything in `reason` is shown to the human. Persistent AI instructions belong in SKILL.md (system context loaded at session start), not in transient hook `reason` values. The `reason` should carry only the minimum transient signal the agent needs right now. + +**Code Issue**: +```bash +# Before (verbose — full checklist in reason, shown to user) +REASON="${LABEL}: ${PREAMBLE} + +Before stopping, do each of these checks: +1. RE-READ THE ORIGINAL USER MESSAGE(S)... +2. CHECK THE TASK LIST... +[etc]" +jq -n --arg reason "$REASON" '{ decision: "block", reason: $reason }' + +# After (minimal — only the done signal; checklist lives in SKILL.md) +DONE_SIGNAL="${DONE_PREFIX}::${SESSION_ID}" +jq -n --arg reason "$DONE_SIGNAL" '{ decision: "block", reason: $reason }' +``` + +**Solution**: Strip the checklist from `reason`. Put it only in SKILL.md, which is always loaded as system context. The `reason` now contains only the done signal token the agent must emit. + +**Prevention**: When designing Claude Code hooks, ask: "Does this text need to be in the reason, or is it already in system context?" If it's in a skill file, it doesn't belong in `reason`. + +--- + +## 2026-02-25T14:30 - `last_assistant_message` is faster than transcript scanning for done-signal detection + +**Problem**: The hook was opening and scanning potentially large transcript JSON files on every stop attempt to detect whether the agent had emitted the done signal. + +**Root Cause**: The hook relied exclusively on transcript-file parsing, which requires disk I/O and JSON scanning on every invocation. + +**Lesson**: The Claude Code hook input JSON exposes `last_assistant_message` directly. Checking that field is O(1) and avoids the file read in the common case (agent just emitted the signal in its latest message). + +**Code Issue**: +```bash +# Before (always scans transcript file) +if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then + HAS_DONE_SIGNAL=true +fi + +# After (fast path via last_assistant_message, transcript as fallback) +LAST_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // ""') +if [ -n "$LAST_MSG" ] && echo "$LAST_MSG" | grep -Fq "$DONE_SIGNAL" 2>/dev/null; then + HAS_DONE_SIGNAL=true +fi +if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then + if tail -400 "$TRANSCRIPT" 2>/dev/null | grep -Fq "$DONE_SIGNAL"; then + HAS_DONE_SIGNAL=true + fi +fi +``` + +**Prevention**: Always check `last_assistant_message` before falling back to transcript file parsing in stop hooks. From c600b868018da2a9d9d02c1cf1165ec185ca73de Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 25 Feb 2026 06:24:29 -0800 Subject: [PATCH 12/41] fix: expand tilde in transcript_path; add upstream review docs - Apply tilde expansion fix from upstream blader/taskmaster 6598e99d to both check-completion.sh and hooks/check-completion.sh. Bash does not expand ~ inside double-quoted strings, so transcript_path values like ~/.claude/... would fail [ -f ] checks. - Add docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md with per-commit cherry-pick/rewrite/ignore decisions for all 13 upstream commits --- .../2026-02-25-blader-taskmaster-main.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md diff --git a/docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md b/docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md new file mode 100644 index 0000000..504c305 --- /dev/null +++ b/docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md @@ -0,0 +1,192 @@ +# Upstream Review: blader/taskmaster main + +**Date:** 2026-02-25 +**Compare URL:** https://github.com/micahstubbs/taskmaster/compare/main...blader:taskmaster:main +**Upstream:** blader/taskmaster@main +**Our fork:** micahstubbs/taskmaster@main +**Status:** diverged — 13 commits ahead in upstream + +--- + +## Context + +Our fork focuses on Claude Code stop hook behavior. The upstream (blader) has moved to +support OpenAI Codex TUI as a first-class target alongside Claude Code, using external +session-log monitoring and tmux/expect PTY injection instead of native hook registration. + +This architectural divergence drives most of the ignore decisions below. + +--- + +## Commit Decisions + +### cbd9443e — chore: sync local skill updates +**Decision: IGNORE** + +Adds the Codex integration layer: +- `hooks/check-completion-codex.sh` (237 lines, Codex monitor) +- `hooks/inject-continue-codex-tmux.sh` (307 lines, tmux transport) +- `hooks/run-codex-expect-bridge.exp` (91 lines, expect PTY bridge) +- `run-taskmaster-codex.sh` (364 lines, Codex session launcher) + +Also rewrites README, SKILL.md, docs/SPEC.md, install.sh, and uninstall.sh from a Codex-first perspective. + +Codex support is out of scope for this fork. Our focus is the Claude Code stop hook. Adding tmux/expect infrastructure would significantly increase complexity with no benefit to Claude Code users. + +--- + +### 755d165f — chore: sync local skill updates +**Decision: IGNORE** + +Refinements to the Codex layer introduced in cbd9443e: renames +`inject-continue-codex-tmux.sh` → `inject-continue-codex.sh`, simplifies +`run-taskmaster-codex.sh`, and trims docs. + +Depends on Codex infrastructure we're not adopting. + +--- + +### 88ffd335 — feat: support codex+claude auto install and cleanup docs +**Decision: IGNORE** + +Rewrites `install.sh` to auto-detect and install for both Codex (`~/.codex`) and +Claude (`~/.claude`). The new installer is 215 lines vs our 83 lines. While +auto-detection is a nice concept, the upstream now defaults to the Codex path +and the Claude path is a secondary target. Our simpler installer is better +suited to this fork's Claude-only focus. + +--- + +### 452417af — docs: rewrite README for clarity +**Decision: IGNORE** + +README is rewritten to be Codex-first, describing the Codex session-log +monitoring approach. Our README is accurate and Claude-focused. Nothing to port. + +--- + +### 4e5075fd — docs: add taskmaster philosophy and compliance rationale +**Decision: IGNORE** + +Adds 35 lines to README covering Taskmaster's philosophy. The content is already +present in our `SKILL.md` (the 6-item checklist including HONESTY CHECK). Our +approach of keeping the compliance text in SKILL.md (always loaded as system +context) is architecturally correct for Claude Code — no need to duplicate it +in the README. + +--- + +### 547bfa74 — refactor: remove monitor-only mode +**Decision: N/A (IGNORE)** + +Removes `hooks/check-completion-codex.sh` (235 lines) which was added in +cbd9443e and which we never adopted. Also trims SPEC.md. No action needed. + +--- + +### ca471bd8 — refactor: unify codex and claude compliance prompt +**Decision: IGNORE** + +Extracts the compliance prompt into `taskmaster-compliance-prompt.sh`, which +`hooks/check-completion.sh` now sources. This allows the same prompt to be +shared between Claude and Codex hooks. + +Our architecture keeps the compliance checklist in `SKILL.md`, which Claude Code +loads as system context on every turn — no shell file required. The upstream's +shell-based approach is a workaround for the lack of a native context mechanism +in Codex. We don't have this constraint. + +*Note:* The compliance prompt text in the new file is essentially identical to +what we already have in `SKILL.md`. No content to cherry-pick. + +--- + +### c04eeb18 — fix: restore long canonical compliance prompt +**Decision: IGNORE** + +Restores the longer version of `taskmaster-compliance-prompt.sh`. Depends on +the file introduced in ca471bd8, which we're not adopting. + +--- + +### c2475d9c — fix: default QUIET=1 in inject-continue-codex +**Decision: IGNORE** + +One-line fix in `hooks/inject-continue-codex.sh` — a Codex-specific file we +don't have. + +--- + +### 6814a3f5 — fix: symlink taskmaster-compliance-prompt.sh into hooks dir +**Decision: IGNORE** + +Adds one line to `install.sh` to symlink `taskmaster-compliance-prompt.sh` +into the hooks directory. Depends on `taskmaster-compliance-prompt.sh` which +we're not adopting. + +--- + +### 6598e99d — fix: expand tilde in transcript_path for done signal detection +**Decision: APPLY (rewrite to match our structure)** + +**Bug:** Claude Code passes `transcript_path` with a leading `~` (e.g., +`~/.claude/projects/.../session.jsonl`). Bash does not expand `~` inside +double-quoted strings, so `[ -f "$TRANSCRIPT" ]` always fails. The transcript +fallback never fires, and error detection (via `tail -40`) also silently fails. + +**Fix:** Add `TRANSCRIPT="${TRANSCRIPT/#\~/$HOME}"` immediately after reading +`transcript_path` from the input JSON. + +**Upstream patch (hooks/check-completion.sh):** +```diff + TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path') ++# Expand leading ~ to $HOME (tilde not expanded inside quotes by bash) ++TRANSCRIPT="${TRANSCRIPT/#\~/$HOME}" +``` + +**Action:** Apply to both `check-completion.sh` (root) and `hooks/check-completion.sh`. + +*See commit 6598e99d in blader/taskmaster for original.* + +--- + +### 71ff69c4 — fix: check last_assistant_message for done signal before transcript +**Decision: ALREADY IMPLEMENTED** + +This fix checks `last_assistant_message` from the hook input JSON before +falling back to transcript search. The transcript file may not be flushed yet +when the Stop hook fires. + +**Status:** Our v2.3.0 release (commit 1ae2daf, 2026-02-23) independently +implemented this same fix. Both `check-completion.sh` files already check +`last_assistant_message` first. No action needed. + +--- + +### 77c71bbf — fix: honor QUIET for transport banner +**Decision: IGNORE** + +Fixes a QUIET flag check in `run-taskmaster-codex.sh` — a Codex-specific +wrapper we don't have. + +--- + +## Summary + +| Commit | Decision | Reason | +|--------|----------|--------| +| cbd9443e | IGNORE | Codex integration, out of scope | +| 755d165f | IGNORE | Codex refinements, depends on above | +| 88ffd335 | IGNORE | Codex+Claude installer, Codex-first design | +| 452417af | IGNORE | Codex-centric README | +| 4e5075fd | IGNORE | Already in our SKILL.md | +| 547bfa74 | N/A | Removes file we never added | +| ca471bd8 | IGNORE | Shell-based compliance prompt, workaround we don't need | +| c04eeb18 | IGNORE | Depends on ca471bd8 | +| c2475d9c | IGNORE | Codex-only file | +| 6814a3f5 | IGNORE | Depends on ca471bd8 | +| **6598e99d** | **APPLY** | Tilde expansion bug in transcript_path | +| 71ff69c4 | ALREADY DONE | Implemented in our v2.3.0 | +| 77c71bbf | IGNORE | Codex-only file | + +**Net actions: 1 apply, 1 already done, 11 ignored** From cfc01ec05ca3e2f6f64ab219796f5d57c7d98f51 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Mon, 27 Apr 2026 23:57:23 -0700 Subject: [PATCH 13/41] docs: add session summary (fix-expand-tilde-in-transcript) --- ...7-235723-fix-expand-tilde-in-transcript.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/session-summaries/2026-04-27-235723-fix-expand-tilde-in-transcript.md diff --git a/docs/session-summaries/2026-04-27-235723-fix-expand-tilde-in-transcript.md b/docs/session-summaries/2026-04-27-235723-fix-expand-tilde-in-transcript.md new file mode 100644 index 0000000..87cd0af --- /dev/null +++ b/docs/session-summaries/2026-04-27-235723-fix-expand-tilde-in-transcript.md @@ -0,0 +1,42 @@ +# Session Summary + +**Date:** 2026-04-27 +**Time:** 23:57 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 12 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `c600b86` - fix: expand tilde in transcript_path; add upstream review docs +- `9997f36` - docs(lessons): hook reason dual-use, last_assistant_message detection +- `bdf29fb` - further edits. the agent --> Claude; it --> he +- `566d780` - add docs link at the end +- `daeed44` - manual blog post edits +- `9a17abf` - Add blog post: taskmaster hook cleanup (3 versions) +- `8eb006c` - release v2.3.0: minimal hook output, TASKMASTER_DONE signal detection +- `6a4bd8a` - docs: add session summary (hide-verbose-checklist-from-us) +- `689d830` - docs: add session summary (docs-add-session-summary-make-) +- `a7542fe` - docs: add session summary (make-installsh-posix-portable-) + +## Key Changes + +### Files Modified +- `CHANGELOG.md` +- `LESSONS.md` +- `docs/blog/2026-02-25-taskmaster-hook-cleanup.md` +- `docs/session-summaries/2026-02-19-142857-make-installsh-posix-portable-.md` +- `docs/session-summaries/2026-02-23-145341-docs-add-session-summary-make-.md` +- `docs/session-summaries/2026-02-25-043407-hide-verbose-checklist-from-us.md` +- `docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md` + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From ad5729d2c49dd9ca88cf984be7766467f556bfcf Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 00:01:26 -0700 Subject: [PATCH 14/41] docs: add session summary (docs-add-session-summary-fix-e) --- ...8-000126-docs-add-session-summary-fix-e.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/session-summaries/2026-04-28-000126-docs-add-session-summary-fix-e.md diff --git a/docs/session-summaries/2026-04-28-000126-docs-add-session-summary-fix-e.md b/docs/session-summaries/2026-04-28-000126-docs-add-session-summary-fix-e.md new file mode 100644 index 0000000..b04665d --- /dev/null +++ b/docs/session-summaries/2026-04-28-000126-docs-add-session-summary-fix-e.md @@ -0,0 +1,42 @@ +# Session Summary + +**Date:** 2026-04-28 +**Time:** 00:01 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 13 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `cfc01ec` - docs: add session summary (fix-expand-tilde-in-transcript) +- `c600b86` - fix: expand tilde in transcript_path; add upstream review docs +- `9997f36` - docs(lessons): hook reason dual-use, last_assistant_message detection +- `bdf29fb` - further edits. the agent --> Claude; it --> he +- `566d780` - add docs link at the end +- `daeed44` - manual blog post edits +- `9a17abf` - Add blog post: taskmaster hook cleanup (3 versions) +- `8eb006c` - release v2.3.0: minimal hook output, TASKMASTER_DONE signal detection +- `6a4bd8a` - docs: add session summary (hide-verbose-checklist-from-us) +- `689d830` - docs: add session summary (docs-add-session-summary-make-) + +## Key Changes + +### Files Modified +- `CHANGELOG.md` +- `LESSONS.md` +- `docs/blog/2026-02-25-taskmaster-hook-cleanup.md` +- `docs/session-summaries/2026-02-23-145341-docs-add-session-summary-make-.md` +- `docs/session-summaries/2026-02-25-043407-hide-verbose-checklist-from-us.md` +- `docs/session-summaries/2026-04-27-235723-fix-expand-tilde-in-transcript.md` +- `docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md` + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From 7c364b4b7be7838b0f08d66397a8dcf88a125496 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 00:07:08 -0700 Subject: [PATCH 15/41] docs: add fork-network review of blader/taskmaster Survey of all 32 forks. Only mickn/taskmaster has substantial original engineering (native Codex hooks + semantic completion verifier, v5.0.0). Documents recommended adoption tiers and open questions before porting. --- .../blader-taskmaster-forks.md | 314 ++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 docs/upstream-reviews/blader-taskmaster-forks.md diff --git a/docs/upstream-reviews/blader-taskmaster-forks.md b/docs/upstream-reviews/blader-taskmaster-forks.md new file mode 100644 index 0000000..2f5d90a --- /dev/null +++ b/docs/upstream-reviews/blader-taskmaster-forks.md @@ -0,0 +1,314 @@ +# Fork Network Review: blader/taskmaster + +**Date**: 2026-04-28 +**Upstream baseline**: `blader/taskmaster@a1f3feb` ("chore: sync local skill updates", 2026-03-11) +**Our fork**: `micahstubbs/taskmaster` at v4.2.0 (codex wrapper + expect PTY bridge architecture) +**Scope**: All 32 forks of `blader/taskmaster`, default and feature branches. + +## Methodology + +For every fork in the network, compared default branch to upstream `main` via +`gh api repos/blader/taskmaster/compare/blader:main...:`. Then +enumerated all non-`main` branches and compared those too. Ignored forks that +were even with or only behind upstream. + +## Fork Activity Summary + +| Bucket | Count | Notes | +|---|---|---| +| Even with upstream | 6 | Pure forks, no original commits | +| Behind upstream only | 21 | Stale forks, no rebase needed | +| **Ahead of upstream** | **5** | Worth examining (1 fork, multiple branches) | + +The five branches with original commits (excluding our own fork): + +| Fork / branch | Ahead | Behind | Theme | +|---|---|---|---| +| `mickn:main` | 5 | 0 | **Native Codex hooks + semantic verifier** (major rewrite, v5.0.0) | +| `mickn:feat/codex-native-hooks` | 3 | 0 | Subset of `mickn:main` | +| `gjlondon:fix/stop-hook-feedback-loop` | 1 | 18 | **Single-fire-by-design** philosophy | +| `levi-openclaw:claude/openclaw-agent-skill-7JoVl` | 1 | 18 | OpenClaw platform port (not adoptable) | +| `Semenka:claude/create-claude-guide-NvneU` | 1 | 18 | Auto-generated CLAUDE.md (not adoptable) | + +Effectively, **mickn** is the only fork with substantial original engineering; +**gjlondon** contributes one focused architectural insight. + +--- + +## mickn/taskmaster — Native Hooks + Semantic Verifier (v5.0.0) + +5-commit chain that takes the project from "Codex via PTY wrapper" to "Codex via +native hooks." Files removed: + +- `hooks/inject-continue-codex.sh` (414 LOC queue emitter) +- `hooks/run-codex-expect-bridge.exp` (84 LOC expect bridge) +- `run-taskmaster-codex.sh` (215 LOC wrapper) +- All tests for the above + +Files added: + +- `hooks/taskmaster-session-start.sh` (27 LOC) +- `hooks/taskmaster-user-prompt-submit.sh` (107 LOC) +- `hooks/taskmaster-stop.sh` (356 LOC) +- `taskmaster-completion-verifier.py` (311 LOC) +- `taskmaster-state.sh` (15 LOC) +- New test suite + +Premise: the OpenAI Codex CLI now supports a Claude-Code-style hooks model +(`~/.codex/hooks.json` with `SessionStart`, `UserPromptSubmit`, `Stop` events). +This obviates the entire PTY-injection architecture our fork currently relies +on. **This is the most consequential change in the fork network and worth +verifying independently** (see "Open Questions" below). + +### Patterns worth adopting + +#### A1. Per-turn user prompt capture via UserPromptSubmit hook (HIGH VALUE) + +`hooks/taskmaster-user-prompt-submit.sh` writes the latest external user prompt +to `~/.codex/taskmaster/state/.json`, with explicit filtering of: + +1. **Hook-injected reprompts** — strings starting with `...` +3. **AGENTS.md preludes** — `# AGENTS.md instructions for ...` + +Why it matters: solves the "is this a real user goal or just a hook re-prompt?" +problem cleanly. The current fork has no equivalent — it relies on transcript +parsing inside the stop hook, which is brittle and can re-anchor onto its own +output. + +**Adopt this pattern** even if we keep the wrapper architecture — we can write +to a state file from the wrapper layer the same way. + +#### A2. Semantic completion verifier (HIGH VALUE, MEDIUM RISK) + +`taskmaster-completion-verifier.py` calls an OpenAI model +(`TASKMASTER_COMPLETION_MODEL`, default `gpt-5.4-mini`) with: + +- the captured user goal (from A1) +- `last_assistant_message` +- a clipped transcript excerpt (`TASKMASTER_COMPLETION_MAX_CONTEXT_CHARS`, + default 20000) + +Returns JSON `{complete: bool, reason: str, next_action: str}`. If +`complete=false`, the stop hook blocks with the verifier's `reason` and +`next_action` injected into the block reason. + +Notable engineering details: + +- **Secret redaction** before sending to the model (regexes for + `Authorization: bearer`, `api_key=`, `sk-...`, `lin_api_...`, `phx_...`, + `xox[baprs]-...`) +- **Loads `.env`** for `OPENAI_API_KEY` if not already in env +- **Pluggable**: `TASKMASTER_COMPLETION_VERIFIER_COMMAND` lets you swap in any + command that reads the same JSON stdin and returns the same JSON shape — so + users without an OpenAI key can wire in a local model +- **Fail-open on disable**: `TASKMASTER_COMPLETION_VERIFY=0|false|off|no` + reverts to the legacy `TASKMASTER_DONE::` token flow + +Why it matters: replaces "agent self-reports done" with "second-agent verifies +done." The legacy token approach trusts the agent's own assessment; the +verifier doesn't. For long-running autonomous work this is the difference +between "agent declared victory after 2/5 sub-tasks" and a hard machine check. + +**Adopt with care.** Two concrete concerns: (1) every stop attempt now costs +an OpenAI API call — at 30+ stop attempts per long session and gpt-5.4-mini +input pricing, this adds up; (2) `gpt-5.4-mini` is referenced as a default — +verify availability/pricing before defaulting; consider `claude-haiku-4-5` as +the Anthropic-side default with `OPENAI_API_KEY` as an alternative. + +#### A3. Optional repo-local verifier command (HIGH VALUE, LOW RISK) + +`TASKMASTER_VERIFY_COMMAND`: stop is blocked until the named shell command +exits 0. Output is captured (capped at `TASKMASTER_VERIFY_MAX_OUTPUT`, default +4000 bytes) and echoed back to the agent. + +Use cases: `cargo test`, `pnpm typecheck`, `make ci`, custom smoke scripts. +Pure win — pairs with the semantic verifier (or replaces it for repos with a +strong test suite). + +**Adopt.** Cheap to add, no external dependencies, immediately useful. + +#### A4. JSON state-file architecture (MEDIUM VALUE) + +`taskmaster-state.sh` exposes `taskmaster_turn_state_path "$session_id"` that +returns `$TASKMASTER_STATE_DIR/.json` (default +`~/.codex/taskmaster/state/`). All hooks read/write through this single API. + +Cleaner than our current scatter (counter file in `$TMPDIR/taskmaster/`, +queue files in another directory, no shared schema). + +**Adopt the pattern** even if the storage location stays separate per +platform. + +#### A5. `safe_copy` helper that no-ops on same-path source/dest (LOW VALUE) + +`install.sh:30-46` resolves `cd -P` absolute paths for both source and +destination and skips copy when they match. Prevents `cp: 'X' and 'X' are the +same file` errors when running install from inside the install target. Our +install.sh already has this — confirmed it's already in HEAD. + +**Already adopted.** + +### Patterns to consider but not adopt as-is + +#### B1. Wholesale replacement of the PTY wrapper + +`mickn` deletes the wrapper, expect bridge, and queue emitter outright. **Do +not adopt verbatim** without first verifying that: + +1. Codex CLI actually exposes `SessionStart`, `UserPromptSubmit`, and `Stop` + hooks in the version the user is on (`codex --version`) +2. The native `Stop` hook supports `decision: "block"` continuation in the + same way Claude Code does +3. The `last_assistant_message` field is populated by Codex on stop events + +If all three hold, the wrapper is dead weight and we should follow `mickn`. If +any are uncertain, keep both paths and gate on `command -v codex && codex +--help | grep -q hooks` or similar. + +#### B2. Removed test files + +`mickn` removes the wrapper test suite (`tests/inject-continue-codex.test.sh`, +`tests/run-codex-expect-bridge.test.sh`, `tests/run-taskmaster-codex.test.sh`). +If we decide to keep the wrapper as a fallback, keep the tests. + +--- + +## gjlondon/taskmaster — Single-fire by design + +One commit (`30ec9bd` "Fix stop hook feedback loop"). Diagnoses a real bug in +the upstream-style transcript-grep approach: the hook's own checklist text +(containing "status: in_progress") gets written into the transcript, which +then matches the hook's own grep on the next fire — guaranteeing +`HAS_INCOMPLETE_SIGNALS=true` forever. Infinite loop. + +Fix: make `stop_hook_active=true` an unconditional early exit before any +transcript analysis. Hook becomes single-fire — fires once with the checklist +prompt, allows stop on the next attempt regardless of transcript contents. + +### Pattern: rethink the "repeat until token" philosophy (LOW VALUE for our fork) + +Our fork already sidesteps this bug in a different way — we use +`last_assistant_message` for primary detection and only fall back to +transcript parsing for an explicit `TASKMASTER_DONE::` token (not +generic "in_progress" matches). The contamination problem doesn't apply. + +But the underlying philosophy question is worth a beat: + +- **gjlondon's claim**: "If the agent saw the checklist and still tries to + stop, either the work is done or re-firing won't help." +- **Our claim**: "Repeat until the agent emits an explicit done signal, + because some agents will try to bail before reading the prompt fully." + +Empirically, our position is correct for adversarial-stop cases, but +gjlondon's is correct in 95% of real sessions. The cost of being wrong on our +side is one extra reprompt cycle; the cost of being wrong on gjlondon's side +is a session that stops with work undone. + +**Do not adopt.** Keep the repeat-until-token model. But consider documenting +the design tradeoff in `docs/SPEC.md` so it doesn't get re-litigated. + +--- + +## levi-openclaw — Not adoptable + +Single commit "Rework Taskmaster as an OpenClaw agent skill" — wholesale port +to a different platform (`~/.openclaw/` paths, OpenClaw skill frontmatter, +`scripts/` subfolder convention). Has zero overlap with what we're trying to +do. Skip. + +## Semenka — Not adoptable + +Single commit adding a 95-line `CLAUDE.md` that's a generic Claude-Code +project guide for the upstream repo (auto-generated by the +`claude/create-claude-guide-NvneU` workflow). No taskmaster-specific +engineering content. Skip. + +--- + +## Recommended adoption plan + +In order of value/effort ratio: + +### Tier 1 — adopt now (low risk, high value) + +1. **`TASKMASTER_VERIFY_COMMAND`** (A3 above). Pure config addition. ~30 LOC + to wire into the existing `check-completion.sh` and Codex stop path. +2. **JSON state-file layout** (A4). Replace the bare counter file in + `$TMPDIR/taskmaster/` with a JSON state file under + `$TASKMASTER_STATE_DIR/.json` that holds counter, last-known + user prompt, and any future fields. Backward-compatible if the migration + reads the legacy counter file once and discards it. +3. **Hook-internal-prompt detection** in user-facing hooks (subset of A1). + Even without a full UserPromptSubmit hook, we can teach the wrapper layer + to recognize and not reprocess our own injected reprompts. + +### Tier 2 — adopt after upstream-reality check + +4. **Native Codex hooks path** (B1). Conditional on verifying that + `codex` actually supports the three hook types `mickn` assumes. If yes, + add a parallel native-hooks code path and let `install.sh` choose between + wrapper and native at install time based on `codex` capability detection. + Don't delete the PTY wrapper yet — keep as fallback for older Codex + installs. +5. **UserPromptSubmit hook for goal capture** (A1). Only useful with a native + hooks path — depends on (4). + +### Tier 3 — adopt with explicit knobs + +6. **Semantic completion verifier** (A2). High-value but introduces an LLM + dependency and per-stop API cost. Recommended shape for our fork: + - Default OFF (`TASKMASTER_COMPLETION_VERIFY=0`) — opt-in via env + - Default model `claude-haiku-4-5` (cheaper, lower latency, keeps us on + Anthropic infra) when `ANTHROPIC_API_KEY` is set; fall back to + OpenAI `gpt-5.4-mini` when only `OPENAI_API_KEY` is present + - Port the secret-redaction regex set verbatim — that's a free correctness + improvement + - Port `TASKMASTER_COMPLETION_VERIFIER_COMMAND` pluggable interface so + local-model users can wire in `llama-server` or similar + +### Not adopting + +7. Single-fire philosophy (gjlondon) — incompatible with our explicit-token + contract. +8. PTY-wrapper deletion (B1 verbatim) — premature until native hooks + verified. +9. OpenClaw port (levi-openclaw) — different platform. + +--- + +## Open questions for follow-up + +1. **Does `codex` actually support native `SessionStart`, `UserPromptSubmit`, + and `Stop` hooks?** Mickn's install.sh writes to `~/.codex/hooks.json`, + which implies yes, but we should verify on the version our users are + pinned to. If not, his entire architecture is conditional on a future + Codex release. + +2. **Is `gpt-5.4-mini` the right default verifier model?** Mickn picked it + without comment. We should benchmark cost-per-stop and verifier accuracy + against `claude-haiku-4-5` before defaulting. + +3. **Should the verifier short-circuit on transcript size?** A 20k-char + transcript at every stop attempt × 30 stops × dozens of users is real + tokens. Worth a "skip verifier if transcript hasn't changed since last + verifier call" cache. + +## Reproducing this review + +```bash +# enumerate forks ahead of upstream +for fork in $(gh api repos/blader/taskmaster/forks --paginate -q '.[].full_name'); do + ahead=$(gh api "repos/blader/taskmaster/compare/blader:main...${fork#*/}:main" \ + -q '.ahead_by' 2>/dev/null || echo 0) + [ "$ahead" -gt 0 ] && echo "$fork ahead=$ahead" +done + +# for each interesting fork, also check non-main branches +for fork in ; do + gh api "repos/${fork}/taskmaster/branches" -q '.[].name' \ + | grep -v '^main$' +done +``` From bd4877010db611f58a29ed85bcd2fd1006ce4364 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 00:27:13 -0700 Subject: [PATCH 16/41] docs: add design for fork-pattern adoption (T1-T3) Detailed design covering all three tiers from docs/upstream-reviews/blader-taskmaster-forks.md: - T1 (adopt now): TASKMASTER_VERIFY_COMMAND shell-verifier gate, JSON state-file layout with legacy-counter migration, tagged hook-internal-prompt detection - T2 (verify first): Codex native hooks capability probe, then native SessionStart/UserPromptSubmit/Stop hooks with wrapper as fallback - T3 (opt-in): semantic completion verifier with Anthropic-first provider auto-detection, secret redaction, caching, fail-open Each tier has env vars, file lists, JSON schemas, migration paths, test plans, and risk tables. Phased rollout: T1 unblocks T2.0 unblocks T2.1+T2.2 unblocks T3. --- ...2026-04-28-072245-fork-pattern-adoption.md | 852 ++++++++++++++++++ 1 file changed, 852 insertions(+) create mode 100644 docs/designs/2026-04-28-072245-fork-pattern-adoption.md diff --git a/docs/designs/2026-04-28-072245-fork-pattern-adoption.md b/docs/designs/2026-04-28-072245-fork-pattern-adoption.md new file mode 100644 index 0000000..84e7db3 --- /dev/null +++ b/docs/designs/2026-04-28-072245-fork-pattern-adoption.md @@ -0,0 +1,852 @@ +# Design: Fork-Pattern Adoption (T1–T3) + +**Date**: 2026-04-28 +**Source**: `docs/upstream-reviews/blader-taskmaster-forks.md` +**Status**: Draft for review +**Affected version**: targets v4.3.0 (T1), v4.4.0 (T2 conditional), v4.5.0 (T3 opt-in) + +This doc turns the three adoption tiers from the fork review into concrete +designs: file-by-file, env var by env var, with JSON schemas, migration paths, +and tests. Each tier ships independently — T1 has no dependencies, T2 is +conditional on a Codex capability check, T3 layers on top of T1+T2. + +--- + +## Conventions + +- **New env vars** all prefixed `TASKMASTER_` to match existing namespace. +- **Boolean env vars** truthy = `1|true|yes|on` (case-insensitive); else falsy. +- **Atomic file writes** = write to `.tmp.` then `mv` into place. +- **State location** defaults to `${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}/`. +- **All new shell scripts** use `set -euo pipefail` and `bash` shebang + (we already require bash for `[[`, `local`, parameter substitution). +- **Tests** live in `tests/` and follow the existing + `bats`/`shellspec`-equivalent pattern — for new tests use the simplest + approach: `bash tests/.test.sh` returning exit 0 on pass. + +--- + +# Tier 1 — Adopt now (low risk, high value) + +## T1.1 — `TASKMASTER_VERIFY_COMMAND` shell-verifier gate + +### Goal + +Let users gate "stop allowed" behind a repo-local shell command. Use cases: +`cargo test`, `pnpm typecheck`, `make ci`, custom smoke scripts. Pairs with +the existing token-based completion (and later with T3's semantic verifier) +as a hard machine check that complements agent self-report. + +### API surface + +| Env var | Default | Meaning | +|---|---|---| +| `TASKMASTER_VERIFY_COMMAND` | unset | Shell command to run when token is seen. Empty/unset = skip. | +| `TASKMASTER_VERIFY_TIMEOUT` | `60` | Seconds before SIGTERM, +5s grace before SIGKILL. | +| `TASKMASTER_VERIFY_MAX_OUTPUT` | `4000` | Bytes of combined stdout+stderr echoed back into block reason. | +| `TASKMASTER_VERIFY_CWD` | unset | If set, `cd` here before invoking. Else inherit hook's cwd. | + +### Behavior + +``` +on stop hook: + ... existing logic up to "HAS_DONE_SIGNAL=true" ... + + if HAS_DONE_SIGNAL == true and TASKMASTER_VERIFY_COMMAND is set: + run command with timeout + if exit 0: + clear counter, allow stop (existing path) + else: + capture last $TASKMASTER_VERIFY_MAX_OUTPUT bytes of output + block with reason that includes: + - "Verifier failed (exit=N)" + - command invoked + - tail of output + - reminder that token alone is insufficient when verifier configured + counter NOT incremented (verifier failure isn't a stop attempt — agent + gets to fix and retry without burning the budget) + + if HAS_DONE_SIGNAL == false: + existing block-with-checklist behavior (verifier doesn't fire — token must + come first to avoid running expensive verifier on every stop attempt) +``` + +### Why token-then-verify rather than verify-on-every-stop + +Two reasons: + +1. The agent emitting the token is a cheap signal of "I think I'm done." It + filters out the dozens of stop attempts per session where the agent is + mid-work and would just be told to keep going by the verifier. We want the + verifier to run ~once per "I think I'm done" event, not 30 times. +2. Avoids surprising users whose verifier is `make test` (slow). Without the + token gate, every stop attempt would block on `make test`. + +### Files affected + +- **NEW** `taskmaster-verify-command.sh` — small library sourced by both + `check-completion.sh` and the codex stop path, exposing + `taskmaster_run_verify_command` returning exit code and capturing bounded + output via a temp file. +- `check-completion.sh` and `hooks/check-completion.sh` — invoke the verifier + in the `HAS_DONE_SIGNAL=true` branch. +- `taskmaster-compliance-prompt.sh` — extend `build_taskmaster_compliance_prompt` + to optionally append a "verifier configured: $cmd" hint when one is set. +- `install.sh` — no changes (env var read at runtime, not install time). +- `docs/SPEC.md` — document new env vars and behavior. + +### Reference implementation sketch + +```bash +# taskmaster-verify-command.sh +taskmaster_run_verify_command() { + local cmd="${TASKMASTER_VERIFY_COMMAND:-}" + local timeout="${TASKMASTER_VERIFY_TIMEOUT:-60}" + local max_output="${TASKMASTER_VERIFY_MAX_OUTPUT:-4000}" + local cwd="${TASKMASTER_VERIFY_CWD:-}" + local out_file + local exit_code + + [ -z "$cmd" ] && return 0 # not configured = pass + + out_file="$(mktemp "${TMPDIR:-/tmp}/taskmaster-verify.XXXXXX")" + trap 'rm -f "$out_file"' RETURN + + if [ -n "$cwd" ]; then + ( cd "$cwd" && timeout --kill-after=5 "$timeout" bash -c "$cmd" ) >"$out_file" 2>&1 & + else + timeout --kill-after=5 "$timeout" bash -c "$cmd" >"$out_file" 2>&1 & + fi + wait "$!" || exit_code=$? + exit_code="${exit_code:-0}" + + TASKMASTER_VERIFY_OUTPUT_TAIL="$(tail -c "$max_output" "$out_file")" + TASKMASTER_VERIFY_EXIT_CODE="$exit_code" + return "$exit_code" +} +``` + +### Testing + +- `tests/verify-command.test.sh`: + - `TASKMASTER_VERIFY_COMMAND="true"` → exit 0, allows stop + - `TASKMASTER_VERIFY_COMMAND="false"` → non-zero, blocks + - `TASKMASTER_VERIFY_COMMAND="sleep 120" TASKMASTER_VERIFY_TIMEOUT=2` → killed, blocks with timeout marker + - `TASKMASTER_VERIFY_COMMAND="yes | head -c 50000"` → output truncated to `TASKMASTER_VERIFY_MAX_OUTPUT` + - `TASKMASTER_VERIFY_COMMAND` unset → no-op, behaves identically to current code + +### Migration + +Zero impact when unset. Existing users see no change. + +### Risks + +| Risk | Mitigation | +|---|---| +| User sets long-running verifier, sessions stall | Default 60s timeout | +| Verifier writes huge output, OOMs hook | `tail -c $MAX_OUTPUT` from temp file, never load full output in memory | +| Verifier needs project root, hook runs elsewhere | `TASKMASTER_VERIFY_CWD` env var | +| Verifier depends on PATH that wrapper doesn't inherit | Document; use absolute paths in env var | + +--- + +## T1.2 — JSON state-file layout + +### Goal + +Replace the bare counter file (`${TMPDIR}/taskmaster/` containing +just an integer) with structured JSON state. Unblocks T1.3, T2.2, and T3 +without each adding its own ad-hoc file. + +### State file location + +``` +${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}/.json +``` + +Note the `state/` subfolder — keeps it distinct from existing counter files so +the legacy migration logic can find both. + +### Schema (v1) + +```json +{ + "schema_version": 1, + "session_id": "f1b7d967-3043-4422-9ab3-c35693951c9e", + "created_at": "2026-04-28T07:22:45Z", + "updated_at": "2026-04-28T07:24:01Z", + "stop_count": 3, + "latest_user_prompt": { + "captured_at": "2026-04-28T07:23:10Z", + "turn_id": "abc123", + "prompt": "fix the failing test in foo_test.go" + }, + "last_verifier_run": { + "ran_at": "2026-04-28T07:23:55Z", + "input_hash": "sha256:...", + "complete": false, + "reason": "test still failing on line 42", + "next_action": "run `go test -run TestFoo -v` and fix" + }, + "metadata": {} +} +``` + +`metadata` is an open object for future fields without bumping +`schema_version`. + +### Helper API + +New file `taskmaster-state.sh` (sourced): + +```bash +taskmaster_state_dir # echo path, mkdir -p +taskmaster_state_path # echo full path to .json +taskmaster_state_init # create empty file if missing (idempotent) +taskmaster_state_read # cat the JSON (empty {} if missing) +taskmaster_state_jq # run jq on state, echo result +taskmaster_state_set + # atomic update: read → jq | " = " → tmp → mv +taskmaster_state_increment_stop_count +taskmaster_state_capture_prompt +taskmaster_state_record_verifier_run +``` + +All writes use atomic tmp+mv. Concurrent writers (rare in practice — single +agent per session) coordinate via `flock` on `.lock`. + +### Atomic write pattern + +```bash +taskmaster_state_set() { + local sid="$1" jq_expr="$2" json_value="$3" + local path tmp lock + path="$(taskmaster_state_path "$sid")" + tmp="${path}.tmp.$$" + lock="${path}.lock" + + mkdir -p "$(dirname "$path")" + exec 9>"$lock" + flock 9 + if [ -f "$path" ]; then + jq --argjson v "$json_value" "$jq_expr = \$v" "$path" >"$tmp" + else + jq -n --argjson v "$json_value" \ + --arg sid "$sid" --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "{schema_version:1, session_id:\$sid, created_at:\$now, updated_at:\$now, stop_count:0} | $jq_expr = \$v" \ + >"$tmp" + fi + mv "$tmp" "$path" + exec 9>&- +} +``` + +### Legacy migration + +On first state read for a session, check for the legacy counter file: + +```bash +legacy="${TMPDIR:-/tmp}/taskmaster/${SESSION_ID}" +if [ -f "$legacy" ] && [ ! -f "$(taskmaster_state_path "$SESSION_ID")" ]; then + count=$(cat "$legacy" 2>/dev/null || echo 0) + taskmaster_state_init "$SESSION_ID" + taskmaster_state_set "$SESSION_ID" '.stop_count' "$count" + rm -f "$legacy" +fi +``` + +Run this exactly once per session (idempotent because file no longer exists +after migration). Net: existing sessions transparently upgrade. + +### Files affected + +- **NEW** `taskmaster-state.sh` +- `check-completion.sh`, `hooks/check-completion.sh` — replace counter + file logic with `taskmaster_state_increment_stop_count` and + `taskmaster_state_jq '.stop_count'` +- `hooks/inject-continue-codex.sh` — same migration +- `install.sh` — copy new script to skill dirs and `chmod +x` +- `uninstall.sh` — remove the new script +- `docs/SPEC.md` — document state schema and location + +### Testing + +- `tests/state.test.sh`: + - Init creates well-formed JSON with schema_version=1 + - Concurrent writers: 100x parallel `taskmaster_state_increment_stop_count`, + final value == 100 (flock works) + - Legacy file detected and migrated, then deleted + - Atomic write: kill -9 mid-write doesn't corrupt main file (tmp file + abandoned, real file untouched) + - jq read of nonexistent path returns empty / null without erroring + +### Migration + +Backward compatible. Existing counter files auto-migrate. New +`TASKMASTER_STATE_DIR` env var lets users relocate. + +### Risks + +| Risk | Mitigation | +|---|---| +| jq not installed | We already require jq; install.sh checks at install time | +| Disk full when writing tmp | tmp is on same fs as target; mv fails atomically; existing state preserved | +| Stale state files accumulate | Out of scope for v4.3.0 — track as follow-up beads issue (TTL cleanup, e.g., delete files older than 30 days on hook startup) | + +--- + +## T1.3 — Hook-internal-prompt detection (tagged-injection) + +### Goal + +Mark every prompt the hook injects so we (and future verifiers) can tell +"this is the user asking" from "this is taskmaster reminding the agent." +mickn's fork relies on substring-match heuristics; that's fragile to wording +changes. We can do better with a single explicit tag. + +### Design choice: explicit magic tag + +Prefix every injected prompt — block reasons, compliance prompts, queue-emitter +follow-ups — with a stable single-line marker: + +``` +[taskmaster:injected v=1 kind=] + +``` + +Where `` is one of `stop-block | followup | compliance | session-start | +verifier-feedback`. + +Detection helper: + +```bash +is_taskmaster_injected_prompt() { + local text="$1" + case "$text" in + "[taskmaster:injected v="*) return 0 ;; + *) return 1 ;; + esac +} +``` + +For backward compatibility (and to handle prompts injected before this +change), include a fallback substring matcher with mickn's exact phrases: + +```bash +is_taskmaster_injected_prompt_legacy() { + local text="$1" + case "$text" in + "` + helper; prepend to `build_taskmaster_compliance_prompt` output +- `check-completion.sh`, `hooks/check-completion.sh` — wrap the `REASON` + string with the tag +- `hooks/inject-continue-codex.sh` — same for queue payloads +- **NEW** `taskmaster-prompt-detect.sh` — exposes + `is_taskmaster_injected_prompt` for use by hooks and verifier +- `docs/SPEC.md` — document the tag format (so external tooling can detect + too) + +### Schema versioning + +The `v=1` field future-proofs the tag. If we ever change semantics (e.g., add +required machine-readable fields), bump to `v=2` and have the detector accept +both. + +### Testing + +- `tests/prompt-detect.test.sh`: + - Tagged prompt → detected + - Legacy mickn substring → detected (back-compat) + - User text containing the word "taskmaster" but not the tag → NOT detected + - Empty string → NOT detected + - Tag with future version `v=99` → still detected (forward-compat: prefix + match `[taskmaster:injected v=`) + +### Migration + +User-visible: each block reason gets a tag line at top. Document in +release notes. + +### Risks + +| Risk | Mitigation | +|---|---| +| Tag confuses users / models | Use plain ASCII, single line; mention in SKILL.md so models know the tag is metadata, not a directive | +| Some prompt path forgets the tag | `is_taskmaster_injected_prompt` falls back to legacy substring matcher | +| Markdown rendering mangles `[...]` | The tag is plain ASCII outside any code block; if a renderer hides it, the legacy substring matcher still works | + +--- + +# Tier 2 — Adopt after upstream-reality check + +T2 depends on a verifiable claim: that the OpenAI Codex CLI exposes native +hooks similar to Claude Code's. The fork review left this as an open +question. T2.0 is a discovery step that gates T2.1 and T2.2. + +## T2.0 — Codex hook capability detection (precondition) + +### Goal + +Determine whether the installed `codex` binary supports `SessionStart`, +`UserPromptSubmit`, and `Stop` hooks via `~/.codex/hooks.json`. Without this, +T2.1/T2.2 cannot be implemented natively and the wrapper architecture stays. + +### Detection plan + +1. `codex --version` → record version +2. `codex --help 2>&1 | grep -i hook` → does help mention hooks? +3. Check `~/.codex/hooks.json` schema in the installed Codex docs (if any) +4. Smoke test: write a minimal `~/.codex/hooks.json` with a `SessionStart` + hook that writes a sentinel file; launch `codex`; confirm sentinel created +5. Same for `UserPromptSubmit` and `Stop` + +Outcome documented in `docs/upstream-reviews/codex-hooks-capability-.md`. + +### Decision gates + +| Outcome | Action | +|---|---| +| All three hooks supported, `Stop` allows `decision: "block"` | Proceed with T2.1, T2.2 | +| Only some supported | Implement what's supported; keep wrapper for the rest | +| None supported | Park T2 indefinitely; T1+T3 deliver the most value anyway | + +## T2.1 — Native Codex hooks (conditional) + +### Goal + +Add a parallel implementation path that uses Codex's native hook system +instead of the PTY wrapper. Both paths coexist; install.sh picks at install +time based on T2.0 detection. Wrapper stays as the fallback for older Codex +versions. + +### New files + +- `hooks/codex-session-start.sh` — emits the SKILL.md context contract + (parallels current `run-taskmaster-codex.sh` startup) +- `hooks/codex-user-prompt-submit.sh` — captures user prompts (T2.2) +- `hooks/codex-stop.sh` — runs the same completion check as + `check-completion.sh` but adapted to Codex's hook input shape + +The Claude side (`hooks/check-completion.sh`) is unchanged. + +### install.sh changes + +```bash +detect_codex_native_hooks() { + command -v codex >/dev/null 2>&1 || return 1 + # Capability probe — see T2.0 for actual detection logic + codex --help 2>&1 | grep -qi 'hooks.json' || return 1 + return 0 +} + +if [ "$INSTALL_TARGET" = "codex" ] || [ "$INSTALL_TARGET" = "auto" ] || [ "$INSTALL_TARGET" = "both" ]; then + if detect_codex_native_hooks && [ "${TASKMASTER_CODEX_MODE:-auto}" != "wrapper" ]; then + install_codex_native # writes ~/.codex/hooks.json, links new hooks + else + install_codex_wrapper # current behavior + fi +fi +``` + +Override env var `TASKMASTER_CODEX_MODE=wrapper|native|auto` lets the user +force either path even when both work. + +### Hooks.json layout + +```json +{ + "hooks": { + "SessionStart": [{"command": "~/.codex/skills/taskmaster/hooks/codex-session-start.sh"}], + "UserPromptSubmit": [{"command": "~/.codex/skills/taskmaster/hooks/codex-user-prompt-submit.sh"}], + "Stop": [{"command": "~/.codex/skills/taskmaster/hooks/codex-stop.sh"}] + } +} +``` + +(Exact structure depends on T2.0 findings; mickn's install.sh writes +`~/.codex/hooks.json` and his `install.sh` is the reference implementation +to start from.) + +### Coexistence with wrapper + +The wrapper writes `inject.*.txt` queue files; the native path writes JSON +state. Both populate `~/.codex/taskmaster/state/.json` (T1.2). The +codex-stop.sh script reads the same state file, so the verifier (T3) works +identically on both paths. + +### Files affected + +- **NEW** `hooks/codex-session-start.sh`, `hooks/codex-user-prompt-submit.sh`, + `hooks/codex-stop.sh` +- `install.sh` — capability detection, branch, write hooks.json (when + native), preserve wrapper install (when not) +- `uninstall.sh` — remove hooks.json entries cleanly without clobbering + user-added entries (jq-based merge/unmerge, NOT file replacement) +- `docs/SPEC.md` — document both paths and the chooser logic +- `tests/install.test.sh` — both paths covered +- **NEW** `tests/codex-stop.test.sh` + +### Migration + +- New installs on capable Codex: native by default +- Existing wrapper installs: re-run `install.sh` to upgrade; or set + `TASKMASTER_CODEX_MODE=wrapper` to stay on wrapper +- Wrapper code is NOT deleted in this change — strangler-fig pattern, keep + both, monitor breakage for one full release cycle, only then prune + +### Risks + +| Risk | Mitigation | +|---|---| +| `~/.codex/hooks.json` already has user entries | Merge with jq; don't overwrite | +| Codex hook contract changes between versions | Pin tested versions in docs; add a `codex --version` check at hook entry that warns on unknown versions | +| Native and wrapper both run accidentally | install.sh sets one OR the other; sanity check at hook entry that we're not double-firing | + +## T2.2 — UserPromptSubmit goal capture + +### Goal + +Persist the user's actual goal for each turn to state, so T3's verifier has +something concrete to verify against (rather than guessing from transcript). + +### Hook input + +Codex passes JSON on stdin (per mickn's reference; T2.0 must confirm exact +field names): + +```json +{ + "session_id": "...", + "turn_id": "...", + "prompt": "", + "cwd": "...", + "model": "..." +} +``` + +### Behavior + +```bash +# read input +INPUT=$(cat) +SID=$(jq -r .session_id <<<"$INPUT") +TID=$(jq -r .turn_id <<<"$INPUT") +PROMPT=$(jq -r .prompt <<<"$INPUT") + +# filter +if is_taskmaster_injected_prompt "$PROMPT"; then exit 0; fi +if is_environment_context_only_prompt "$PROMPT"; then exit 0; fi +if is_agents_md_prelude "$PROMPT"; then exit 0; fi + +# capture +taskmaster_state_capture_prompt "$SID" "$TID" "$PROMPT" +``` + +### Wrapper-side parity + +For users on the wrapper path (T2.1 not active), we don't have a +UserPromptSubmit event. Two options: + +1. **Skip goal capture** — verifier (T3) has to infer goal from transcript. + Acceptable but lower-quality verifications. +2. **Parse the Codex session log** — `inject-continue-codex.sh` already + tails the session log for `task_complete` events; teach it to also handle + user-prompt events and write to state. + +Option 2 is mostly free since we're already tailing the log. Implement it +during T2.2. + +### Filters (in priority order) + +1. Tagged taskmaster-injected (T1.3) → skip +2. Legacy substring match → skip +3. Pure `...` block → skip +4. `# AGENTS.md instructions for ...` prelude only → skip +5. Else → capture + +Filters live in `taskmaster-prompt-detect.sh` (T1.3) — extend that file with +helpers for env-context and agents-md detection. + +### Files affected + +- `hooks/codex-user-prompt-submit.sh` (new in T2.1, populated here) +- `taskmaster-prompt-detect.sh` (T1.3) — extended with new filters +- `hooks/inject-continue-codex.sh` — extended to also write user prompts to + state (wrapper-side parity) +- `tests/prompt-detect.test.sh` — extended + +### Testing + +- All 4 filter classes correctly skipped +- A real user prompt is captured into `latest_user_prompt` and history +- Concurrent prompts in same session don't lose data (per-turn keying via + `turns[$turn_id]`) +- Prompts > 100KB are stored in full (no truncation at capture time; + truncation happens at verifier-input time, T3.1) + +--- + +# Tier 3 — Adopt with explicit knobs + +## T3.1 — Semantic completion verifier + +### Goal + +Replace "agent self-reports done" with "second agent verifies done" at +opt-in. When enabled, the stop hook calls an LLM with the captured user goal, +the agent's last message, and a transcript excerpt; the LLM returns +`{complete, reason, next_action}`. This catches cases where the agent +declares victory after partial work. + +**Default OFF.** Users opt in explicitly via env var. + +### API surface + +| Env var | Default | Meaning | +|---|---|---| +| `TASKMASTER_COMPLETION_VERIFY` | `0` | Master switch. Truthy = verifier runs. | +| `TASKMASTER_COMPLETION_PROVIDER` | `auto` | `anthropic\|openai\|command\|auto` | +| `TASKMASTER_COMPLETION_MODEL` | provider-dependent | See below | +| `TASKMASTER_COMPLETION_VERIFIER_COMMAND` | unset | Custom shell verifier; overrides built-in | +| `TASKMASTER_COMPLETION_TIMEOUT` | `30` | Seconds, then fail-open with logged warning | +| `TASKMASTER_COMPLETION_MAX_CONTEXT_CHARS` | `20000` | Total chars sent to LLM (input+goal+last_msg+transcript_tail) | +| `TASKMASTER_COMPLETION_CACHE` | `1` | Cache by input hash; `0` disables | +| `TASKMASTER_COMPLETION_FAIL_OPEN` | `1` | On API/timeout error: `1` allow stop, `0` block stop | + +### Provider auto-detection + +``` +provider == "auto": + if ANTHROPIC_API_KEY set → "anthropic" with model claude-haiku-4-5 + elif OPENAI_API_KEY set → "openai" with model gpt-5.4-mini + elif TASKMASTER_COMPLETION_VERIFIER_COMMAND set → "command" + else → log warning, fall back to legacy token detection (don't block) +``` + +Default Anthropic over OpenAI for two reasons: (1) we ship Claude users +predominantly; (2) Haiku is cheaper than gpt-5.4-mini at comparable quality +for this task. Users on OpenAI infra can set +`TASKMASTER_COMPLETION_PROVIDER=openai` explicitly. + +### Verifier I/O contract + +Same shape regardless of provider. Custom commands implement this protocol. + +**Input (stdin JSON)**: + +```json +{ + "schema_version": 1, + "session_id": "...", + "user_goal": "", + "last_assistant_message": "", + "transcript_excerpt": "" +} +``` + +**Output (stdout JSON)**: + +```json +{ + "complete": true, + "reason": "test passes; types check; no TODO comments added", + "next_action": null, + "evidence": "ran `pnpm test` mentally based on transcript" +} +``` + +When `complete=false`, `next_action` MUST be a single concrete next step +(not a list), and it gets injected verbatim into the block reason. + +### Built-in verifier prompt structure + +The built-in `taskmaster-completion-verifier.py` builds a prompt like: + +``` +You are a strict completion verifier. Your job is to decide whether the +agent has fully achieved the user's stated goal. + +USER GOAL: +{user_goal} + +AGENT'S LAST MESSAGE: +{last_assistant_message} + +TRANSCRIPT EXCERPT (most recent activity): +{transcript_excerpt} + +Respond with JSON only: {"complete": bool, "reason": str, "next_action": +str | null, "evidence": str}. + +Strict rules: +- "Made progress" is not complete. Only "goal fully achieved" is complete. +- If verification steps in the goal are unrun, complete = false. +- If the agent says "I cannot" without trying, complete = false. +- If the user explicitly deprioritized something, treat it as resolved. +``` + +Port mickn's secret-redaction regex set verbatim before the prompt is +constructed. + +### Caching + +Hash inputs (sha256 of `user_goal + "|" + last_assistant_message + "|" + tail(transcript_excerpt, 4000)`). +Store last hash + result in `state.last_verifier_run` (T1.2 schema). + +Cache hit logic: +- If input hash matches last run AND the previous result was `complete=true`, + reuse → allow stop +- If input hash matches AND previous result was `complete=false`, reuse → + block with same reason (avoids re-querying when agent retried stop without + any new work) +- If input hash differs → new query + +Net effect: an agent that hammers stop without doing new work pays for one +verifier call, not N. + +### Integration with stop hook + +``` +on stop: + ...existing logic up to HAS_DONE_SIGNAL detection... + + if TASKMASTER_COMPLETION_VERIFY is truthy: + if no latest_user_prompt captured (T2.2 inactive or first turn): + log warning, fall through to token-based detection + else: + run verifier (with cache) + if complete: + if TASKMASTER_VERIFY_COMMAND set: run that too (T1.1) + if exit 0: allow stop + else: block with verifier output + else: allow stop + else: + block with verifier reason + next_action (counter still NOT + incremented when verifier blocks — same rationale as T1.1) + else: + existing token-based logic +``` + +### Files affected + +- **NEW** `taskmaster-completion-verifier.py` (Python, ported from mickn with + redaction regexes intact, provider auto-detection added, caching added, + Anthropic-first defaults) +- **NEW** `taskmaster-completion-verifier-anthropic.py` (or single file with + provider abstraction; single file is simpler) +- `check-completion.sh`, `hooks/check-completion.sh`, `hooks/codex-stop.sh` — + invoke verifier when configured +- `taskmaster-state.sh` — `taskmaster_state_record_verifier_run` helper + (introduced in T1.2 schema, used here) +- `install.sh` — copy verifier scripts; `chmod +x`; check Python 3 available + if `TASKMASTER_COMPLETION_VERIFY=1` at install time (warn, don't block) +- `docs/SPEC.md` — full env-var table +- **NEW** `docs/cost-and-performance.md` — back-of-envelope cost per session + for each provider/model + +### Testing + +- `tests/verifier.test.sh`: + - `TASKMASTER_COMPLETION_VERIFY=0` → verifier never runs (no API calls + made; pkill any rogue processes) + - Mock provider via `TASKMASTER_COMPLETION_VERIFIER_COMMAND=tests/mock-verifier.sh` + that returns scripted JSON + - Cache hit: same input twice → only first call hits the mock + - Cache miss: input changes → second call hits mock + - Timeout: mock that sleeps > timeout → verifier exits with fail-open + - `TASKMASTER_COMPLETION_FAIL_OPEN=0`: timeout blocks stop instead + - Provider auto-detection: ANTHROPIC_API_KEY set → anthropic chosen + - Secret redaction: mock that echoes input verifies `sk-...` was redacted + +### Migration + +Default OFF means existing users see no change. Adoption is opt-in by +setting `TASKMASTER_COMPLETION_VERIFY=1`. + +### Risks + +| Risk | Mitigation | +|---|---| +| API outage blocks all stops | `TASKMASTER_COMPLETION_FAIL_OPEN=1` (default); log warning | +| Cost surprise | Default OFF; document per-provider per-session cost in cost-and-performance.md | +| Wrong default model | `TASKMASTER_COMPLETION_MODEL` overrides; document; revisit after first month of usage data | +| Secrets leaked to LLM | Port regex set verbatim; add tests that validate redaction; consider opt-out via `TASKMASTER_COMPLETION_REDACT=0` only for users who explicitly want raw context (don't make this the default) | +| Verifier disagrees with token | Verifier wins. Token alone is insufficient when verifier is on. Document this clearly. | +| Latency added to every stop | Cache cuts repeated queries; default 30s timeout caps worst case | + +--- + +# Open questions for follow-up + +These were flagged in the fork review and are restated here as design-time +risks: + +1. **Codex native hooks reality** (gates T2). Action: T2.0 capability probe + ASAP, on the user's installed Codex version. If supported, design proceeds + as written. If not, T2 is parked and T1+T3 still ship. + +2. **Verifier model selection** (impacts T3 cost/quality). Action: after T3 + ships behind the OFF default, pilot with `claude-haiku-4-5` and + `gpt-5.4-mini` on real sessions, compare false-positive and + false-negative rates, document findings in + `docs/cost-and-performance.md`. + +3. **Transcript-tail caching invariant** (impacts T3 cost). Action: confirm + that hashing the last 4000 chars of transcript_excerpt is stable enough + that "agent retried stop after producing 1KB of new output" reliably + misses the cache (we want a fresh verifier call), while "agent retried + stop with no new output" reliably hits. + +4. **Hooks.json merge semantics** (impacts T2.1). Action: T2.0 should also + verify whether multiple `Stop` hooks compose (run all? short-circuit on + first block? user-defined order?). If only one is allowed, install.sh + needs a "we'd overwrite your existing Stop hook" warning gate. + +# Rollout sequencing + +| Phase | Tier(s) | Gating | +|---|---|---| +| 1 | T1.1, T1.2, T1.3 | None — independent of upstream | +| 2 | T2.0 (capability probe) | Phase 1 merged | +| 3 | T2.1, T2.2 | T2.0 outcome positive | +| 4 | T3.1 | T1+T2 merged | + +Each phase is its own PR with its own version bump. + +# Out of scope (deliberately) + +- Stale state-file cleanup / TTL — separate beads issue +- POSIX-portable install.sh (mickn's `46f6a44` pattern) — incompatible with + our bash-only constructs; skipped per fork review +- Single-fire stop philosophy (`gjlondon`) — incompatible with our + explicit-token contract; skipped per fork review +- OpenClaw platform port (`levi-openclaw`) — different platform; skipped +- Auto-generated CLAUDE.md (`Semenka`) — generic content, no project value; + skipped From 4751e4b9ba55c77122b8fcbb51a6f0927e6bbf87 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 00:28:00 -0700 Subject: [PATCH 17/41] Add report: Codex native hooks verification before mickn port --- ...-002421-codex-native-hooks-verification.md | 271 +++++++ ...002421-codex-native-hooks-verification.pdf | Bin 0 -> 179803 bytes ...002421-codex-native-hooks-verification.tex | 681 ++++++++++++++++++ 3 files changed, 952 insertions(+) create mode 100644 docs/reports/2026-04-28-002421-codex-native-hooks-verification.md create mode 100644 docs/reports/2026-04-28-002421-codex-native-hooks-verification.pdf create mode 100644 docs/reports/2026-04-28-002421-codex-native-hooks-verification.tex diff --git a/docs/reports/2026-04-28-002421-codex-native-hooks-verification.md b/docs/reports/2026-04-28-002421-codex-native-hooks-verification.md new file mode 100644 index 0000000..d0e4f46 --- /dev/null +++ b/docs/reports/2026-04-28-002421-codex-native-hooks-verification.md @@ -0,0 +1,271 @@ +# Codex Native Hooks: Verification Before Adopting mickn's Architecture + +**Generated:** 2026-04-28 +**Topic:** Does the OpenAI Codex CLI actually support `SessionStart`, `UserPromptSubmit`, and `Stop` hooks natively today, or is `mickn:main`'s rewrite conditional on a future Codex release? + +## Executive Summary + +**Codex native hooks are real, shipped, and stable.** The OpenAI Codex +CLI exposes `SessionStart`, `UserPromptSubmit`, and `Stop` as first-class +hook events documented at `developers.openai.com/codex/hooks`. Hooks +were marked stable in **v0.122.0 (2026-04-20)** via PR #19012 +("Mark codex_hooks stable"). The locally installed version on this +machine is **`codex-cli 0.125.0`** — three releases past the stable +cutoff and well within the supported window. + +`mickn:main`'s v5.0.0 rewrite — which deletes the PTY wrapper, expect +bridge, and queue emitter, replacing them with `taskmaster-session-start.sh`, +`taskmaster-user-prompt-submit.sh`, and `taskmaster-stop.sh` — therefore +does **not** depend on any unreleased Codex feature. It targets shipping, +documented behavior. All three preconditions from the original fork +review (`docs/upstream-reviews/blader-taskmaster-forks.md` §B1) are +satisfied: + +1. Codex CLI exposes `SessionStart`, `UserPromptSubmit`, and `Stop` ✅ +2. The native `Stop` hook supports `decision: "block"` continuation ✅ +3. `last_assistant_message` is populated on Stop events ✅ (with one + caveat — see §3) + +The answer to the open question is: **proceed with the port. The +wrapper layer is dead weight on Codex 0.122+.** Two caveats worth +gating on are documented in §3 and flow into the punch list. + +## Research Findings + +### 1. Hook events explicitly documented + +The official Codex hooks reference +([developers.openai.com/codex/hooks](https://developers.openai.com/codex/hooks)) +lists six hook events: `SessionStart`, `PreToolUse`, `PermissionRequest`, +`PostToolUse`, `UserPromptSubmit`, `Stop`. The three Taskmaster needs +are all present and have dedicated sections. + +Hooks are configured via `~/.codex/hooks.json` (user scope) or +`/.codex/hooks.json` (project scope), with optional inline +configuration in `config.toml`. Per-layer hooks are merged, not +overridden — higher-precedence layers add to lower ones. Project-local +hooks only load when the `.codex/` layer is trusted. + +### 2. `Stop` hook semantics match Claude Code + +The docs explicitly state, for the `Stop` event: + +> "For this event, `decision: "block"` doesn't reject the turn. +> Instead, it tells Codex to continue and automatically creates a new +> continuation prompt" + +The `reason` field becomes the continuation prompt. This is the same +contract Claude Code's Stop hook uses, which is exactly what +`taskmaster-stop.sh` needs in order to push a TASKMASTER continuation +prompt back into the same running session. + +`Stop`'s stdin payload includes: + +- `turn_id` — the active Codex turn ID +- `stop_hook_active` — whether continuation has already occurred + (the standard guard against infinite re-fire loops; matches Claude's + field of the same name) +- `last_assistant_message` — "Latest assistant message text, if available" + +The "if available" caveat on `last_assistant_message` is the only +non-trivial parity gap with Claude Code. It is the basis for caveat +(C2) in §3. + +### 3. Release timeline + +From the Codex changelog +([developers.openai.com/codex/changelog](https://developers.openai.com/codex/changelog)): + +| Version | Date | Hook-related change | +|---------|------|---------------------| +| v0.116.0 | 2026-03 | Hooks present in experimental form (referenced in issue #15266 reproductions) | +| v0.122.0 | 2026-04-20 | `PermissionRequest` hooks added (#17563); OTEL metrics for hook runs (#18026) | +| v0.123.0 | 2026-04-23 | Hooks in `config.toml` / `requirements.toml` (#18893); MCP tool support in hooks (#18385); **`codex_hooks` marked stable (#19012)** | +| v0.124.0 | 2026-04-23 | `apply_patch` emits hooks (#18391); Bash `PostToolUse` on `exec_command` (#18888); **regression: hooks broke at startup if config used map syntax (#19199)** | +| v0.125.0 | 2026-04 | (locally installed; current) | + +The stable marker landed eight days before this report. mickn's rewrite +(repo timeline aligns with v5.0.0 around the same window) targets the +post-stable surface, not pre-release behavior. + +### 4. Known issues that don't block adoption but warrant gating + +**Issue #15266 — SessionStart + UserPromptSubmit fire simultaneously on +first prompt** ([github.com/openai/codex/issues/15266](https://github.com/openai/codex/issues/15266)). +Filed against v0.116.0 (March 2026). Closed, but the closing +commit/version is not visible in the page content. Behavior described: +on the first prompt of a session, both hooks fire concurrently rather +than `SessionStart` completing before `UserPromptSubmit`. On subsequent +prompts, only `UserPromptSubmit` fires correctly. + +Implication for Taskmaster: if `taskmaster-session-start.sh` writes +state that `taskmaster-user-prompt-submit.sh` reads (e.g., +seeding the per-session state file), there's a race on the first +prompt. mickn's `taskmaster-session-start.sh` is 27 LOC — small enough +to inspect for whether it depends on this ordering. We should verify +on 0.125.0 before merging. + +**Issue #19199 — v0.124.0 hook config parsing regression** +([github.com/openai/codex/issues/19199](https://github.com/openai/codex/issues/19199)). +`codex-cli` failed to start when hooks were configured in +`config.toml` using map syntax (the documented form). Closed; resolution +version not shown. The local install is 0.125.0, which post-dates the +fix, so this is informational only — but it's a reminder that hook +config schemas are still in flux at the toml-vs-json boundary. + +### 5. Third-party confirmation + +Independent projects already shipping against Codex's native hooks: + +- **`hatayama/codex-hooks`** — a hooks runner that reuses Claude + Code's hooks settings against Codex CLI + ([github.com/hatayama/codex-hooks](https://github.com/hatayama/codex-hooks)). + Existence of this project confirms the surface is real and + Claude-Code-compatible enough to be adapted. +- **`Yeachan-Heo/oh-my-codex` (OmX)** — a Codex enhancement framework + with an active roadmap issue (#1307) about mapping its hook surfaces + onto Codex's native hooks, indicating a community migration from + bespoke wrappers to native is in progress. +- **ArcKit v4** — released March 2026 with first-class Codex hooks + support + ([medium.com/arckit/arckit-v4](https://medium.com/arckit/arckit-v4-first-class-codex-and-gemini-support-with-hooks-mcp-servers-and-native-policies-abdf9569e00e)). + +The pattern across all three: bespoke PTY/wrapper hacks are being +deleted in favor of the native hook surface throughout April 2026. +mickn's rewrite is the same move applied to Taskmaster. + +## Analysis + +The fork review's §B1 set three preconditions for adopting mickn's +wholesale wrapper deletion. All three are satisfied: + +1. **Codex CLI exposes SessionStart/UserPromptSubmit/Stop hooks** + in the version the user is on. Local install: `codex-cli 0.125.0`. + Hook events documented since v0.122 stable; we are on 0.125. + ✅ confirmed. + +2. **The native `Stop` hook supports `decision: "block"` continuation** + in the same way Claude Code does. Documented verbatim in + `developers.openai.com/codex/hooks`: "doesn't reject the turn. + Instead, it tells Codex to continue and automatically creates a new + continuation prompt." ✅ confirmed. + +3. **`last_assistant_message` is populated by Codex on stop events.** + Documented as a Stop-event stdin field, with the qualifier "if + available." This matches Claude Code's behavior, which also has + cases where the field is empty (e.g., when the assistant emits no + message text on stop). ⚠️ confirmed-with-caveat. + +The caveat on (3) is meaningful but not blocking. The current fork's +detection is layered: `last_assistant_message` for primary detection, +transcript-grep for explicit `TASKMASTER_DONE::` token as +fallback. That layering should survive the port unchanged — the +fallback handles the "if available" gap. + +The PTY wrapper, expect bridge, and queue emitter become genuinely +redundant at 0.122+. They cost LOC, complexity, and a `expect` runtime +dependency. Their only remaining justification was as a portability +floor for Codex versions without native hooks — which now means +versions older than April 2026, a window users will only stay in +deliberately. + +The right migration shape mirrors §B1's hedge: keep both code paths, +gate on `codex --version` or a feature probe (`codex --help | grep -q +hook` or test for the `~/.codex/hooks.json` schema), and let +`install.sh` choose. Default to native on detection, fall back to +wrapper on older Codex. Delete the wrapper path only after a deprecation +window where logs confirm zero installs are using it. + +## Recommendations + +Adopt mickn's native-hooks architecture, but stage it. Two safety +rails make this safe rather than risky: + +1. **Version-gated install.** Probe Codex version in `install.sh`. + If `>= 0.122.0`, install native hooks; if `< 0.122.0` or no codex + detected, install the existing wrapper path. The user's machine + (0.125.0) gets the native path automatically. +2. **Keep the wrapper path on disk.** Don't `git rm` the PTY wrapper, + expect bridge, or their tests in the same PR. Mark them + `legacy/`-prefixed and have `install.sh` install from `legacy/` + when version-gated. Plan to remove them after one minor release if + no one reports using them. + +The `last_assistant_message` + transcript-token layered detection +already in the fork is the right pattern and ports cleanly. Do not +collapse to a single detection mode. + +## Punch List (for `/mei`) + +Each item is a self-contained adoption decision. Numbered for +priority. Phase tags only — no time estimates. + +1. **[Phase 1, HIGH] Add Codex version probe to `install.sh`.** + Detect `codex --version` and parse semver; expose as + `$CODEX_HOOKS_NATIVE` (true if `>= 0.122.0`). Touches only + `install.sh`. No behavior change yet — just the detection. + +2. **[Phase 1, HIGH] Port `taskmaster-session-start.sh` from + `mickn:main`.** 27 LOC. Place at `hooks/taskmaster-session-start.sh`. + Verify it does not depend on completing-before-`UserPromptSubmit` + ordering (issue #15266). If it does, add a state-file lock that + both hooks honor. + +3. **[Phase 1, HIGH] Port `taskmaster-stop.sh` from `mickn:main`.** + 356 LOC. Replaces the wrapper's stop-detection role. Must emit + `decision: "block"` JSON with the shared compliance prompt as + `reason`. Reuses `taskmaster-compliance-prompt.sh`. Keep the + `last_assistant_message` → transcript-token fallback layered + detection. + +4. **[Phase 1, HIGH] Port `taskmaster-user-prompt-submit.sh` from + `mickn:main`.** 107 LOC. Implements per-turn external user prompt + capture with `` filtering. This is pattern A1 from the + fork review and the highest-value behavioral upgrade. + +5. **[Phase 1, MEDIUM] Move existing wrapper artifacts to `legacy/`.** + `hooks/inject-continue-codex.sh`, `hooks/run-codex-expect-bridge.exp`, + `run-taskmaster-codex.sh`, plus their tests in + `tests/inject-continue-codex.test.sh` etc. Update `install.sh` to + choose `hooks/` (native) or `legacy/` (wrapper) based on the + version probe. + +6. **[Phase 1, MEDIUM] Write `~/.codex/hooks.json` template** in + `install.sh` mapping the three event names to the three new + `hooks/taskmaster-*.sh` scripts. Merge-safe with any existing + user hooks (don't clobber). + +7. **[Phase 2, MEDIUM] Add a feature smoke test:** create `taskmaster + selftest --codex-hooks` that fires a no-op session, asserts each of + the three hooks executed via state-file markers, and reports OK/FAIL. + Exercised in `install.sh --verify` and CI. + +8. **[Phase 2, LOW] Document the version gate in + `docs/SPEC.md`.** Single section explaining the dual code paths, + the 0.122.0 cutover, the issue-#15266 race awareness, and the + deprecation plan for `legacy/`. + +9. **[Phase 3, LOW] Sunset `legacy/` after one minor release** if no + reports of installs using it. Track via an `install.sh` + instrumentation line that logs which path was selected. Drop after + evidence justifies it. + +10. **[Phase 3, LOW] Watch upstream issue #15266** for a definitive + fix-version. If the simultaneous-fire race is confirmed fixed in + a known version, tighten `$CODEX_HOOKS_NATIVE` lower bound to + that version and remove any race-mitigation lock added in (2). + +## Sources + +- [Hooks – Codex | OpenAI Developers](https://developers.openai.com/codex/hooks) — primary reference; documents all six hook events including `SessionStart`, `UserPromptSubmit`, `Stop`. Contains the verbatim specification of `decision: "block"` for `Stop` and the `last_assistant_message` field. +- [Changelog – Codex | OpenAI Developers](https://developers.openai.com/codex/changelog) — release timeline confirming hooks went stable in v0.122.0 (April 20, 2026) via PR #19012 "Mark codex_hooks stable." +- [Issue #15266 — UserPromptSubmit and SessionStart hooks fire simultaneously on first prompt](https://github.com/openai/codex/issues/15266) — known caveat, filed v0.116.0 (March 2026), closed. +- [Issue #19199 — codex-cli 0.124.0 fails to start when hook config is present and codex_hooks is enabled](https://github.com/openai/codex/issues/19199) — informational; not a blocker on 0.125.0. +- [PR #14867 — hooks: use a user message > developer message for prompt continuation](https://github.com/openai/codex/pull/14867) — early-development context for hook continuation semantics. +- [PR #15118 — hooks: turn_id extension for Stop & UserPromptSubmit](https://github.com/openai/codex/pull/15118) — confirms `turn_id` field added to the stdin payload of these specific hooks. +- [Discussion #2150 — Hook would be a great feature](https://github.com/openai/codex/discussions/2150) — historical context for how hooks landed. +- [hatayama/codex-hooks](https://github.com/hatayama/codex-hooks) — third-party hooks runner reusing Claude Code's hooks settings against Codex; corroborates Claude-compatible surface. +- [Yeachan-Heo/oh-my-codex issue #1307 — roadmap: map OMC hook surfaces onto OMX native Codex hooks](https://github.com/Yeachan-Heo/oh-my-codex/issues/1307) — community-wide migration pattern from bespoke wrappers to native hooks. +- [ArcKit v4: First-Class Codex and Gemini Support with Hooks](https://medium.com/arckit/arckit-v4-first-class-codex-and-gemini-support-with-hooks-mcp-servers-and-native-policies-abdf9569e00e) — independent third-party adoption of the same hook surface in March 2026. +- Local fork review: `docs/upstream-reviews/blader-taskmaster-forks.md` §B1 — the three preconditions verified by this report. +- Local Codex install: `codex-cli 0.125.0` (`/usr/local/bin/codex`) — three releases past the stable cutoff. diff --git a/docs/reports/2026-04-28-002421-codex-native-hooks-verification.pdf b/docs/reports/2026-04-28-002421-codex-native-hooks-verification.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ff50ca59cf8f8f9c1406bcccde5e2b6de3ac538e GIT binary patch literal 179803 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 literal 0 HcmV?d00001 diff --git a/docs/reports/2026-04-28-002421-codex-native-hooks-verification.tex b/docs/reports/2026-04-28-002421-codex-native-hooks-verification.tex new file mode 100644 index 0000000..fad7967 --- /dev/null +++ b/docs/reports/2026-04-28-002421-codex-native-hooks-verification.tex @@ -0,0 +1,681 @@ +\documentclass[twocolumn,10pt,letterpaper]{article} + +% -- Page geometry -- +\usepackage[ + top=0.85in, + bottom=0.75in, + left=0.65in, + right=0.65in, + columnsep=0.25in +]{geometry} + +% -- Beautiful typography: Century Schoolbook -- +\usepackage[T1]{fontenc} +\usepackage{tgschola} % TeX Gyre Schola (Century Schoolbook) +\usepackage[scaled=0.82]{beramono} % Bera Mono for code +\usepackage{microtype} % Microtypographic refinements +\usepackage{setspace} % Line spacing control +\setstretch{1.08} % Slightly open leading for readability + +% -- Packages -- +\usepackage{xurl} % Allow URL breaks at any character (load before hyperref) +\usepackage{hyperref} +\usepackage{graphicx} +\usepackage{booktabs} % Professional tables +\usepackage{tabularx} % Width-constrained tables +\usepackage{enumitem} % List customization +\usepackage{fancyhdr} +\usepackage{xcolor} +\usepackage{balance} +\usepackage{stfloats} % Enable [h] placement for table* in two-column layout +\usepackage{titlesec} % Section heading customization +\usepackage{listings} % Code blocks with line wrapping + +% -- Paragraph spacing (no indent, modest skip) -- +\setlength{\parindent}{0pt} +\setlength{\parskip}{0.4em plus 0.1em minus 0.05em} +\setlength{\emergencystretch}{1em} % Extra stretch to avoid overfull hboxes in narrow columns + +% -- Code listings with line wrapping -- +\lstset{ + basicstyle=\small\ttfamily, + breaklines=true, + breakatwhitespace=false, + breakautoindent=true, + postbreak=\mbox{\textcolor{accentrule}{$\hookrightarrow$}\space}, + columns=fullflexible, + keepspaces=true, + backgroundcolor=\color{codebg}, + frame=single, + rulecolor=\color{codebg}, + framesep=3pt, + aboveskip=0.5em, + belowskip=0.5em, + xleftmargin=4pt, + xrightmargin=0pt, +} + +% -- Colors -- +\definecolor{linkblue}{HTML}{1D4ED8} +\definecolor{headergray}{HTML}{1F2937} +\definecolor{rulegray}{HTML}{D1D5DB} +\definecolor{titlebg}{HTML}{111827} +\definecolor{accentrule}{HTML}{6B7280} +\definecolor{brandred}{HTML}{8B2635} +\definecolor{codebg}{HTML}{F3F4F6} % Very light gray for code backgrounds + +% -- Hyperlinks -- +\hypersetup{ + colorlinks=true, + linkcolor=linkblue, + urlcolor=linkblue, + citecolor=linkblue, + pdfstartview=FitH +} + +% -- Section numbering: Roman > Alph > Arabic > alph, no decimals -- +\renewcommand{\thesection}{\Roman{section}} +\renewcommand{\thesubsection}{\Alph{subsection}} +\renewcommand{\thesubsubsection}{\arabic{subsubsection}} +\renewcommand{\theparagraph}{\alph{paragraph}} +\makeatletter +\@addtoreset{subsection}{section} +\@addtoreset{subsubsection}{subsection} +\@addtoreset{paragraph}{subsubsection} +\makeatother + +% -- Section headings -- +\titleformat{\section} + {\large\bfseries\color{titlebg}} + {\thesection.}{0.5em}{} + [\vspace{-0.3em}{\color{accentrule}\rule{\columnwidth}{0.5pt}}] +\titleformat{\subsection} + {\normalsize\bfseries\color{headergray}} + {\thesubsection.}{0.4em}{} +\titleformat{\subsubsection} + {\small\bfseries\itshape\color{headergray}} + {\thesubsubsection.}{0.4em}{} +\titleformat{\paragraph}[runin] + {\small\bfseries\color{headergray}} + {\theparagraph)}{0.4em}{}[.\hspace{0.4em}] +\titlespacing*{\section}{0pt}{1.0em}{0.35em} +\titlespacing*{\subsection}{0pt}{0.75em}{0.2em} +\titlespacing*{\subsubsection}{0pt}{0.55em}{0.15em} +\titlespacing*{\paragraph}{0pt}{0.4em}{0em} + +% -- Lists: compact, elegant -- +\setlist{nosep, leftmargin=1.1em, topsep=0.2em, itemsep=0.05em} +\setlist[itemize]{label={\small\textcolor{accentrule}{\textbullet}}} +\setlist[enumerate]{label={\small\textcolor{headergray}{\arabic*.}}} + +% -- Tables: tighter, small text -- +\renewcommand{\arraystretch}{1.15} + +% -- Header/footer (pages 2+) -- +\pagestyle{fancy} +\fancyhf{} +\renewcommand{\headrulewidth}{0.3pt} +\renewcommand{\headrule}{\hbox to\headwidth{\color{rulegray}\leaders\hrule height \headrulewidth\hfill}} +\fancyhead[L]{\footnotesize\color{headergray}\textit{Codex Native Hooks: Verification Before Adopting mickn's Architecture}} +\renewcommand{\footrulewidth}{0.3pt} +\renewcommand{\footrule}{\hbox to\headwidth{\color{rulegray}\leaders\hrule height \footrulewidth\hfill}} +% Brand mark footer: CaseMirror wordmark left, page number right (both burgundy) +\fancyfoot[L]{\footnotesize\textbf{\color{headergray}CaseMirror}} +\fancyfoot[R]{\footnotesize\color{headergray}\thepage} + +% -- Page 1 style: footer only, no header rule -- +\fancypagestyle{firstpage}{% + \fancyhf{}% + \renewcommand{\headrulewidth}{0pt}% + \renewcommand{\footrulewidth}{0.3pt}% + \renewcommand{\footrule}{\hbox to\headwidth{\color{rulegray}\leaders\hrule height 0.3pt\hfill}}% + \fancyfoot[L]{\footnotesize\textbf{\color{headergray}CaseMirror}}% + \fancyfoot[R]{\footnotesize\color{headergray}\thepage}% +} + +% -- Column rule -- +\setlength{\columnseprule}{0.15pt} +\renewcommand{\columnseprulecolor}{\color{rulegray}} + +% -- Title -- +\title{\vspace{-1.8em}\LARGE\bfseries\color{titlebg} Codex Native Hooks: Verification Before Adopting mickn's Architecture\vspace{-0.3em}} +\author{CaseMirror Research \\ \small\itshape\href{https://casemirror.ai}{\textcolor{headergray}{casemirror.ai}}} +\date{{\small\color{headergray} \today}} + +\begin{document} + +\maketitle +\thispagestyle{firstpage} +\vspace{-0.5em} + + +\textbf{Generated:} 2026-04-28 + +\textbf{Topic:} Does the OpenAI Codex CLI actually support \texttt{SessionStart}, \texttt{UserPromptSubmit}, and \texttt{Stop} hooks natively today, or is \texttt{mickn:main}'s rewrite conditional on a future Codex release? + + +\subsection{Executive Summary} + +\textbf{Codex native hooks are real, shipped, and stable.} The OpenAI Codex + +CLI exposes \texttt{SessionStart}, \texttt{UserPromptSubmit}, and \texttt{Stop} as first-class + +hook events documented at \texttt{developers.openai.com/\allowbreak codex/\allowbreak hooks}. Hooks + +were marked stable in \textbf{v0.122.0 (2026-04-20)} via PR \#19012 + +("Mark codex\_hooks stable"). The locally installed version on this + +machine is \textbf{\texttt{codex-\allowbreak cli 0.125.0}} — three releases past the stable + +cutoff and well within the supported window. + + +\texttt{mickn:main}'s v5.0.0 rewrite — which deletes the PTY wrapper, expect + +bridge, and queue emitter, replacing them with \texttt{taskmaster-\allowbreak session-\allowbreak start.sh}, + +\texttt{taskmaster-\allowbreak user-\allowbreak prompt-\allowbreak submit.sh}, and \texttt{taskmaster-\allowbreak stop.sh} — therefore + +does \textbf{not} depend on any unreleased Codex feature. It targets shipping, + +documented behavior. All three preconditions from the original fork + +review (\texttt{docs/\allowbreak upstream-\allowbreak reviews/\allowbreak blader-\allowbreak taskmaster-\allowbreak forks.md} §B1) are + +satisfied: + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item Codex CLI exposes \texttt{SessionStart}, \texttt{UserPromptSubmit}, and \texttt{Stop} ✅ + \item The native \texttt{Stop} hook supports \texttt{decision: "block"} continuation ✅ + \item \texttt{last\_assistant\_message} is populated on Stop events ✅ (with one +\end{enumerate} + caveat — see §3) + + +The answer to the open question is: **proceed with the port. The + +wrapper layer is dead weight on Codex 0.122+.** Two caveats worth + +gating on are documented in §3 and flow into the punch list. + + +\subsection{Research Findings} + +\subsubsection{Hook events explicitly documented} + +The official Codex hooks reference + +(\href{https://developers.openai.com/codex/hooks}{developers.openai.com/codex/hooks}) + +lists six hook events: \texttt{SessionStart}, \texttt{PreToolUse}, \texttt{PermissionRequest}, + +\texttt{PostToolUse}, \texttt{UserPromptSubmit}, \texttt{Stop}. The three Taskmaster needs + +are all present and have dedicated sections. + + +Hooks are configured via \texttt{\textasciitilde{}/\allowbreak .codex/\allowbreak hooks.json} (user scope) or + +\texttt{/\allowbreak .codex/\allowbreak hooks.json} (project scope), with optional inline + +configuration in \texttt{config.toml}. Per-layer hooks are merged, not + +overridden — higher-precedence layers add to lower ones. Project-local + +hooks only load when the \texttt{.codex/\allowbreak } layer is trusted. + + +\subsubsection{\texttt{Stop} hook semantics match Claude Code} + +The docs explicitly state, for the \texttt{Stop} event: + + +> "For this event, \texttt{decision: "block"} doesn't reject the turn. + +> Instead, it tells Codex to continue and automatically creates a new + +> continuation prompt" + + +The \texttt{reason} field becomes the continuation prompt. This is the same + +contract Claude Code's Stop hook uses, which is exactly what + +\texttt{taskmaster-\allowbreak stop.sh} needs in order to push a TASKMASTER continuation + +prompt back into the same running session. + + +\texttt{Stop}'s stdin payload includes: + + +\begin{itemize} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \texttt{turn\_id} — the active Codex turn ID + \item \texttt{stop\_hook\_active} — whether continuation has already occurred +\end{itemize} + (the standard guard against infinite re-fire loops; matches Claude's + + field of the same name) + +\begin{itemize} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \texttt{last\_assistant\_message} — "Latest assistant message text, if available" +\end{itemize} + +The "if available" caveat on \texttt{last\_assistant\_message} is the only + +non-trivial parity gap with Claude Code. It is the basis for caveat + +(C2) in §3. + + +\subsubsection{Release timeline} + +From the Codex changelog + +(\href{https://developers.openai.com/codex/changelog}{developers.openai.com/codex/changelog}): + + +\vspace{0.4em} +\noindent +\small +\begin{tabularx}{\columnwidth}{@{}XXX@{}} +\toprule +\textbf{Version} & \textbf{Date} & \textbf{Hook-related change} \\ +\midrule +v0.116.0 & 2026-03 & Hooks present in experimental form (referenced in issue \#15266 reproductions) \\ +v0.122.0 & 2026-04-20 & \texttt{PermissionRequest} hooks added (\#17563); OTEL metrics for hook runs (\#18026) \\ +v0.123.0 & 2026-04-23 & Hooks in \texttt{config.toml} / \texttt{requirements.toml} (\#18893); MCP tool support in hooks (\#18385); \textbf{\texttt{codex\_hooks} marked stable (\#19012)} \\ +v0.124.0 & 2026-04-23 & \texttt{apply\_patch} emits hooks (\#18391); Bash \texttt{PostToolUse} on \texttt{exec\_command} (\#18888); \textbf{regression: hooks broke at startup if config used map syntax (\#19199)} \\ +v0.125.0 & 2026-04 & (locally installed; current) \\ +\bottomrule +\end{tabularx} +\vspace{0.4em} + +The stable marker landed eight days before this report. mickn's rewrite + +(repo timeline aligns with v5.0.0 around the same window) targets the + +post-stable surface, not pre-release behavior. + + +\subsubsection{Known issues that don't block adoption but warrant gating} + +**Issue \#15266 — SessionStart + UserPromptSubmit fire simultaneously on + +first prompt** (\href{https://github.com/openai/codex/issues/15266}{github.com/openai/codex/issues/15266}). + +Filed against v0.116.0 (March 2026). Closed, but the closing + +commit/version is not visible in the page content. Behavior described: + +on the first prompt of a session, both hooks fire concurrently rather + +than \texttt{SessionStart} completing before \texttt{UserPromptSubmit}. On subsequent + +prompts, only \texttt{UserPromptSubmit} fires correctly. + + +Implication for Taskmaster: if \texttt{taskmaster-\allowbreak session-\allowbreak start.sh} writes + +state that \texttt{taskmaster-\allowbreak user-\allowbreak prompt-\allowbreak submit.sh} reads (e.g., + +seeding the per-session state file), there's a race on the first + +prompt. mickn's \texttt{taskmaster-\allowbreak session-\allowbreak start.sh} is 27 LOC — small enough + +to inspect for whether it depends on this ordering. We should verify + +on 0.125.0 before merging. + + +\textbf{Issue \#19199 — v0.124.0 hook config parsing regression} + +(\href{https://github.com/openai/codex/issues/19199}{github.com/openai/codex/issues/19199}). + +\texttt{codex-\allowbreak cli} failed to start when hooks were configured in + +\texttt{config.toml} using map syntax (the documented form). Closed; resolution + +version not shown. The local install is 0.125.0, which post-dates the + +fix, so this is informational only — but it's a reminder that hook + +config schemas are still in flux at the toml-vs-json boundary. + + +\subsubsection{Third-party confirmation} + +Independent projects already shipping against Codex's native hooks: + + +\begin{itemize} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{\texttt{hatayama/\allowbreak codex-\allowbreak hooks}} — a hooks runner that reuses Claude +\end{itemize} + Code's hooks settings against Codex CLI + + (\href{https://github.com/hatayama/codex-hooks}{github.com/hatayama/codex-hooks}). + + Existence of this project confirms the surface is real and + + Claude-Code-compatible enough to be adapted. + +\begin{itemize} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{\texttt{Yeachan-\allowbreak Heo/\allowbreak oh-\allowbreak my-\allowbreak codex} (OmX)} — a Codex enhancement framework +\end{itemize} + with an active roadmap issue (\#1307) about mapping its hook surfaces + + onto Codex's native hooks, indicating a community migration from + + bespoke wrappers to native is in progress. + +\begin{itemize} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{ArcKit v4} — released March 2026 with first-class Codex hooks +\end{itemize} + support + + (\href{https://medium.com/arckit/arckit-v4-first-class-codex-and-gemini-support-with-hooks-mcp-servers-and-native-policies-abdf9569e00e}{medium.com/arckit/arckit-v4}). + + +The pattern across all three: bespoke PTY/wrapper hacks are being + +deleted in favor of the native hook surface throughout April 2026. + +mickn's rewrite is the same move applied to Taskmaster. + + +\subsection{Analysis} + +The fork review's §B1 set three preconditions for adopting mickn's + +wholesale wrapper deletion. All three are satisfied: + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{Codex CLI exposes SessionStart/UserPromptSubmit/Stop hooks} +\end{enumerate} + in the version the user is on. Local install: \texttt{codex-\allowbreak cli 0.125.0}. + + Hook events documented since v0.122 stable; we are on 0.125. + + ✅ confirmed. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{The native \texttt{Stop} hook supports \texttt{decision: "block"} continuation} +\end{enumerate} + in the same way Claude Code does. Documented verbatim in + + \texttt{developers.openai.com/\allowbreak codex/\allowbreak hooks}: "doesn't reject the turn. + + Instead, it tells Codex to continue and automatically creates a new + + continuation prompt." ✅ confirmed. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{\texttt{last\_assistant\_message} is populated by Codex on stop events.} +\end{enumerate} + Documented as a Stop-event stdin field, with the qualifier "if + + available." This matches Claude Code's behavior, which also has + + cases where the field is empty (e.g., when the assistant emits no + + message text on stop). ⚠️ confirmed-with-caveat. + + +The caveat on (3) is meaningful but not blocking. The current fork's + +detection is layered: \texttt{last\_assistant\_message} for primary detection, + +transcript-grep for explicit \texttt{TASKMASTER\_DONE::} token as + +fallback. That layering should survive the port unchanged — the + +fallback handles the "if available" gap. + + +The PTY wrapper, expect bridge, and queue emitter become genuinely + +redundant at 0.122+. They cost LOC, complexity, and a \texttt{expect} runtime + +dependency. Their only remaining justification was as a portability + +floor for Codex versions without native hooks — which now means + +versions older than April 2026, a window users will only stay in + +deliberately. + + +The right migration shape mirrors §B1's hedge: keep both code paths, + +gate on \texttt{codex -\allowbreak -\allowbreak version} or a feature probe (`codex --help | grep -q + +hook\texttt{ or test for the }\textasciitilde{}/.codex/hooks.json` schema), and let + +\texttt{install.sh} choose. Default to native on detection, fall back to + +wrapper on older Codex. Delete the wrapper path only after a deprecation + +window where logs confirm zero installs are using it. + + +\subsection{Recommendations} + +Adopt mickn's native-hooks architecture, but stage it. Two safety + +rails make this safe rather than risky: + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{Version-gated install.} Probe Codex version in \texttt{install.sh}. +\end{enumerate} + If \texttt{>= 0.122.0}, install native hooks; if \texttt{< 0.122.0} or no codex + + detected, install the existing wrapper path. The user's machine + + (0.125.0) gets the native path automatically. + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{Keep the wrapper path on disk.} Don't \texttt{git rm} the PTY wrapper, +\end{enumerate} + expect bridge, or their tests in the same PR. Mark them + + \texttt{legacy/\allowbreak }-prefixed and have \texttt{install.sh} install from \texttt{legacy/\allowbreak } + + when version-gated. Plan to remove them after one minor release if + + no one reports using them. + + +The \texttt{last\_assistant\_message} + transcript-token layered detection + +already in the fork is the right pattern and ports cleanly. Do not + +collapse to a single detection mode. + + +\subsection{Punch List (for \texttt{/\allowbreak mei})} + +Each item is a self-contained adoption decision. Numbered for + +priority. Phase tags only — no time estimates. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{[Phase 1, HIGH] Add Codex version probe to \texttt{install.sh}.} +\end{enumerate} + Detect \texttt{codex -\allowbreak -\allowbreak version} and parse semver; expose as + + \texttt{$CODEX\_HOOKS\_NATIVE} (true if \texttt{>= 0.122.0}). Touches only + + \texttt{install.sh}. No behavior change yet — just the detection. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item **[Phase 1, HIGH] Port \texttt{taskmaster-\allowbreak session-\allowbreak start.sh} from +\end{enumerate} + \texttt{mickn:main}.** 27 LOC. Place at \texttt{hooks/\allowbreak taskmaster-\allowbreak session-\allowbreak start.sh}. + + Verify it does not depend on completing-before-\texttt{UserPromptSubmit} + + ordering (issue \#15266). If it does, add a state-file lock that + + both hooks honor. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{[Phase 1, HIGH] Port \texttt{taskmaster-\allowbreak stop.sh} from \texttt{mickn:main}.} +\end{enumerate} + 356 LOC. Replaces the wrapper's stop-detection role. Must emit + + \texttt{decision: "block"} JSON with the shared compliance prompt as + + \texttt{reason}. Reuses \texttt{taskmaster-\allowbreak compliance-\allowbreak prompt.sh}. Keep the + + \texttt{last\_assistant\_message} → transcript-token fallback layered + + detection. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item **[Phase 1, HIGH] Port \texttt{taskmaster-\allowbreak user-\allowbreak prompt-\allowbreak submit.sh} from +\end{enumerate} + \texttt{mickn:main}.** 107 LOC. Implements per-turn external user prompt + + capture with \texttt{} filtering. This is pattern A1 from the + + fork review and the highest-value behavioral upgrade. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{[Phase 1, MEDIUM] Move existing wrapper artifacts to \texttt{legacy/\allowbreak }.} +\end{enumerate} + \texttt{hooks/\allowbreak inject-\allowbreak continue-\allowbreak codex.sh}, \texttt{hooks/\allowbreak run-\allowbreak codex-\allowbreak expect-\allowbreak bridge.exp}, + + \texttt{run-\allowbreak taskmaster-\allowbreak codex.sh}, plus their tests in + + \texttt{tests/\allowbreak inject-\allowbreak continue-\allowbreak codex.test.sh} etc. Update \texttt{install.sh} to + + choose \texttt{hooks/\allowbreak } (native) or \texttt{legacy/\allowbreak } (wrapper) based on the + + version probe. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{[Phase 1, MEDIUM] Write \texttt{\textasciitilde{}/\allowbreak .codex/\allowbreak hooks.json} template} in +\end{enumerate} + \texttt{install.sh} mapping the three event names to the three new + + \texttt{hooks/\allowbreak taskmaster-\allowbreak *.sh} scripts. Merge-safe with any existing + + user hooks (don't clobber). + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{[Phase 2, MEDIUM] Add a feature smoke test:} create `taskmaster +\end{enumerate} + selftest --codex-hooks` that fires a no-op session, asserts each of + + the three hooks executed via state-file markers, and reports OK/FAIL. + + Exercised in \texttt{install.sh -\allowbreak -\allowbreak verify} and CI. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item **[Phase 2, LOW] Document the version gate in +\end{enumerate} + \texttt{docs/\allowbreak SPEC.md}.** Single section explaining the dual code paths, + + the 0.122.0 cutover, the issue-\#15266 race awareness, and the + + deprecation plan for \texttt{legacy/\allowbreak }. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{[Phase 3, LOW] Sunset \texttt{legacy/\allowbreak } after one minor release} if no +\end{enumerate} + reports of installs using it. Track via an \texttt{install.sh} + + instrumentation line that logs which path was selected. Drop after + + evidence justifies it. + + +\begin{enumerate} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \textbf{[Phase 3, LOW] Watch upstream issue \#15266} for a definitive +\end{enumerate} + fix-version. If the simultaneous-fire race is confirmed fixed in + + a known version, tighten \texttt{$CODEX\_HOOKS\_NATIVE} lower bound to + + that version and remove any race-mitigation lock added in (2). + + +\subsection{Sources} + +\begin{itemize} +\setlength{\itemsep}{0pt} +\setlength{\parskip}{0pt} + \item \href{https://developers.openai.com/codex/hooks}{Hooks – Codex | OpenAI Developers} — primary reference; documents all six hook events including \texttt{SessionStart}, \texttt{UserPromptSubmit}, \texttt{Stop}. Contains the verbatim specification of \texttt{decision: "block"} for \texttt{Stop} and the \texttt{last\_assistant\_message} field. + \item \href{https://developers.openai.com/codex/changelog}{Changelog – Codex | OpenAI Developers} — release timeline confirming hooks went stable in v0.122.0 (April 20, 2026) via PR \#19012 "Mark codex\_hooks stable." + \item \href{https://github.com/openai/codex/issues/15266}{Issue \#15266 — UserPromptSubmit and SessionStart hooks fire simultaneously on first prompt} — known caveat, filed v0.116.0 (March 2026), closed. + \item \href{https://github.com/openai/codex/issues/19199}{Issue \#19199 — codex-cli 0.124.0 fails to start when hook config is present and codex\_hooks is enabled} — informational; not a blocker on 0.125.0. + \item \href{https://github.com/openai/codex/pull/14867}{PR \#14867 — hooks: use a user message > developer message for prompt continuation} — early-development context for hook continuation semantics. + \item \href{https://github.com/openai/codex/pull/15118}{PR \#15118 — hooks: turn\_id extension for Stop \& UserPromptSubmit} — confirms \texttt{turn\_id} field added to the stdin payload of these specific hooks. + \item \href{https://github.com/openai/codex/discussions/2150}{Discussion \#2150 — Hook would be a great feature} — historical context for how hooks landed. + \item \href{https://github.com/hatayama/codex-hooks}{hatayama/codex-hooks} — third-party hooks runner reusing Claude Code's hooks settings against Codex; corroborates Claude-compatible surface. + \item \href{https://github.com/Yeachan-Heo/oh-my-codex/issues/1307}{Yeachan-Heo/oh-my-codex issue \#1307 — roadmap: map OMC hook surfaces onto OMX native Codex hooks} — community-wide migration pattern from bespoke wrappers to native hooks. + \item \href{https://medium.com/arckit/arckit-v4-first-class-codex-and-gemini-support-with-hooks-mcp-servers-and-native-policies-abdf9569e00e}{ArcKit v4: First-Class Codex and Gemini Support with Hooks} — independent third-party adoption of the same hook surface in March 2026. + \item Local fork review: \texttt{docs/\allowbreak upstream-\allowbreak reviews/\allowbreak blader-\allowbreak taskmaster-\allowbreak forks.md} §B1 — the three preconditions verified by this report. + \item Local Codex install: \texttt{codex-\allowbreak cli 0.125.0} (\texttt{/\allowbreak usr/\allowbreak local/\allowbreak bin/\allowbreak codex}) — three releases past the stable cutoff. +\end{itemize} + + +\balance +\end{document} From dc503c9669c59a2028ec9ad3fbeca9cf08f8035f Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 01:40:31 -0700 Subject: [PATCH 18/41] docs: add implementation plan for T1 fork-pattern adoption Bite-sized TDD plan covering all three Tier-1 features from docs/designs/2026-04-28-072245-fork-pattern-adoption.md: - Phase A (T1.1): TASKMASTER_VERIFY_COMMAND helper lib + integration - Phase B (T1.3): tagged hook-injected prompts + detection lib - Phase C (T1.2): JSON state file with flock + legacy migration - Phase D: version bump to 4.3.0, CHANGELOG, tag Each phase: failing-test commit, then implementation commit. Commits are bisect-friendly. Plan includes complete code (not pseudocode), exact file paths, and smoke tests at every integration point. --- ...6-04-28-083546-t1-fork-pattern-adoption.md | 1509 +++++++++++++++++ 1 file changed, 1509 insertions(+) create mode 100644 docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md diff --git a/docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md b/docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md new file mode 100644 index 0000000..ff4647e --- /dev/null +++ b/docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md @@ -0,0 +1,1509 @@ +# Tier 1 Fork-Pattern Adoption — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. + +**Goal:** Ship the three Tier-1 patterns from `docs/designs/2026-04-28-072245-fork-pattern-adoption.md`: a `TASKMASTER_VERIFY_COMMAND` shell-verifier gate (T1.1), tagged hook-internal-prompt detection (T1.3), and a JSON state-file layout with legacy-counter migration (T1.2). Bumps version 4.2.0 → 4.3.0. + +**Architecture:** Three independent additions. T1.1 adds a sourced helper (`taskmaster-verify-command.sh`) called from both stop-hook variants when the done token is seen. T1.3 introduces an explicit `[taskmaster:injected v=1 kind=...]` tag that wraps every prompt the hook injects, plus a sourced detector (`taskmaster-prompt-detect.sh`) with a legacy substring-match fallback for back-compat. T1.2 replaces the bare counter file at `${TMPDIR}/taskmaster/` with a `flock`-protected JSON file at `${TASKMASTER_STATE_DIR:-${TMPDIR}/taskmaster/state}/.json` exposed through a sourced library (`taskmaster-state.sh`); a one-time migration on first read absorbs the legacy counter and deletes the old file. + +**Tech Stack:** bash 5+, `jq`, `flock`, `timeout` (GNU coreutils), `mktemp`, plain-bash test scripts. + +**Order rationale:** Phase A first (T1.1) — smallest, no migration. Phase B (T1.3) — independent, but touches every prompt-injection site. Phase C (T1.2) — biggest change because counter usage is in three files; doing it last means we don't rewrite already-touched code twice. Phase D — version bump, CHANGELOG, tag. + +**Test invocation:** every new test is `bash tests/.test.sh` returning exit 0 on pass, non-zero on fail. Existing tests follow the same convention. + +--- + +## Pre-flight + +**Step 1: Confirm clean working tree and current version** + +Run: `git status && grep '^version:' SKILL.md` +Expected: `working tree clean` and `version: 4.2.0`. If anything modified, stop and ask the user. + +**Step 2: Confirm required tools are installed** + +Run: `which jq flock timeout mktemp` +Expected: all four resolve. If `timeout` is missing on macOS, install GNU coreutils (`brew install coreutils`) and use `gtimeout`. The plan assumes `timeout`; on macOS, swap globally before starting Phase A. + +--- + +# Phase A — T1.1: `TASKMASTER_VERIFY_COMMAND` + +### Task A1: Write the failing tests for the verify-command helper + +**Files:** +- Create: `tests/verify-command.test.sh` + +**Step 1: Write the test file** + +```bash +#!/usr/bin/env bash +# +# Tests for taskmaster-verify-command.sh. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB="$REPO_ROOT/taskmaster-verify-command.sh" + +# shellcheck disable=SC1090 +source "$LIB" + +PASS_COUNT=0 +FAIL_COUNT=0 + +assert() { + local name="$1" + local condition="$2" + if eval "$condition"; then + printf 'ok %s\n' "$name" + PASS_COUNT=$((PASS_COUNT + 1)) + else + printf 'FAIL %s\n' "$name" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +# --- Unset command is a no-op pass --- +unset TASKMASTER_VERIFY_COMMAND TASKMASTER_VERIFY_TIMEOUT TASKMASTER_VERIFY_MAX_OUTPUT TASKMASTER_VERIFY_CWD +TASKMASTER_VERIFY_OUTPUT_TAIL="" +TASKMASTER_VERIFY_EXIT_CODE="" +taskmaster_run_verify_command +assert "unset command returns 0" "[[ \"$?\" == \"0\" ]]" +assert "unset command leaves exit code blank" "[[ -z \"$TASKMASTER_VERIFY_EXIT_CODE\" ]]" + +# --- Successful command --- +TASKMASTER_VERIFY_COMMAND="true" +taskmaster_run_verify_command +rc=$? +assert "successful command returns 0" "[[ \"$rc\" == \"0\" ]]" +assert "successful command sets exit code 0" "[[ \"$TASKMASTER_VERIFY_EXIT_CODE\" == \"0\" ]]" + +# --- Failing command --- +TASKMASTER_VERIFY_COMMAND="exit 7" +set +e; taskmaster_run_verify_command; rc=$?; set -e +assert "failing command propagates exit code" "[[ \"$rc\" == \"7\" ]]" +assert "failing command captures exit code 7" "[[ \"$TASKMASTER_VERIFY_EXIT_CODE\" == \"7\" ]]" + +# --- Output captured --- +TASKMASTER_VERIFY_COMMAND='echo hello-world; echo to-stderr >&2' +taskmaster_run_verify_command +assert "stdout captured" '[[ "$TASKMASTER_VERIFY_OUTPUT_TAIL" == *hello-world* ]]' +assert "stderr captured (combined)" '[[ "$TASKMASTER_VERIFY_OUTPUT_TAIL" == *to-stderr* ]]' + +# --- Output truncation --- +TASKMASTER_VERIFY_COMMAND='yes hello | head -c 50000' +TASKMASTER_VERIFY_MAX_OUTPUT=200 +taskmaster_run_verify_command +unset TASKMASTER_VERIFY_MAX_OUTPUT +assert "output truncated to MAX_OUTPUT bytes" "[[ \"\${#TASKMASTER_VERIFY_OUTPUT_TAIL}\" -le 200 ]]" + +# --- Timeout --- +TASKMASTER_VERIFY_COMMAND='sleep 30' +TASKMASTER_VERIFY_TIMEOUT=1 +set +e; START=$(date +%s); taskmaster_run_verify_command; rc=$?; END=$(date +%s); set -e +unset TASKMASTER_VERIFY_TIMEOUT +ELAPSED=$((END - START)) +assert "timeout fires within 10s" "[[ \"$ELAPSED\" -lt 10 ]]" +assert "timeout produces non-zero exit" "[[ \"$rc\" != \"0\" ]]" + +# --- CWD respected --- +TMPCWD="$(mktemp -d)" +trap 'rm -rf "$TMPCWD"' EXIT +TASKMASTER_VERIFY_COMMAND='pwd' +TASKMASTER_VERIFY_CWD="$TMPCWD" +taskmaster_run_verify_command +unset TASKMASTER_VERIFY_CWD +TMPCWD_REAL="$(cd "$TMPCWD" && pwd -P)" +assert "cwd honored" '[[ "$TASKMASTER_VERIFY_OUTPUT_TAIL" == *"$TMPCWD_REAL"* ]]' + +printf '\n%d passed, %d failed\n' "$PASS_COUNT" "$FAIL_COUNT" +[[ "$FAIL_COUNT" == 0 ]] +``` + +**Step 2: Run test to verify it fails** + +Run: `bash tests/verify-command.test.sh` +Expected: FAIL with `taskmaster-verify-command.sh: No such file or directory` (because the lib doesn't exist yet). + +**Step 3: Commit the failing test** + +```bash +git add tests/verify-command.test.sh +git commit -m "test: add failing tests for taskmaster-verify-command lib (T1.1)" +``` + +--- + +### Task A2: Implement `taskmaster-verify-command.sh` + +**Files:** +- Create: `taskmaster-verify-command.sh` + +**Step 1: Write the helper** + +```bash +#!/usr/bin/env bash +# +# Optional shell verifier gate for the Taskmaster stop hook. +# +# When TASKMASTER_VERIFY_COMMAND is set, calling taskmaster_run_verify_command +# runs the command with a timeout, captures combined output (truncated), and +# sets: +# TASKMASTER_VERIFY_EXIT_CODE the command's exit code +# TASKMASTER_VERIFY_OUTPUT_TAIL last $TASKMASTER_VERIFY_MAX_OUTPUT bytes of output +# It returns the command's exit code (0 = pass, non-zero = block). +# When unset, returns 0 with empty fields (no-op pass). +# +# Env knobs: +# TASKMASTER_VERIFY_COMMAND command string; empty/unset = skip +# TASKMASTER_VERIFY_TIMEOUT seconds before SIGTERM (default 60); +5s grace SIGKILL +# TASKMASTER_VERIFY_MAX_OUTPUT bytes of output kept (default 4000) +# TASKMASTER_VERIFY_CWD optional cwd override +# + +taskmaster_run_verify_command() { + TASKMASTER_VERIFY_OUTPUT_TAIL="" + TASKMASTER_VERIFY_EXIT_CODE="" + + local cmd="${TASKMASTER_VERIFY_COMMAND:-}" + if [[ -z "$cmd" ]]; then + return 0 + fi + + local timeout_sec="${TASKMASTER_VERIFY_TIMEOUT:-60}" + local max_output="${TASKMASTER_VERIFY_MAX_OUTPUT:-4000}" + local cwd="${TASKMASTER_VERIFY_CWD:-}" + local out_file rc=0 + + out_file="$(mktemp "${TMPDIR:-/tmp}/taskmaster-verify.XXXXXX")" + + if [[ -n "$cwd" ]]; then + set +e + ( cd "$cwd" && timeout --kill-after=5 "$timeout_sec" bash -c "$cmd" ) \ + >"$out_file" 2>&1 + rc=$? + set -e + else + set +e + timeout --kill-after=5 "$timeout_sec" bash -c "$cmd" >"$out_file" 2>&1 + rc=$? + set -e + fi + + TASKMASTER_VERIFY_OUTPUT_TAIL="$(tail -c "$max_output" "$out_file" 2>/dev/null || true)" + TASKMASTER_VERIFY_EXIT_CODE="$rc" + + rm -f "$out_file" + return "$rc" +} +``` + +**Step 2: Make it executable** + +```bash +chmod +x taskmaster-verify-command.sh +``` + +**Step 3: Run the test to verify it passes** + +Run: `bash tests/verify-command.test.sh` +Expected: `8 passed, 0 failed`. If any test fails, fix the implementation — do NOT change the test. + +**Step 4: Commit the implementation** + +```bash +git add taskmaster-verify-command.sh +git commit -m "feat: add taskmaster-verify-command lib for shell-verifier gate (T1.1)" +``` + +--- + +### Task A3: Wire the verifier into `check-completion.sh` + +**Files:** +- Modify: `check-completion.sh` + +**Step 1: Source the helper near the top (after the compliance-prompt source)** + +Find the line that sources `taskmaster-compliance-prompt.sh` (around line 20). Immediately after it, add: + +```bash +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/taskmaster-verify-command.sh" +``` + +**Step 2: Insert the verifier call inside the `HAS_DONE_SIGNAL=true` branch** + +Find the block (around line 92): + +```bash +if [ "$HAS_DONE_SIGNAL" = true ]; then + rm -f "$COUNTER_FILE" + exit 0 +fi +``` + +Replace with: + +```bash +if [ "$HAS_DONE_SIGNAL" = true ]; then + if [ -n "${TASKMASTER_VERIFY_COMMAND:-}" ]; then + if taskmaster_run_verify_command; then + rm -f "$COUNTER_FILE" + exit 0 + else + VERIFY_REASON="TASKMASTER: verifier failed (exit=${TASKMASTER_VERIFY_EXIT_CODE}). Command: ${TASKMASTER_VERIFY_COMMAND} + +Output (last ${TASKMASTER_VERIFY_MAX_OUTPUT:-4000} bytes): +${TASKMASTER_VERIFY_OUTPUT_TAIL} + +Token alone is insufficient when a verifier is configured. Fix the failures and try again." + jq -n --arg reason "$VERIFY_REASON" '{ decision: "block", reason: $reason }' + exit 0 + fi + fi + rm -f "$COUNTER_FILE" + exit 0 +fi +``` + +**Step 3: Sanity-check the script** + +Run: `bash -n check-completion.sh` +Expected: no output (parses cleanly). + +**Step 4: Smoke test the integration manually** + +Run: +```bash +TASKMASTER_VERIFY_COMMAND="false" \ + echo '{"session_id":"smoke-A3","transcript_path":"/dev/null","last_assistant_message":"TASKMASTER_DONE::smoke-A3"}' \ + | bash check-completion.sh +``` +Expected: a JSON object with `"decision":"block"` and a `"reason"` field that contains `verifier failed (exit=1)`. + +Run again with `TASKMASTER_VERIFY_COMMAND="true"`. Expected: empty output, exit 0. + +--- + +### Task A4: Wire the verifier into `hooks/check-completion.sh` + +**Files:** +- Modify: `hooks/check-completion.sh` + +Apply the **same** two edits as A3, but the source path is `$SCRIPT_DIR/../taskmaster-verify-command.sh` (one directory up). + +**Step 1: Source the helper** + +After the existing `source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh"` line, add: + +```bash +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../taskmaster-verify-command.sh" +``` + +**Step 2: Insert the verifier call** in the same `HAS_DONE_SIGNAL=true` branch (same code as A3). + +**Step 3: Sanity-check** + +Run: `bash -n hooks/check-completion.sh` +Expected: no output. + +--- + +### Task A5: Update `install.sh` to copy and chmod the new file + +**Files:** +- Modify: `install.sh` + +**Step 1: Find the `copy_skill_files` function (around line 49) and add a `safe_copy` line for the new file** + +Locate this block: +```bash +safe_copy "$SCRIPT_DIR/taskmaster-compliance-prompt.sh" "$skill_dir/taskmaster-compliance-prompt.sh" +``` + +Add immediately below it: +```bash +safe_copy "$SCRIPT_DIR/taskmaster-verify-command.sh" "$skill_dir/taskmaster-verify-command.sh" +``` + +**Step 2: Add a `chmod +x` line** + +Locate the `chmod +x "$skill_dir/taskmaster-compliance-prompt.sh"` line. Add immediately below: +```bash +chmod +x "$skill_dir/taskmaster-verify-command.sh" +``` + +**Step 3: Sanity-check** + +Run: `bash -n install.sh` +Expected: no output. + +--- + +### Task A6: Update `uninstall.sh` to remove the new file + +**Files:** +- Modify: `uninstall.sh` + +**Step 1: Find where compliance-prompt.sh is removed and add a parallel rm for verify-command.sh.** + +Locate `rm -f "$skill_dir/taskmaster-compliance-prompt.sh"` (or grep for `taskmaster-compliance-prompt.sh` in `uninstall.sh`). Add immediately below: + +```bash +rm -f "$skill_dir/taskmaster-verify-command.sh" +``` + +**Step 2: Sanity-check** + +Run: `bash -n uninstall.sh` + +--- + +### Task A7: Document the new env vars in `docs/SPEC.md` + +**Files:** +- Modify: `docs/SPEC.md` + +**Step 1: Find the "Configuration" section (section 5) and add a subsection for verify-command env vars.** + +Add at the end of the configuration section: + +```markdown +### 5.x Optional verifier command + +| Env var | Default | Meaning | +|---|---|---| +| `TASKMASTER_VERIFY_COMMAND` | unset | Shell command run when the done token is seen. Empty/unset = skip. | +| `TASKMASTER_VERIFY_TIMEOUT` | `60` | Seconds before SIGTERM, +5s grace before SIGKILL. | +| `TASKMASTER_VERIFY_MAX_OUTPUT` | `4000` | Bytes of combined stdout+stderr echoed back into the block reason. | +| `TASKMASTER_VERIFY_CWD` | unset | If set, `cd` here before invoking. Else inherit hook's cwd. | + +When `TASKMASTER_VERIFY_COMMAND` is set, stop is allowed only when (a) the +done token is present **and** (b) the command exits 0. A failing verifier +overrides token-based completion and blocks with the command's exit code and +truncated output. + +The verifier runs **only** when the done token is present, not on every stop +attempt — this keeps slow verifiers (test suites, builds) from gating +mid-work stop attempts. +``` + +(Replace `5.x` with the next available subsection number when adding.) + +--- + +### Task A8: Phase A end-to-end run + commit + +**Step 1: Run all tests** + +Run: `bash tests/verify-command.test.sh` +Expected: `8 passed, 0 failed`. + +**Step 2: Confirm syntax across all touched scripts** + +Run: `bash -n check-completion.sh hooks/check-completion.sh install.sh uninstall.sh taskmaster-verify-command.sh` +Expected: no output. + +**Step 3: Commit Phase A integration** + +```bash +git add check-completion.sh hooks/check-completion.sh install.sh uninstall.sh docs/SPEC.md +git commit -m "feat: gate stop on TASKMASTER_VERIFY_COMMAND when token present (T1.1) + +When TASKMASTER_VERIFY_COMMAND is set, the stop hook runs the command +after the done token is detected. Exit 0 allows stop; non-zero blocks +with a truncated output dump. Verifier only fires when the token is +present, so mid-work stop attempts don't pay the cost of a slow verifier." +``` + +--- + +# Phase B — T1.3: Tagged hook-internal-prompt detection + +### Task B1: Write the failing tests for the prompt-detect helper + +**Files:** +- Create: `tests/prompt-detect.test.sh` + +**Step 1: Write the test file** + +```bash +#!/usr/bin/env bash +# +# Tests for taskmaster-prompt-detect.sh. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB="$REPO_ROOT/taskmaster-prompt-detect.sh" + +# shellcheck disable=SC1090 +source "$LIB" + +PASS_COUNT=0 +FAIL_COUNT=0 + +assert_detected() { + local name="$1" + local text="$2" + if is_taskmaster_injected_prompt "$text"; then + printf 'ok %s\n' "$name" + PASS_COUNT=$((PASS_COUNT + 1)) + else + printf 'FAIL %s\n' "$name" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +assert_not_detected() { + local name="$1" + local text="$2" + if is_taskmaster_injected_prompt "$text"; then + printf 'FAIL %s (false positive)\n' "$name" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + else + printf 'ok %s\n' "$name" + PASS_COUNT=$((PASS_COUNT + 1)) + fi +} + +# --- Tag detection --- +assert_detected "tagged stop-block" \ + "[taskmaster:injected v=1 kind=stop-block] +TASKMASTER (1): Stop is blocked..." + +assert_detected "tagged followup" \ + "[taskmaster:injected v=1 kind=followup] +continue" + +assert_detected "tagged compliance" "[taskmaster:injected v=1 kind=compliance]" + +# --- Forward-compat: future schema version still detected --- +assert_detected "future schema v=99" "[taskmaster:injected v=99 kind=anything]" + +# --- Legacy substring matches (back-compat with mickn's prompts and our own) --- +assert_detected "legacy: ..." +assert_detected "legacy: Stop is blocked" "Stop is blocked until completion is explicitly confirmed." +assert_detected "legacy: TASKMASTER (N) label" "TASKMASTER (5/100): Stop is blocked..." +assert_detected "legacy: TASKMASTER (N) label, no max" "TASKMASTER (5): Stop is blocked..." +assert_detected "legacy: Goal not yet verified complete" "Goal not yet verified complete." +assert_detected "legacy: Recent tool errors were detected" "Recent tool errors were detected." + +# --- Negatives --- +assert_not_detected "empty string" "" +assert_not_detected "real user prompt" "fix the failing test in foo_test.go" +assert_not_detected "user mentions taskmaster word" "I want to use taskmaster for this project" +assert_not_detected "tag-like but malformed" "[taskmaster:injected]" +assert_not_detected "tag-like but missing v=" "[taskmaster:injected kind=stop-block]" + +# --- generate_taskmaster_injected_tag produces a parseable tag --- +TAG="$(generate_taskmaster_injected_tag stop-block)" +assert_detected "generated tag is detectable" "$TAG" +[[ "$TAG" == "[taskmaster:injected v=1 kind=stop-block]" ]] && { + printf 'ok generated tag exact format\n'; PASS_COUNT=$((PASS_COUNT + 1)); +} || { + printf 'FAIL generated tag exact format (got: %s)\n' "$TAG" >&2; FAIL_COUNT=$((FAIL_COUNT + 1)); +} + +printf '\n%d passed, %d failed\n' "$PASS_COUNT" "$FAIL_COUNT" +[[ "$FAIL_COUNT" == 0 ]] +``` + +**Step 2: Run to verify it fails** + +Run: `bash tests/prompt-detect.test.sh` +Expected: FAIL with `taskmaster-prompt-detect.sh: No such file or directory`. + +**Step 3: Commit failing tests** + +```bash +git add tests/prompt-detect.test.sh +git commit -m "test: add failing tests for taskmaster-prompt-detect lib (T1.3)" +``` + +--- + +### Task B2: Implement `taskmaster-prompt-detect.sh` + +**Files:** +- Create: `taskmaster-prompt-detect.sh` + +**Step 1: Write the lib** + +```bash +#!/usr/bin/env bash +# +# Detect prompts that Taskmaster itself injected, so they don't get +# treated as fresh user goals by downstream consumers (T2.2 user-prompt +# capture, T3 verifier). +# +# Two-tier detection: +# 1. Forward path: explicit `[taskmaster:injected v= kind=]` tag +# on the first non-empty line. Forward-compatible across schema bumps. +# 2. Legacy fallback: substring match against known wording from this +# project and from mickn/taskmaster's fork. +# + +readonly TASKMASTER_INJECTED_TAG_VERSION=1 + +# Emit the canonical tag for a given kind. Caller prepends to their prompt. +# Kinds: stop-block, followup, compliance, session-start, verifier-feedback. +generate_taskmaster_injected_tag() { + local kind="${1:-unknown}" + printf '[taskmaster:injected v=%d kind=%s]' \ + "$TASKMASTER_INJECTED_TAG_VERSION" "$kind" +} + +is_taskmaster_injected_tag_line() { + local text="$1" + # Match `[taskmaster:injected v= kind=]` at start of text. + [[ "$text" =~ ^\[taskmaster:injected[[:space:]]v=[0-9]+[[:space:]]kind=[a-zA-Z0-9_-]+\] ]] +} + +is_taskmaster_legacy_injected_prompt() { + local text="$1" + case "$text" in + "] + +``` + +`` ∈ `stop-block | followup | compliance | session-start | verifier-feedback`. + +Downstream consumers (UserPromptSubmit hook, completion verifier, external +tooling) detect injected prompts via `is_taskmaster_injected_prompt` from +`taskmaster-prompt-detect.sh`. Legacy substring detection is preserved for +prompts emitted before this version. +``` + +**Step 2: Add a brief mention in `SKILL.md`** + +In the SKILL.md system context, after the "How It Works" section, add a short paragraph (helps the model treat the tag as metadata, not directive): + +```markdown +## A note on the injected-prompt tag + +If you see a line starting with `[taskmaster:injected v=…]` at the top of a +message, that's metadata the hook adds to its own prompts. Treat it as a +marker, not as content you need to act on. +``` + +--- + +### Task B7: Update `install.sh` and `uninstall.sh` for the new file + +**Files:** +- Modify: `install.sh` +- Modify: `uninstall.sh` + +**Step 1: install.sh** — add `safe_copy` and `chmod +x` for `taskmaster-prompt-detect.sh` parallel to the changes in A5. + +**Step 2: uninstall.sh** — add `rm -f` parallel to A6. + +**Step 3: Sanity-check** + +Run: `bash -n install.sh uninstall.sh` + +--- + +### Task B8: Phase B end-to-end + commit + +**Step 1: Run all tests** + +Run: `bash tests/prompt-detect.test.sh && bash tests/verify-command.test.sh` +Expected: both pass. + +**Step 2: Smoke test the tag is present in real hook output** + +Run: +```bash +echo '{"session_id":"smoke-B8","transcript_path":"/dev/null","last_assistant_message":""}' \ + | bash check-completion.sh \ + | jq -r .reason \ + | head -3 +``` +Expected: first line is `[taskmaster:injected v=1 kind=stop-block]`. + +**Step 3: Commit Phase B** + +```bash +git add check-completion.sh hooks/check-completion.sh hooks/inject-continue-codex.sh \ + install.sh uninstall.sh docs/SPEC.md SKILL.md +git commit -m "feat: tag every hook-injected prompt with [taskmaster:injected v=1 kind=...] (T1.3) + +Adds an explicit single-line marker to the top of every prompt the hook +injects. Downstream consumers detect injected prompts via the new +taskmaster-prompt-detect lib (forward path: tag match; back-compat: +legacy substring match against current and mickn/taskmaster wording)." +``` + +--- + +# Phase C — T1.2: JSON state-file layout + +### Task C1: Write the failing tests for the state lib + +**Files:** +- Create: `tests/state.test.sh` + +**Step 1: Write the test file** + +```bash +#!/usr/bin/env bash +# +# Tests for taskmaster-state.sh. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB="$REPO_ROOT/taskmaster-state.sh" + +# Isolate state under a temp dir +TEST_HOME="$(mktemp -d "${TMPDIR:-/tmp}/taskmaster-state-test.XXXXXX")" +trap 'rm -rf "$TEST_HOME"' EXIT +export TASKMASTER_STATE_DIR="$TEST_HOME/state" + +# shellcheck disable=SC1090 +source "$LIB" + +PASS=0; FAIL=0 +ok() { printf 'ok %s\n' "$1"; PASS=$((PASS+1)); } +fail() { printf 'FAIL %s\n' "$1" >&2; FAIL=$((FAIL+1)); } + +# --- init creates well-formed JSON with schema_version=1 --- +SID="sess-$$" +taskmaster_state_init "$SID" +PATH_OUT="$(taskmaster_state_path "$SID")" +[[ -f "$PATH_OUT" ]] && ok "init creates file" || fail "init creates file" +SV="$(jq -r .schema_version <"$PATH_OUT")" +[[ "$SV" == "1" ]] && ok "schema_version is 1" || fail "schema_version is 1 (got $SV)" +SI="$(jq -r .session_id <"$PATH_OUT")" +[[ "$SI" == "$SID" ]] && ok "session_id stamped" || fail "session_id stamped" +SC="$(jq -r .stop_count <"$PATH_OUT")" +[[ "$SC" == "0" ]] && ok "stop_count starts at 0" || fail "stop_count starts at 0 (got $SC)" + +# --- increment --- +taskmaster_state_increment_stop_count "$SID" +SC="$(jq -r .stop_count <"$PATH_OUT")" +[[ "$SC" == "1" ]] && ok "stop_count after one increment is 1" || fail "increment 1 (got $SC)" + +taskmaster_state_increment_stop_count "$SID" +taskmaster_state_increment_stop_count "$SID" +SC="$(jq -r .stop_count <"$PATH_OUT")" +[[ "$SC" == "3" ]] && ok "stop_count after three increments is 3" || fail "increment 3 (got $SC)" + +# --- concurrent increments --- +SID2="sess-conc-$$" +taskmaster_state_init "$SID2" +PATH_C="$(taskmaster_state_path "$SID2")" +N=50 +for i in $(seq 1 "$N"); do + ( taskmaster_state_increment_stop_count "$SID2" ) & +done +wait +SC="$(jq -r .stop_count <"$PATH_C")" +[[ "$SC" == "$N" ]] && ok "concurrent $N increments reach $N" \ + || fail "concurrent increments lost some (got $SC, expected $N)" + +# --- legacy migration --- +LEGACY_DIR="${TMPDIR:-/tmp}/taskmaster" +mkdir -p "$LEGACY_DIR" +SID3="sess-legacy-$$" +LEGACY_FILE="$LEGACY_DIR/$SID3" +echo "7" > "$LEGACY_FILE" + +# State doesn't exist yet; migration should pull the 7 +taskmaster_state_migrate_legacy_counter "$SID3" +[[ -f "$(taskmaster_state_path "$SID3")" ]] && ok "legacy migration creates state file" \ + || fail "legacy migration creates state file" +SC="$(jq -r .stop_count <"$(taskmaster_state_path "$SID3")")" +[[ "$SC" == "7" ]] && ok "legacy counter value migrated" \ + || fail "legacy counter value migrated (got $SC, expected 7)" +[[ ! -f "$LEGACY_FILE" ]] && ok "legacy file deleted after migration" \ + || fail "legacy file deleted after migration" + +# --- migration is idempotent (rerun without legacy file is a no-op) --- +taskmaster_state_migrate_legacy_counter "$SID3" +SC="$(jq -r .stop_count <"$(taskmaster_state_path "$SID3")")" +[[ "$SC" == "7" ]] && ok "second migration call is a no-op" \ + || fail "second migration mutated state (got $SC)" + +# --- jq read of nonexistent path returns null --- +SID4="sess-empty-$$" +VAL="$(taskmaster_state_jq "$SID4" '.latest_user_prompt.prompt' 2>/dev/null || echo "MISSING")" +[[ "$VAL" == "null" || -z "$VAL" || "$VAL" == "MISSING" ]] && ok "nonexistent path read is safe" \ + || fail "nonexistent path read is safe (got $VAL)" + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +[[ "$FAIL" == 0 ]] +``` + +**Step 2: Run to verify failure** + +Run: `bash tests/state.test.sh` +Expected: FAIL with `taskmaster-state.sh: No such file or directory`. + +**Step 3: Commit failing tests** + +```bash +git add tests/state.test.sh +git commit -m "test: add failing tests for taskmaster-state lib (T1.2)" +``` + +--- + +### Task C2: Implement `taskmaster-state.sh` + +**Files:** +- Create: `taskmaster-state.sh` + +**Step 1: Write the lib** + +```bash +#!/usr/bin/env bash +# +# Persistent JSON session state for Taskmaster. +# +# Layout: ${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}/.json +# +# Schema (v1): +# { +# "schema_version": 1, +# "session_id": "", +# "created_at": "", +# "updated_at": "", +# "stop_count": 0, +# "latest_user_prompt": null | {captured_at, turn_id, prompt}, +# "last_verifier_run": null | {ran_at, input_hash, complete, reason, next_action}, +# "metadata": {} +# } +# +# Atomicity: all writes go through tmp+mv guarded by flock on .lock. +# + +taskmaster_state_dir() { + printf '%s\n' "${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}" +} + +taskmaster_state_path() { + local sid="$1" + printf '%s/%s.json\n' "$(taskmaster_state_dir)" "$sid" +} + +taskmaster_state_now() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +taskmaster_state_init() { + local sid="$1" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + mkdir -p "$(dirname "$path")" + [[ -f "$path" ]] && return 0 + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + if [[ ! -f "$path" ]]; then + jq -n \ + --arg sid "$sid" \ + --arg now "$now" \ + '{ + schema_version: 1, + session_id: $sid, + created_at: $now, + updated_at: $now, + stop_count: 0, + latest_user_prompt: null, + last_verifier_run: null, + metadata: {} + }' >"$tmp" + mv "$tmp" "$path" + fi + exec 9>&- +} + +taskmaster_state_jq() { + local sid="$1" expr="$2" + local path + path="$(taskmaster_state_path "$sid")" + [[ -f "$path" ]] || return 0 + jq -r "$expr" <"$path" 2>/dev/null +} + +# Run jq with a transformation expression and atomically write the result back. +taskmaster_state_update() { + local sid="$1" expr="$2" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + taskmaster_state_init "$sid" + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + jq --arg now "$now" "$expr | .updated_at = \$now" "$path" >"$tmp" + mv "$tmp" "$path" + exec 9>&- +} + +taskmaster_state_increment_stop_count() { + local sid="$1" + taskmaster_state_update "$sid" '.stop_count = (.stop_count + 1)' +} + +taskmaster_state_capture_prompt() { + local sid="$1" turn_id="$2" prompt="$3" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + taskmaster_state_init "$sid" + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + jq \ + --arg now "$now" \ + --arg turn "$turn_id" \ + --arg prompt "$prompt" \ + '.latest_user_prompt = {captured_at: $now, turn_id: $turn, prompt: $prompt} + | .updated_at = $now' \ + "$path" >"$tmp" + mv "$tmp" "$path" + exec 9>&- +} + +taskmaster_state_record_verifier_run() { + local sid="$1" input_hash="$2" complete="$3" reason="$4" next_action="$5" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + taskmaster_state_init "$sid" + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + jq \ + --arg now "$now" \ + --arg hash "$input_hash" \ + --argjson complete "$complete" \ + --arg reason "$reason" \ + --arg next "$next_action" \ + '.last_verifier_run = { + ran_at: $now, + input_hash: $hash, + complete: $complete, + reason: $reason, + next_action: $next + } | .updated_at = $now' \ + "$path" >"$tmp" + mv "$tmp" "$path" + exec 9>&- +} + +# One-time migration: absorb legacy ${TMPDIR}/taskmaster/ counter into the +# state file's stop_count, then delete the legacy file. Idempotent — safe to +# call on every hook entry. +taskmaster_state_migrate_legacy_counter() { + local sid="$1" + local legacy="${TMPDIR:-/tmp}/taskmaster/${sid}" + [[ -f "$legacy" ]] || return 0 + + local count + count="$(cat "$legacy" 2>/dev/null || echo 0)" + [[ "$count" =~ ^[0-9]+$ ]] || count=0 + + taskmaster_state_init "$sid" + taskmaster_state_update "$sid" ".stop_count = $count" + rm -f "$legacy" +} +``` + +**Step 2: Make executable** + +```bash +chmod +x taskmaster-state.sh +``` + +**Step 3: Run tests** + +Run: `bash tests/state.test.sh` +Expected: all pass. Common failure modes: +- macOS `flock` not in PATH — install `flock` from coreutils, or skip the concurrent test on systems without it. +- `mkdir -p` race — already handled by `taskmaster_state_init`. + +**Step 4: Commit** + +```bash +git add taskmaster-state.sh +git commit -m "feat: add taskmaster-state JSON state lib with flock + atomic writes (T1.2)" +``` + +--- + +### Task C3: Refactor `check-completion.sh` to use the state lib + +**Files:** +- Modify: `check-completion.sh` + +**Step 1: Source the lib** + +After existing `source` calls, add: + +```bash +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/taskmaster-state.sh" +``` + +**Step 2: Replace the counter logic** + +Locate the existing block: + +```bash +# --- counter --- +COUNTER_DIR="${TMPDIR:-/tmp}/taskmaster" +mkdir -p "$COUNTER_DIR" +COUNTER_FILE="${COUNTER_DIR}/${SESSION_ID}" +MAX=${TASKMASTER_MAX:-100} + +COUNT=0 +if [ -f "$COUNTER_FILE" ]; then + COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0") +fi +``` + +Replace with: + +```bash +# --- counter (state-file backed) --- +taskmaster_state_migrate_legacy_counter "$SESSION_ID" +taskmaster_state_init "$SESSION_ID" + +MAX=${TASKMASTER_MAX:-100} +COUNT="$(taskmaster_state_jq "$SESSION_ID" '.stop_count')" +[[ "$COUNT" =~ ^[0-9]+$ ]] || COUNT=0 +``` + +**Step 3: Replace `rm -f "$COUNTER_FILE"` (allow-stop paths) with state reset** + +Find every occurrence of `rm -f "$COUNTER_FILE"` (there are two: HAS_DONE_SIGNAL=true branch, and MAX-reached branch). Replace each with: + +```bash +taskmaster_state_update "$SESSION_ID" '.stop_count = 0' +``` + +**Step 4: Replace `echo "$NEXT" > "$COUNTER_FILE"` with state increment** + +Find: + +```bash +NEXT=$((COUNT + 1)) +echo "$NEXT" > "$COUNTER_FILE" +``` + +Replace with: + +```bash +taskmaster_state_increment_stop_count "$SESSION_ID" +NEXT=$((COUNT + 1)) +``` + +(`NEXT` is still computed locally for the LABEL string; the source of truth is the state file.) + +**Step 5: Sanity-check** + +Run: `bash -n check-completion.sh` +Expected: no output. + +**Step 6: Smoke test** + +Run: +```bash +TASKMASTER_STATE_DIR="$(mktemp -d)/state" \ + echo '{"session_id":"smoke-C3","transcript_path":"/dev/null","last_assistant_message":""}' \ + | bash check-completion.sh \ + | jq -r .reason | head -2 +echo "---" +ls "$TASKMASTER_STATE_DIR" +cat "$TASKMASTER_STATE_DIR"/smoke-C3.json | jq . +``` +Expected: tag line + `TASKMASTER (1/100): ...`; state file shows `stop_count: 1`. + +--- + +### Task C4: Refactor `hooks/check-completion.sh` similarly + +**Files:** +- Modify: `hooks/check-completion.sh` + +Apply the **same** four edits as C3. Source path is `$SCRIPT_DIR/../taskmaster-state.sh`. + +**Step 1: Source the lib** (with `..` prefix). + +**Step 2–4: Same replacements as C3.** + +**Step 5: Sanity-check + smoke test** (same probe). + +--- + +### Task C5: Refactor `hooks/inject-continue-codex.sh` to use state lib + +**Files:** +- Modify: `hooks/inject-continue-codex.sh` + +The injector tracks injection counts in a runtime file already; this task is **purely additive** — also write to the JSON state when we have a session id, so that future tooling can read both. Do not break the existing injector state file. + +**Step 1: Source the lib** + +After existing sources, add: + +```bash +# shellcheck disable=SC1091 +source "$(dirname "${BASH_SOURCE[0]}")/../taskmaster-state.sh" +``` + +**Step 2: In `inject_prompt`, also bump the JSON state's stop_count when SESSION_ID is known** + +Find the existing increment line: + +```bash +INJECTION_COUNT=$((INJECTION_COUNT + 1)) +``` + +Add immediately after: + +```bash +if [[ -n "${SESSION_ID:-}" ]]; then + taskmaster_state_increment_stop_count "$SESSION_ID" 2>/dev/null || true +fi +``` + +(`|| true` because the injector's startup ordering can call `inject_prompt` very early; we don't want a state-file write failure to crash the injector.) + +**Step 3: Sanity-check** + +Run: `bash -n hooks/inject-continue-codex.sh` +Expected: no output. + +--- + +### Task C6: Update `install.sh` and `uninstall.sh` for the state lib + +**Files:** +- Modify: `install.sh` +- Modify: `uninstall.sh` + +**Step 1: install.sh** — `safe_copy` and `chmod +x` for `taskmaster-state.sh` parallel to A5/B7. + +**Step 2: uninstall.sh** — `rm -f` parallel to A6/B7. + +**Step 3: Sanity-check** + +Run: `bash -n install.sh uninstall.sh` + +--- + +### Task C7: Document the state file in `docs/SPEC.md` + +**Files:** +- Modify: `docs/SPEC.md` + +**Step 1: Add a "Session state" subsection to the Architecture section.** + +```markdown +### 3.x Session state file + +Path: `${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}/.json` + +Schema (v1): + +```json +{ + "schema_version": 1, + "session_id": "", + "created_at": "", + "updated_at": "", + "stop_count": 0, + "latest_user_prompt": null, + "last_verifier_run": null, + "metadata": {} +} +``` + +All writes go through `flock` on `.lock` and atomic tmp+mv. + +**Legacy migration:** on first read per session, the hook absorbs any +existing counter file at `${TMPDIR}/taskmaster/` into +`stop_count` and deletes the legacy file. Idempotent. +``` + +--- + +### Task C8: Phase C end-to-end + commit + +**Step 1: Run all three test suites** + +Run: `bash tests/state.test.sh && bash tests/prompt-detect.test.sh && bash tests/verify-command.test.sh` +Expected: all pass. + +**Step 2: Verify legacy migration works against a real legacy file** + +Run: +```bash +LEGACY_DIR="${TMPDIR:-/tmp}/taskmaster" +mkdir -p "$LEGACY_DIR" +echo "5" > "$LEGACY_DIR/migrate-c8" +TASKMASTER_STATE_DIR="$(mktemp -d)/state" \ + echo '{"session_id":"migrate-c8","transcript_path":"/dev/null","last_assistant_message":""}' \ + | bash check-completion.sh >/dev/null +# After hook fires: legacy file should be gone, state file should have stop_count = 6 (5 migrated + 1 increment) +ls "$LEGACY_DIR/migrate-c8" 2>&1 | grep -q "No such file" && echo "ok: legacy file removed" +jq -r '.stop_count' "$TASKMASTER_STATE_DIR/migrate-c8.json" +``` +Expected: `ok: legacy file removed` and `stop_count: 6`. + +**Step 3: Sanity-check all touched scripts** + +Run: `bash -n check-completion.sh hooks/check-completion.sh hooks/inject-continue-codex.sh install.sh uninstall.sh taskmaster-state.sh` +Expected: no output. + +**Step 4: Commit Phase C** + +```bash +git add check-completion.sh hooks/check-completion.sh hooks/inject-continue-codex.sh \ + install.sh uninstall.sh docs/SPEC.md +git commit -m "feat: replace counter file with JSON state file + flock + migration (T1.2) + +stop_count now lives in a flock-protected JSON file at +\$TASKMASTER_STATE_DIR/.json (default: \$TMPDIR/taskmaster/state). +Legacy counter files are absorbed on first read and deleted. Schema is +versioned for forward compatibility with T2/T3 fields (latest_user_prompt, +last_verifier_run)." +``` + +--- + +# Phase D — Release plumbing + +### Task D1: Bump version to 4.3.0 + +**Files:** +- Modify: `SKILL.md` + +**Step 1: Bump the `version:` line in the YAML frontmatter from `4.2.0` to `4.3.0`.** + +Run: `sed -i 's/^version: 4\.2\.0$/version: 4.3.0/' SKILL.md && grep '^version:' SKILL.md` +Expected: `version: 4.3.0`. + +--- + +### Task D2: Bump version in `docs/SPEC.md` + +**Files:** +- Modify: `docs/SPEC.md` + +**Step 1: Update the `**Version**:` line at the top of SPEC.md from `4.2.0` to `4.3.0`.** + +Use `Edit` tool — match exactly to avoid clobbering similar lines. + +--- + +### Task D3: Add a CHANGELOG.md entry for 4.3.0 + +**Files:** +- Modify: `CHANGELOG.md` (created during the rebase from upstream) + +**Step 1: Insert a new section at the top of the file (above the existing `## v2.3.0` entry):** + +```markdown +## v4.3.0 — 2026-04-28 + +### Added +- `TASKMASTER_VERIFY_COMMAND` env var: opt-in shell verifier that gates + stop after the done token is seen. Pairs with test suites, type-checkers, + or any repo-local check. Companion knobs: `TASKMASTER_VERIFY_TIMEOUT` + (default 60s), `TASKMASTER_VERIFY_MAX_OUTPUT` (default 4000 bytes), + `TASKMASTER_VERIFY_CWD`. (T1.1) +- Tagged hook-injected prompts: every prompt the hook injects starts + with `[taskmaster:injected v=1 kind=]`. New + `taskmaster-prompt-detect.sh` lib lets downstream consumers + distinguish injected reprompts from real user goals. Legacy substring + detection preserved for back-compat. (T1.3) +- JSON session state file at + `${TASKMASTER_STATE_DIR:-${TMPDIR}/taskmaster/state}/.json`, + flock-protected, atomic writes. Schema v1 with `stop_count`, + `latest_user_prompt`, `last_verifier_run`, `metadata` fields ready for + T2/T3. (T1.2) + +### Changed +- Stop-count tracking moved from the bare counter file at + `${TMPDIR}/taskmaster/` to the new JSON state file. + Legacy counter files are absorbed on first read and deleted — + no user action required. + +### References +- Design: `docs/designs/2026-04-28-072245-fork-pattern-adoption.md` +- Plan: `docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md` +- Source review: `docs/upstream-reviews/blader-taskmaster-forks.md` +``` + +--- + +### Task D4: Final test run + +**Step 1: Run every test in `tests/`** + +Run: `for t in tests/*.test.sh; do echo "=== $t ==="; bash "$t" || exit 1; done && echo "ALL TESTS PASS"` +Expected: `ALL TESTS PASS`. + +**Step 2: Smoke test the full hook with all three features active** + +Run: +```bash +TM_STATE="$(mktemp -d)/state" +TASKMASTER_STATE_DIR="$TM_STATE" \ +TASKMASTER_VERIFY_COMMAND="true" \ + echo '{"session_id":"final-smoke","transcript_path":"/dev/null","last_assistant_message":"TASKMASTER_DONE::final-smoke"}' \ + | bash check-completion.sh +echo "exit=$?" +ls "$TM_STATE" +jq . "$TM_STATE/final-smoke.json" +``` +Expected: empty output (allow stop), `exit=0`, state file shows the session was tracked. (`stop_count` should be 0 because the done token short-circuited before the increment, then was reset.) + +Repeat with `TASKMASTER_VERIFY_COMMAND="false"`. Expected: blocking JSON output with `verifier failed (exit=1)` in the reason. + +--- + +### Task D5: Commit version bump and CHANGELOG + +**Step 1: Stage and commit** + +```bash +git add SKILL.md docs/SPEC.md CHANGELOG.md +git commit -m "release v4.3.0: T1 fork-pattern adoption (verify-command, tag, state-file) + +See CHANGELOG.md for the full entry. Three independent additions +ported from the fork-network review (mickn/taskmaster), each +opt-in or backward-compatible." +``` + +**Step 2: Tag the release** + +```bash +git tag -a v4.3.0 -m "v4.3.0: T1 fork-pattern adoption (T1.1 verify-command, T1.2 state-file, T1.3 prompt tag)" +git tag -n v4.3.0 +``` + +**Step 3: Confirm clean state** + +Run: `git status && git log --oneline -10` +Expected: `working tree clean`; the last seven commits trace the plan (test, impl, test, impl, test, impl, release). + +--- + +# Out of scope for this plan + +The design doc lists items deliberately deferred: + +- Stale state-file cleanup / TTL — separate beads issue. +- Native Codex hooks (T2) — gated on capability probe. +- Semantic completion verifier (T3) — opt-in, ships after T2. + +# Risks watched during execution + +- **macOS `flock` / `timeout` availability**: if the executing engineer is on macOS without GNU coreutils, install before starting (`brew install coreutils flock`) or stop and ask the user. +- **State-dir collision with parallel sessions**: the `flock` per file makes this safe, but if `$TASKMASTER_STATE_DIR` lives on a network filesystem with broken locking, concurrency tests will flake. Document this as a known caveat in SPEC if encountered. +- **Smoke tests that depend on `last_assistant_message` shape**: the field is what Claude Code passes; if the engineer is testing with a different runtime that uses a different field, override the test JSON shape accordingly. From 1d824301371fac215026b5b4e4bf6215dbd5948e Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 02:12:54 -0700 Subject: [PATCH 19/41] docs: add session summary (docs-add-implementation-plan-f) --- ...8-021254-docs-add-implementation-plan-f.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/session-summaries/2026-04-28-021254-docs-add-implementation-plan-f.md diff --git a/docs/session-summaries/2026-04-28-021254-docs-add-implementation-plan-f.md b/docs/session-summaries/2026-04-28-021254-docs-add-implementation-plan-f.md new file mode 100644 index 0000000..8954afb --- /dev/null +++ b/docs/session-summaries/2026-04-28-021254-docs-add-implementation-plan-f.md @@ -0,0 +1,39 @@ +# Session Summary + +**Date:** 2026-04-28 +**Time:** 02:12 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 3 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `dc503c9` - docs: add implementation plan for T1 fork-pattern adoption +- `4751e4b` - Add report: Codex native hooks verification before mickn port +- `bd48770` - docs: add design for fork-pattern adoption (T1-T3) + +## Key Changes + +### Files Modified +- `LESSONS.md` +- `docs/blog/2026-02-25-taskmaster-hook-cleanup.md` +- `docs/designs/2026-04-28-072245-fork-pattern-adoption.md` +- `docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md` +- `docs/reports/2026-04-28-002421-codex-native-hooks-verification.md` +- `docs/reports/2026-04-28-002421-codex-native-hooks-verification.pdf` +- `docs/reports/2026-04-28-002421-codex-native-hooks-verification.tex` +- `docs/session-summaries/2026-04-27-235723-fix-expand-tilde-in-transcript.md` +- `docs/session-summaries/2026-04-28-000126-docs-add-session-summary-fix-e.md` +- `docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md` +- `docs/upstream-reviews/blader-taskmaster-forks.md` + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From e0736657133143d6b6c087e07e831a531f1c801f Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 02:47:57 -0700 Subject: [PATCH 20/41] test: add failing tests for taskmaster-verify-command lib (T1.1) --- tests/verify-command.test.sh | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/verify-command.test.sh diff --git a/tests/verify-command.test.sh b/tests/verify-command.test.sh new file mode 100644 index 0000000..81c2767 --- /dev/null +++ b/tests/verify-command.test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# Tests for taskmaster-verify-command.sh. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB="$REPO_ROOT/taskmaster-verify-command.sh" + +# shellcheck disable=SC1090 +source "$LIB" + +PASS_COUNT=0 +FAIL_COUNT=0 + +assert() { + local name="$1" + local condition="$2" + if eval "$condition"; then + printf 'ok %s\n' "$name" + PASS_COUNT=$((PASS_COUNT + 1)) + else + printf 'FAIL %s\n' "$name" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +# --- Unset command is a no-op pass --- +unset TASKMASTER_VERIFY_COMMAND TASKMASTER_VERIFY_TIMEOUT TASKMASTER_VERIFY_MAX_OUTPUT TASKMASTER_VERIFY_CWD +TASKMASTER_VERIFY_OUTPUT_TAIL="" +TASKMASTER_VERIFY_EXIT_CODE="" +taskmaster_run_verify_command +assert "unset command returns 0" "[[ \"$?\" == \"0\" ]]" +assert "unset command leaves exit code blank" "[[ -z \"$TASKMASTER_VERIFY_EXIT_CODE\" ]]" + +# --- Successful command --- +TASKMASTER_VERIFY_COMMAND="true" +taskmaster_run_verify_command +rc=$? +assert "successful command returns 0" "[[ \"$rc\" == \"0\" ]]" +assert "successful command sets exit code 0" "[[ \"$TASKMASTER_VERIFY_EXIT_CODE\" == \"0\" ]]" + +# --- Failing command --- +TASKMASTER_VERIFY_COMMAND="exit 7" +set +e; taskmaster_run_verify_command; rc=$?; set -e +assert "failing command propagates exit code" "[[ \"$rc\" == \"7\" ]]" +assert "failing command captures exit code 7" "[[ \"$TASKMASTER_VERIFY_EXIT_CODE\" == \"7\" ]]" + +# --- Output captured --- +TASKMASTER_VERIFY_COMMAND='echo hello-world; echo to-stderr >&2' +taskmaster_run_verify_command +assert "stdout captured" '[[ "$TASKMASTER_VERIFY_OUTPUT_TAIL" == *hello-world* ]]' +assert "stderr captured (combined)" '[[ "$TASKMASTER_VERIFY_OUTPUT_TAIL" == *to-stderr* ]]' + +# --- Output truncation --- +TASKMASTER_VERIFY_COMMAND='yes hello | head -c 50000' +TASKMASTER_VERIFY_MAX_OUTPUT=200 +taskmaster_run_verify_command +unset TASKMASTER_VERIFY_MAX_OUTPUT +assert "output truncated to MAX_OUTPUT bytes" "[[ \"\${#TASKMASTER_VERIFY_OUTPUT_TAIL}\" -le 200 ]]" + +# --- Timeout --- +TASKMASTER_VERIFY_COMMAND='sleep 30' +TASKMASTER_VERIFY_TIMEOUT=1 +set +e; START=$(date +%s); taskmaster_run_verify_command; rc=$?; END=$(date +%s); set -e +unset TASKMASTER_VERIFY_TIMEOUT +ELAPSED=$((END - START)) +assert "timeout fires within 10s" "[[ \"$ELAPSED\" -lt 10 ]]" +assert "timeout produces non-zero exit" "[[ \"$rc\" != \"0\" ]]" + +# --- CWD respected --- +TMPCWD="$(mktemp -d)" +trap 'rm -rf "$TMPCWD"' EXIT +TASKMASTER_VERIFY_COMMAND='pwd' +TASKMASTER_VERIFY_CWD="$TMPCWD" +taskmaster_run_verify_command +unset TASKMASTER_VERIFY_CWD +TMPCWD_REAL="$(cd "$TMPCWD" && pwd -P)" +assert "cwd honored" '[[ "$TASKMASTER_VERIFY_OUTPUT_TAIL" == *"$TMPCWD_REAL"* ]]' + +printf '\n%d passed, %d failed\n' "$PASS_COUNT" "$FAIL_COUNT" +[[ "$FAIL_COUNT" == 0 ]] From fff29d1db61f82809538b0e75ccebb058d03ec50 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 02:48:50 -0700 Subject: [PATCH 21/41] feat: add taskmaster-verify-command lib for shell-verifier gate (T1.1) --- taskmaster-verify-command.sh | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 taskmaster-verify-command.sh diff --git a/taskmaster-verify-command.sh b/taskmaster-verify-command.sh new file mode 100755 index 0000000..d46e325 --- /dev/null +++ b/taskmaster-verify-command.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Optional shell verifier gate for the Taskmaster stop hook. +# +# When TASKMASTER_VERIFY_COMMAND is set, calling taskmaster_run_verify_command +# runs the command with a timeout, captures combined output (truncated), and +# sets: +# TASKMASTER_VERIFY_EXIT_CODE the command's exit code +# TASKMASTER_VERIFY_OUTPUT_TAIL last $TASKMASTER_VERIFY_MAX_OUTPUT bytes of output +# It returns the command's exit code (0 = pass, non-zero = block). +# When unset, returns 0 with empty fields (no-op pass). +# +# Env knobs: +# TASKMASTER_VERIFY_COMMAND command string; empty/unset = skip +# TASKMASTER_VERIFY_TIMEOUT seconds before SIGTERM (default 60); +5s grace SIGKILL +# TASKMASTER_VERIFY_MAX_OUTPUT bytes of output kept (default 4000) +# TASKMASTER_VERIFY_CWD optional cwd override +# + +taskmaster_run_verify_command() { + TASKMASTER_VERIFY_OUTPUT_TAIL="" + TASKMASTER_VERIFY_EXIT_CODE="" + + local cmd="${TASKMASTER_VERIFY_COMMAND:-}" + if [[ -z "$cmd" ]]; then + return 0 + fi + + local timeout_sec="${TASKMASTER_VERIFY_TIMEOUT:-60}" + local max_output="${TASKMASTER_VERIFY_MAX_OUTPUT:-4000}" + local cwd="${TASKMASTER_VERIFY_CWD:-}" + local out_file rc=0 + local prev_errexit=0 + case $- in *e*) prev_errexit=1;; esac + set +e + + out_file="$(mktemp "${TMPDIR:-/tmp}/taskmaster-verify.XXXXXX")" + + if [[ -n "$cwd" ]]; then + ( cd "$cwd" && timeout --kill-after=5 "$timeout_sec" bash -c "$cmd" ) \ + >"$out_file" 2>&1 + rc=$? + else + timeout --kill-after=5 "$timeout_sec" bash -c "$cmd" >"$out_file" 2>&1 + rc=$? + fi + + TASKMASTER_VERIFY_OUTPUT_TAIL="$(tail -c "$max_output" "$out_file" 2>/dev/null || true)" + TASKMASTER_VERIFY_EXIT_CODE="$rc" + + rm -f "$out_file" + if [[ "$prev_errexit" == "1" ]]; then set -e; fi + return "$rc" +} From 17a5240f1ecaf854c0d48559c8690c6f108fc18b Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 02:51:35 -0700 Subject: [PATCH 22/41] feat: gate stop on TASKMASTER_VERIFY_COMMAND when token present (T1.1) When TASKMASTER_VERIFY_COMMAND is set, the stop hook runs the command after the done token is detected. Exit 0 allows stop; non-zero blocks with a truncated output dump. Verifier only fires when the token is present, so mid-work stop attempts don't pay the cost of a slow verifier. --- check-completion.sh | 17 +++++++++++++++++ docs/SPEC.md | 18 ++++++++++++++++++ hooks/check-completion.sh | 17 +++++++++++++++++ install.sh | 3 +++ uninstall.sh | 2 ++ 5 files changed, 57 insertions(+) diff --git a/check-completion.sh b/check-completion.sh index 1cd849b..5ad2eaf 100755 --- a/check-completion.sh +++ b/check-completion.sh @@ -13,6 +13,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "$SCRIPT_DIR/taskmaster-compliance-prompt.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/taskmaster-verify-command.sh" INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id') @@ -85,6 +87,21 @@ if [ -f "$TRANSCRIPT" ]; then fi if [ "$HAS_DONE_SIGNAL" = true ]; then + if [ -n "${TASKMASTER_VERIFY_COMMAND:-}" ]; then + if taskmaster_run_verify_command; then + rm -f "$COUNTER_FILE" + exit 0 + else + VERIFY_REASON="TASKMASTER: verifier failed (exit=${TASKMASTER_VERIFY_EXIT_CODE}). Command: ${TASKMASTER_VERIFY_COMMAND} + +Output (last ${TASKMASTER_VERIFY_MAX_OUTPUT:-4000} bytes): +${TASKMASTER_VERIFY_OUTPUT_TAIL} + +Token alone is insufficient when a verifier is configured. Fix the failures and try again." + jq -n --arg reason "$VERIFY_REASON" '{ decision: "block", reason: $reason }' + exit 0 + fi + fi rm -f "$COUNTER_FILE" exit 0 fi diff --git a/docs/SPEC.md b/docs/SPEC.md index 9601428..7cd0ac9 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -98,6 +98,24 @@ Fixed: - Codex transport: expect only - expect payload mode + submit timing +### 5.1 Optional verifier command + +| Env var | Default | Meaning | +|---|---|---| +| `TASKMASTER_VERIFY_COMMAND` | unset | Shell command run when the done token is seen. Empty/unset = skip. | +| `TASKMASTER_VERIFY_TIMEOUT` | `60` | Seconds before SIGTERM, +5s grace before SIGKILL. | +| `TASKMASTER_VERIFY_MAX_OUTPUT` | `4000` | Bytes of combined stdout+stderr echoed back into the block reason. | +| `TASKMASTER_VERIFY_CWD` | unset | If set, `cd` here before invoking. Else inherit hook's cwd. | + +When `TASKMASTER_VERIFY_COMMAND` is set, stop is allowed only when (a) the +done token is present **and** (b) the command exits 0. A failing verifier +overrides token-based completion and blocks with the command's exit code and +truncated output. + +The verifier runs **only** when the done token is present, not on every stop +attempt — this keeps slow verifiers (test suites, builds) from gating +mid-work stop attempts. + ## 6. Operational Notes - Enforcement is same-process for Codex and stop-hook based for Claude. diff --git a/hooks/check-completion.sh b/hooks/check-completion.sh index c0da960..d38c713 100755 --- a/hooks/check-completion.sh +++ b/hooks/check-completion.sh @@ -13,6 +13,8 @@ set -u SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../taskmaster-verify-command.sh" INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id') @@ -82,6 +84,21 @@ if [ "$HAS_DONE_SIGNAL" = false ] && [ -f "$TRANSCRIPT" ]; then fi if [ "$HAS_DONE_SIGNAL" = true ]; then + if [ -n "${TASKMASTER_VERIFY_COMMAND:-}" ]; then + if taskmaster_run_verify_command; then + rm -f "$COUNTER_FILE" + exit 0 + else + VERIFY_REASON="TASKMASTER: verifier failed (exit=${TASKMASTER_VERIFY_EXIT_CODE}). Command: ${TASKMASTER_VERIFY_COMMAND} + +Output (last ${TASKMASTER_VERIFY_MAX_OUTPUT:-4000} bytes): +${TASKMASTER_VERIFY_OUTPUT_TAIL} + +Token alone is insufficient when a verifier is configured. Fix the failures and try again." + jq -n --arg reason "$VERIFY_REASON" '{ decision: "block", reason: $reason }' + exit 0 + fi + fi rm -f "$COUNTER_FILE" exit 0 fi diff --git a/install.sh b/install.sh index 4c4b382..7646113 100755 --- a/install.sh +++ b/install.sh @@ -52,6 +52,7 @@ copy_skill_files() { safe_copy "$SCRIPT_DIR/install.sh" "$skill_dir/install.sh" safe_copy "$SCRIPT_DIR/uninstall.sh" "$skill_dir/uninstall.sh" safe_copy "$SCRIPT_DIR/taskmaster-compliance-prompt.sh" "$skill_dir/taskmaster-compliance-prompt.sh" + safe_copy "$SCRIPT_DIR/taskmaster-verify-command.sh" "$skill_dir/taskmaster-verify-command.sh" safe_copy "$SCRIPT_DIR/run-taskmaster-codex.sh" "$skill_dir/run-taskmaster-codex.sh" safe_copy "$SCRIPT_DIR/check-completion.sh" "$skill_dir/check-completion.sh" @@ -62,6 +63,7 @@ copy_skill_files() { chmod +x "$skill_dir/install.sh" chmod +x "$skill_dir/uninstall.sh" chmod +x "$skill_dir/taskmaster-compliance-prompt.sh" + chmod +x "$skill_dir/taskmaster-verify-command.sh" chmod +x "$skill_dir/run-taskmaster-codex.sh" chmod +x "$skill_dir/check-completion.sh" chmod +x "$skill_dir/hooks/check-completion.sh" @@ -230,6 +232,7 @@ install_claude() { mkdir -p "$CLAUDE_HOOKS_DIR" ln -sf "$CLAUDE_SKILL_DIR/check-completion.sh" "$CLAUDE_HOOK_LINK" ln -sf "$CLAUDE_SKILL_DIR/taskmaster-compliance-prompt.sh" "$CLAUDE_HOOKS_DIR/taskmaster-compliance-prompt.sh" + ln -sf "$CLAUDE_SKILL_DIR/taskmaster-verify-command.sh" "$CLAUDE_HOOKS_DIR/taskmaster-verify-command.sh" chmod +x "$CLAUDE_HOOK_LINK" echo " Claude: installed skill files to $CLAUDE_SKILL_DIR" diff --git a/uninstall.sh b/uninstall.sh index c1a2c5e..e174af6 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -201,6 +201,8 @@ uninstall_claude() { echo "Removing Taskmaster from Claude..." remove_claude_stop_hook_from_settings "$CLAUDE_SETTINGS_PATH" remove_symlink_if_target "$CLAUDE_HOOK_LINK" "$CLAUDE_CHECK_SCRIPT" + rm -f "$CLAUDE_ROOT/hooks/taskmaster-compliance-prompt.sh" + rm -f "$CLAUDE_ROOT/hooks/taskmaster-verify-command.sh" remove_dir_if_exists "$CLAUDE_SKILL_DIR" } From c15f5416f10dbfe76ef5008066cfc74045aa9cdb Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 02:59:22 -0700 Subject: [PATCH 23/41] fix: add RETURN trap so verify-command tmpfile is removed on signal (T1.1) Code review on Phase A flagged that mktemp output could leak if the hook is killed mid-run (between mktemp and rm). RETURN trap inside the function ensures cleanup on any function exit path, including signal-induced ones. --- taskmaster-verify-command.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/taskmaster-verify-command.sh b/taskmaster-verify-command.sh index d46e325..f5ec09c 100755 --- a/taskmaster-verify-command.sh +++ b/taskmaster-verify-command.sh @@ -35,6 +35,7 @@ taskmaster_run_verify_command() { set +e out_file="$(mktemp "${TMPDIR:-/tmp}/taskmaster-verify.XXXXXX")" + trap 'rm -f "$out_file"' RETURN if [[ -n "$cwd" ]]; then ( cd "$cwd" && timeout --kill-after=5 "$timeout_sec" bash -c "$cmd" ) \ From beabd15837960a67f80e303f4be49673e9db2c15 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:02:28 -0700 Subject: [PATCH 24/41] test: add failing tests for taskmaster-prompt-detect lib (T1.3) --- tests/prompt-detect.test.sh | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/prompt-detect.test.sh diff --git a/tests/prompt-detect.test.sh b/tests/prompt-detect.test.sh new file mode 100644 index 0000000..a53a8e5 --- /dev/null +++ b/tests/prompt-detect.test.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# +# Tests for taskmaster-prompt-detect.sh. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB="$REPO_ROOT/taskmaster-prompt-detect.sh" + +# shellcheck disable=SC1090 +source "$LIB" + +PASS_COUNT=0 +FAIL_COUNT=0 + +assert_detected() { + local name="$1" + local text="$2" + if is_taskmaster_injected_prompt "$text"; then + printf 'ok %s\n' "$name" + PASS_COUNT=$((PASS_COUNT + 1)) + else + printf 'FAIL %s\n' "$name" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +assert_not_detected() { + local name="$1" + local text="$2" + if is_taskmaster_injected_prompt "$text"; then + printf 'FAIL %s (false positive)\n' "$name" >&2 + FAIL_COUNT=$((FAIL_COUNT + 1)) + else + printf 'ok %s\n' "$name" + PASS_COUNT=$((PASS_COUNT + 1)) + fi +} + +# --- Tag detection --- +assert_detected "tagged stop-block" \ + "[taskmaster:injected v=1 kind=stop-block] +TASKMASTER (1): Stop is blocked..." + +assert_detected "tagged followup" \ + "[taskmaster:injected v=1 kind=followup] +continue" + +assert_detected "tagged compliance" "[taskmaster:injected v=1 kind=compliance]" + +# --- Forward-compat: future schema version still detected --- +assert_detected "future schema v=99" "[taskmaster:injected v=99 kind=anything]" + +# --- Legacy substring matches (back-compat with mickn's prompts and our own) --- +assert_detected "legacy: ..." +assert_detected "legacy: Stop is blocked" "Stop is blocked until completion is explicitly confirmed." +assert_detected "legacy: TASKMASTER (N) label" "TASKMASTER (5/100): Stop is blocked..." +assert_detected "legacy: TASKMASTER (N) label, no max" "TASKMASTER (5): Stop is blocked..." +assert_detected "legacy: Goal not yet verified complete" "Goal not yet verified complete." +assert_detected "legacy: Recent tool errors were detected" "Recent tool errors were detected." + +# --- Negatives --- +assert_not_detected "empty string" "" +assert_not_detected "real user prompt" "fix the failing test in foo_test.go" +assert_not_detected "user mentions taskmaster word" "I want to use taskmaster for this project" +assert_not_detected "tag-like but malformed" "[taskmaster:injected]" +assert_not_detected "tag-like but missing v=" "[taskmaster:injected kind=stop-block]" + +# --- generate_taskmaster_injected_tag produces a parseable tag --- +TAG="$(generate_taskmaster_injected_tag stop-block)" +assert_detected "generated tag is detectable" "$TAG" +[[ "$TAG" == "[taskmaster:injected v=1 kind=stop-block]" ]] && { + printf 'ok generated tag exact format\n'; PASS_COUNT=$((PASS_COUNT + 1)); +} || { + printf 'FAIL generated tag exact format (got: %s)\n' "$TAG" >&2; FAIL_COUNT=$((FAIL_COUNT + 1)); +} + +printf '\n%d passed, %d failed\n' "$PASS_COUNT" "$FAIL_COUNT" +[[ "$FAIL_COUNT" == 0 ]] From c2e9f87376b5b336387b851ebec70a1c91ba1e19 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:02:47 -0700 Subject: [PATCH 25/41] feat: add taskmaster-prompt-detect lib with tag + legacy detection (T1.3) --- taskmaster-prompt-detect.sh | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100755 taskmaster-prompt-detect.sh diff --git a/taskmaster-prompt-detect.sh b/taskmaster-prompt-detect.sh new file mode 100755 index 0000000..4de8e0f --- /dev/null +++ b/taskmaster-prompt-detect.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# Detect prompts that Taskmaster itself injected, so they don't get +# treated as fresh user goals by downstream consumers (T2.2 user-prompt +# capture, T3 verifier). +# +# Two-tier detection: +# 1. Forward path: explicit `[taskmaster:injected v= kind=]` tag +# on the first non-empty line. Forward-compatible across schema bumps. +# 2. Legacy fallback: substring match against known wording from this +# project and from mickn/taskmaster's fork. +# + +readonly TASKMASTER_INJECTED_TAG_VERSION=1 + +# Emit the canonical tag for a given kind. Caller prepends to their prompt. +# Kinds: stop-block, followup, compliance, session-start, verifier-feedback. +generate_taskmaster_injected_tag() { + local kind="${1:-unknown}" + printf '[taskmaster:injected v=%d kind=%s]' \ + "$TASKMASTER_INJECTED_TAG_VERSION" "$kind" +} + +is_taskmaster_injected_tag_line() { + local text="$1" + [[ "$text" =~ ^\[taskmaster:injected[[:space:]]v=[0-9]+[[:space:]]kind=[a-zA-Z0-9_-]+\] ]] +} + +is_taskmaster_legacy_injected_prompt() { + local text="$1" + case "$text" in + " Date: Tue, 28 Apr 2026 03:04:57 -0700 Subject: [PATCH 26/41] feat: tag every hook-injected prompt with [taskmaster:injected v=1 kind=...] (T1.3) Adds an explicit single-line marker to the top of every prompt the hook injects. Downstream consumers detect injected prompts via the new taskmaster-prompt-detect lib (forward path: tag match; back-compat: legacy substring match against current and mickn/taskmaster wording). --- SKILL.md | 6 ++++++ check-completion.sh | 9 +++++++-- docs/SPEC.md | 16 ++++++++++++++++ hooks/check-completion.sh | 9 +++++++-- hooks/inject-continue-codex.sh | 5 +++++ install.sh | 3 +++ uninstall.sh | 1 + 7 files changed, 45 insertions(+), 4 deletions(-) diff --git a/SKILL.md b/SKILL.md index 9be5719..e1616e4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -26,6 +26,12 @@ skill implements the same completion contract externally. expect PTY bridge transport, using the shared compliance prompt. 5. **Token present**: no further injection. +## A note on the injected-prompt tag + +If you see a line starting with `[taskmaster:injected v=…]` at the top of a +message, that's metadata the hook adds to its own prompts. Treat it as a +marker, not as content you need to act on. + ## Parseable Done Signal When the work is genuinely complete, the agent must include this exact line diff --git a/check-completion.sh b/check-completion.sh index 5ad2eaf..8a336e0 100755 --- a/check-completion.sh +++ b/check-completion.sh @@ -15,6 +15,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/taskmaster-compliance-prompt.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/taskmaster-verify-command.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/taskmaster-prompt-detect.sh" INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id') @@ -92,7 +94,8 @@ if [ "$HAS_DONE_SIGNAL" = true ]; then rm -f "$COUNTER_FILE" exit 0 else - VERIFY_REASON="TASKMASTER: verifier failed (exit=${TASKMASTER_VERIFY_EXIT_CODE}). Command: ${TASKMASTER_VERIFY_COMMAND} + VERIFY_REASON="$(generate_taskmaster_injected_tag verifier-feedback) +TASKMASTER: verifier failed (exit=${TASKMASTER_VERIFY_EXIT_CODE}). Command: ${TASKMASTER_VERIFY_COMMAND} Output (last ${TASKMASTER_VERIFY_MAX_OUTPUT:-4000} bytes): ${TASKMASTER_VERIFY_OUTPUT_TAIL} @@ -129,7 +132,9 @@ fi # --- reprompt --- SHARED_PROMPT="$(build_taskmaster_compliance_prompt "$DONE_SIGNAL")" -REASON="${LABEL}: ${PREAMBLE} +INJECTED_TAG="$(generate_taskmaster_injected_tag stop-block)" +REASON="${INJECTED_TAG} +${LABEL}: ${PREAMBLE} ${SHARED_PROMPT}" diff --git a/docs/SPEC.md b/docs/SPEC.md index 7cd0ac9..5e1a601 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -78,6 +78,22 @@ TASKMASTER_DONE:: - Injects payload into the same Codex PTY via bracketed paste. - Submits prompt with Enter after fixed short delay. +### 3.5 Hook-injected prompt tag + +Every prompt the hook injects starts with a single-line tag: + +``` +[taskmaster:injected v=1 kind=] + +``` + +`` ∈ `stop-block | followup | compliance | session-start | verifier-feedback`. + +Downstream consumers (UserPromptSubmit hook, completion verifier, external +tooling) detect injected prompts via `is_taskmaster_injected_prompt` from +`taskmaster-prompt-detect.sh`. Legacy substring detection is preserved for +prompts emitted before this version. + ## 4. Installation Behavior `install.sh` auto-detects Codex and/or Claude and installs matching targets. diff --git a/hooks/check-completion.sh b/hooks/check-completion.sh index d38c713..4923d25 100755 --- a/hooks/check-completion.sh +++ b/hooks/check-completion.sh @@ -15,6 +15,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/../taskmaster-verify-command.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../taskmaster-prompt-detect.sh" INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id') @@ -89,7 +91,8 @@ if [ "$HAS_DONE_SIGNAL" = true ]; then rm -f "$COUNTER_FILE" exit 0 else - VERIFY_REASON="TASKMASTER: verifier failed (exit=${TASKMASTER_VERIFY_EXIT_CODE}). Command: ${TASKMASTER_VERIFY_COMMAND} + VERIFY_REASON="$(generate_taskmaster_injected_tag verifier-feedback) +TASKMASTER: verifier failed (exit=${TASKMASTER_VERIFY_EXIT_CODE}). Command: ${TASKMASTER_VERIFY_COMMAND} Output (last ${TASKMASTER_VERIFY_MAX_OUTPUT:-4000} bytes): ${TASKMASTER_VERIFY_OUTPUT_TAIL} @@ -126,7 +129,9 @@ fi # --- reprompt --- SHARED_PROMPT="$(build_taskmaster_compliance_prompt "$DONE_SIGNAL")" -REASON="${LABEL}: ${PREAMBLE} +INJECTED_TAG="$(generate_taskmaster_injected_tag stop-block)" +REASON="${INJECTED_TAG} +${LABEL}: ${PREAMBLE} ${SHARED_PROMPT}" diff --git a/hooks/inject-continue-codex.sh b/hooks/inject-continue-codex.sh index b163280..be7b845 100755 --- a/hooks/inject-continue-codex.sh +++ b/hooks/inject-continue-codex.sh @@ -15,6 +15,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh" +# shellcheck disable=SC1091 +source "$(dirname "${BASH_SOURCE[0]}")/../taskmaster-prompt-detect.sh" usage() { cat <<'USAGE' @@ -173,7 +175,10 @@ build_reprompt() { shared_prompt="$(build_taskmaster_compliance_prompt "$token")" + local injected_tag + injected_tag="$(generate_taskmaster_injected_tag followup)" cat < Date: Tue, 28 Apr 2026 03:09:43 -0700 Subject: [PATCH 27/41] fix: cover Completion-check-before-stopping legacy match; mark reserved kinds (T1.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec review on Phase B flagged two gaps: tests asserted 5 of mickn's 6 legacy substrings (missing Completion-check-before-stopping); SPEC §3.5 listed all 5 tag kinds without flagging which are reserved for T2/T3. Both addressed. --- docs/SPEC.md | 1 + tests/prompt-detect.test.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/SPEC.md b/docs/SPEC.md index 5e1a601..60189d9 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -88,6 +88,7 @@ Every prompt the hook injects starts with a single-line tag: ``` `` ∈ `stop-block | followup | compliance | session-start | verifier-feedback`. +The `compliance` and `session-start` kinds are reserved for future use (T2 native Codex hooks, T3 semantic verifier); only `stop-block`, `followup`, and `verifier-feedback` are emitted in v4.3.0. Downstream consumers (UserPromptSubmit hook, completion verifier, external tooling) detect injected prompts via `is_taskmaster_injected_prompt` from diff --git a/tests/prompt-detect.test.sh b/tests/prompt-detect.test.sh index a53a8e5..cb2c03a 100644 --- a/tests/prompt-detect.test.sh +++ b/tests/prompt-detect.test.sh @@ -54,6 +54,7 @@ assert_detected "future schema v=99" "[taskmaster:injected v=99 kind=anything]" # --- Legacy substring matches (back-compat with mickn's prompts and our own) --- assert_detected "legacy: ..." assert_detected "legacy: Stop is blocked" "Stop is blocked until completion is explicitly confirmed." +assert_detected "legacy: Completion check before stopping" "Completion check before stopping." assert_detected "legacy: TASKMASTER (N) label" "TASKMASTER (5/100): Stop is blocked..." assert_detected "legacy: TASKMASTER (N) label, no max" "TASKMASTER (5): Stop is blocked..." assert_detected "legacy: Goal not yet verified complete" "Goal not yet verified complete." From 6db9f14fb71e00d774a045955c5e501d1ff72de5 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:14:01 -0700 Subject: [PATCH 28/41] fix: idempotent re-source guard in taskmaster-prompt-detect (T1.3) Code review on Phase B flagged that the readonly TASKMASTER_INJECTED_TAG_VERSION declaration would error on second source under set -e. No call site does this today, but it's an unforced asymmetry with the Phase A verify-command lib. Sentinel guard makes the lib safe to source multiple times. --- taskmaster-prompt-detect.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/taskmaster-prompt-detect.sh b/taskmaster-prompt-detect.sh index 4de8e0f..8cb264f 100755 --- a/taskmaster-prompt-detect.sh +++ b/taskmaster-prompt-detect.sh @@ -11,6 +11,9 @@ # project and from mickn/taskmaster's fork. # +# Idempotent re-source: avoid `readonly` re-declaration error under `set -e`. +[[ -n "${TASKMASTER_PROMPT_DETECT_LOADED:-}" ]] && return 0 +readonly TASKMASTER_PROMPT_DETECT_LOADED=1 readonly TASKMASTER_INJECTED_TAG_VERSION=1 # Emit the canonical tag for a given kind. Caller prepends to their prompt. From df1fbe15881609f898ad62eae237bdf7c77820ff Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:16:58 -0700 Subject: [PATCH 29/41] test: add failing tests for taskmaster-state lib (T1.2) --- tests/state.test.sh | 87 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/state.test.sh diff --git a/tests/state.test.sh b/tests/state.test.sh new file mode 100644 index 0000000..596eaba --- /dev/null +++ b/tests/state.test.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# +# Tests for taskmaster-state.sh. +# +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB="$REPO_ROOT/taskmaster-state.sh" + +# Isolate state under a temp dir +TEST_HOME="$(mktemp -d "${TMPDIR:-/tmp}/taskmaster-state-test.XXXXXX")" +trap 'rm -rf "$TEST_HOME"' EXIT +export TASKMASTER_STATE_DIR="$TEST_HOME/state" + +# shellcheck disable=SC1090 +source "$LIB" + +PASS=0; FAIL=0 +ok() { printf 'ok %s\n' "$1"; PASS=$((PASS+1)); } +fail() { printf 'FAIL %s\n' "$1" >&2; FAIL=$((FAIL+1)); } + +# --- init creates well-formed JSON with schema_version=1 --- +SID="sess-$$" +taskmaster_state_init "$SID" +PATH_OUT="$(taskmaster_state_path "$SID")" +[[ -f "$PATH_OUT" ]] && ok "init creates file" || fail "init creates file" +SV="$(jq -r .schema_version <"$PATH_OUT")" +[[ "$SV" == "1" ]] && ok "schema_version is 1" || fail "schema_version is 1 (got $SV)" +SI="$(jq -r .session_id <"$PATH_OUT")" +[[ "$SI" == "$SID" ]] && ok "session_id stamped" || fail "session_id stamped" +SC="$(jq -r .stop_count <"$PATH_OUT")" +[[ "$SC" == "0" ]] && ok "stop_count starts at 0" || fail "stop_count starts at 0 (got $SC)" + +# --- increment --- +taskmaster_state_increment_stop_count "$SID" +SC="$(jq -r .stop_count <"$PATH_OUT")" +[[ "$SC" == "1" ]] && ok "stop_count after one increment is 1" || fail "increment 1 (got $SC)" + +taskmaster_state_increment_stop_count "$SID" +taskmaster_state_increment_stop_count "$SID" +SC="$(jq -r .stop_count <"$PATH_OUT")" +[[ "$SC" == "3" ]] && ok "stop_count after three increments is 3" || fail "increment 3 (got $SC)" + +# --- concurrent increments --- +SID2="sess-conc-$$" +taskmaster_state_init "$SID2" +PATH_C="$(taskmaster_state_path "$SID2")" +N=50 +for i in $(seq 1 "$N"); do + ( taskmaster_state_increment_stop_count "$SID2" ) & +done +wait +SC="$(jq -r .stop_count <"$PATH_C")" +[[ "$SC" == "$N" ]] && ok "concurrent $N increments reach $N" \ + || fail "concurrent increments lost some (got $SC, expected $N)" + +# --- legacy migration --- +LEGACY_DIR="${TMPDIR:-/tmp}/taskmaster" +mkdir -p "$LEGACY_DIR" +SID3="sess-legacy-$$" +LEGACY_FILE="$LEGACY_DIR/$SID3" +echo "7" > "$LEGACY_FILE" + +# State doesn't exist yet; migration should pull the 7 +taskmaster_state_migrate_legacy_counter "$SID3" +[[ -f "$(taskmaster_state_path "$SID3")" ]] && ok "legacy migration creates state file" \ + || fail "legacy migration creates state file" +SC="$(jq -r .stop_count <"$(taskmaster_state_path "$SID3")")" +[[ "$SC" == "7" ]] && ok "legacy counter value migrated" \ + || fail "legacy counter value migrated (got $SC, expected 7)" +[[ ! -f "$LEGACY_FILE" ]] && ok "legacy file deleted after migration" \ + || fail "legacy file deleted after migration" + +# --- migration is idempotent (rerun without legacy file is a no-op) --- +taskmaster_state_migrate_legacy_counter "$SID3" +SC="$(jq -r .stop_count <"$(taskmaster_state_path "$SID3")")" +[[ "$SC" == "7" ]] && ok "second migration call is a no-op" \ + || fail "second migration mutated state (got $SC)" + +# --- jq read of nonexistent path returns null --- +SID4="sess-empty-$$" +VAL="$(taskmaster_state_jq "$SID4" '.latest_user_prompt.prompt' 2>/dev/null || echo "MISSING")" +[[ "$VAL" == "null" || -z "$VAL" || "$VAL" == "MISSING" ]] && ok "nonexistent path read is safe" \ + || fail "nonexistent path read is safe (got $VAL)" + +printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" +[[ "$FAIL" == 0 ]] From 26392cc507ad6eb45e77ce645bac35873317aecd Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:17:28 -0700 Subject: [PATCH 30/41] feat: add taskmaster-state JSON state lib with flock + atomic writes (T1.2) --- taskmaster-state.sh | 170 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100755 taskmaster-state.sh diff --git a/taskmaster-state.sh b/taskmaster-state.sh new file mode 100755 index 0000000..cee77b2 --- /dev/null +++ b/taskmaster-state.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# +# Persistent JSON session state for Taskmaster. +# +# Layout: ${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}/.json +# +# Schema (v1): +# { +# "schema_version": 1, +# "session_id": "", +# "created_at": "", +# "updated_at": "", +# "stop_count": 0, +# "latest_user_prompt": null | {captured_at, turn_id, prompt}, +# "last_verifier_run": null | {ran_at, input_hash, complete, reason, next_action}, +# "metadata": {} +# } +# +# Atomicity: all writes go through tmp+mv guarded by flock on .lock. +# + +# Idempotent re-source guard (matches Phase B prompt-detect pattern). +[[ -n "${TASKMASTER_STATE_LOADED:-}" ]] && return 0 +readonly TASKMASTER_STATE_LOADED=1 + +taskmaster_state_dir() { + printf '%s\n' "${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}" +} + +taskmaster_state_path() { + local sid="$1" + printf '%s/%s.json\n' "$(taskmaster_state_dir)" "$sid" +} + +taskmaster_state_now() { + date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +taskmaster_state_init() { + local sid="$1" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + mkdir -p "$(dirname "$path")" + [[ -f "$path" ]] && return 0 + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + if [[ ! -f "$path" ]]; then + jq -n \ + --arg sid "$sid" \ + --arg now "$now" \ + '{ + schema_version: 1, + session_id: $sid, + created_at: $now, + updated_at: $now, + stop_count: 0, + latest_user_prompt: null, + last_verifier_run: null, + metadata: {} + }' >"$tmp" + mv "$tmp" "$path" + fi + exec 9>&- +} + +taskmaster_state_jq() { + local sid="$1" expr="$2" + local path + path="$(taskmaster_state_path "$sid")" + [[ -f "$path" ]] || return 0 + jq -r "$expr" <"$path" 2>/dev/null +} + +# Run jq with a transformation expression and atomically write the result back. +taskmaster_state_update() { + local sid="$1" expr="$2" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + taskmaster_state_init "$sid" + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + jq --arg now "$now" "$expr | .updated_at = \$now" "$path" >"$tmp" + mv "$tmp" "$path" + exec 9>&- +} + +taskmaster_state_increment_stop_count() { + local sid="$1" + taskmaster_state_update "$sid" '.stop_count = (.stop_count + 1)' +} + +taskmaster_state_capture_prompt() { + local sid="$1" turn_id="$2" prompt="$3" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + taskmaster_state_init "$sid" + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + jq \ + --arg now "$now" \ + --arg turn "$turn_id" \ + --arg prompt "$prompt" \ + '.latest_user_prompt = {captured_at: $now, turn_id: $turn, prompt: $prompt} + | .updated_at = $now' \ + "$path" >"$tmp" + mv "$tmp" "$path" + exec 9>&- +} + +taskmaster_state_record_verifier_run() { + local sid="$1" input_hash="$2" complete="$3" reason="$4" next_action="$5" + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + taskmaster_state_init "$sid" + + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + jq \ + --arg now "$now" \ + --arg hash "$input_hash" \ + --argjson complete "$complete" \ + --arg reason "$reason" \ + --arg next "$next_action" \ + '.last_verifier_run = { + ran_at: $now, + input_hash: $hash, + complete: $complete, + reason: $reason, + next_action: $next + } | .updated_at = $now' \ + "$path" >"$tmp" + mv "$tmp" "$path" + exec 9>&- +} + +# One-time migration: absorb legacy ${TMPDIR}/taskmaster/ counter into the +# state file's stop_count, then delete the legacy file. Idempotent — safe to +# call on every hook entry. +taskmaster_state_migrate_legacy_counter() { + local sid="$1" + local legacy="${TMPDIR:-/tmp}/taskmaster/${sid}" + [[ -f "$legacy" ]] || return 0 + + local count + count="$(cat "$legacy" 2>/dev/null || echo 0)" + [[ "$count" =~ ^[0-9]+$ ]] || count=0 + + taskmaster_state_init "$sid" + taskmaster_state_update "$sid" ".stop_count = $count" + rm -f "$legacy" +} From 55bb54de181362788eae4fe0865a5e94e2d5404c Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:19:59 -0700 Subject: [PATCH 31/41] feat: replace counter file with JSON state file + flock + migration (T1.2) stop_count now lives in a flock-protected JSON file at $TASKMASTER_STATE_DIR/.json (default: $TMPDIR/taskmaster/state). Legacy counter files are absorbed on first read and deleted. Schema is versioned for forward compatibility with T2/T3 fields (latest_user_prompt, last_verifier_run). --- check-completion.sh | 25 ++++++++++++------------- docs/SPEC.md | 25 +++++++++++++++++++++++++ hooks/check-completion.sh | 25 ++++++++++++------------- hooks/inject-continue-codex.sh | 5 +++++ install.sh | 3 +++ uninstall.sh | 1 + 6 files changed, 58 insertions(+), 26 deletions(-) diff --git a/check-completion.sh b/check-completion.sh index 8a336e0..6b23dea 100755 --- a/check-completion.sh +++ b/check-completion.sh @@ -17,6 +17,8 @@ source "$SCRIPT_DIR/taskmaster-compliance-prompt.sh" source "$SCRIPT_DIR/taskmaster-verify-command.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/taskmaster-prompt-detect.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/taskmaster-state.sh" INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id') @@ -35,16 +37,13 @@ if [ -f "$TRANSCRIPT" ]; then fi fi -# --- counter --- -COUNTER_DIR="${TMPDIR:-/tmp}/taskmaster" -mkdir -p "$COUNTER_DIR" -COUNTER_FILE="${COUNTER_DIR}/${SESSION_ID}" -MAX=${TASKMASTER_MAX:-100} +# --- counter (state-file backed) --- +taskmaster_state_migrate_legacy_counter "$SESSION_ID" +taskmaster_state_init "$SESSION_ID" -COUNT=0 -if [ -f "$COUNTER_FILE" ]; then - COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0") -fi +MAX=${TASKMASTER_MAX:-100} +COUNT="$(taskmaster_state_jq "$SESSION_ID" '.stop_count')" +[[ "$COUNT" =~ ^[0-9]+$ ]] || COUNT=0 transcript_has_done_signal() { local transcript_path="$1" @@ -91,7 +90,7 @@ fi if [ "$HAS_DONE_SIGNAL" = true ]; then if [ -n "${TASKMASTER_VERIFY_COMMAND:-}" ]; then if taskmaster_run_verify_command; then - rm -f "$COUNTER_FILE" + taskmaster_state_update "$SESSION_ID" '.stop_count = 0' exit 0 else VERIFY_REASON="$(generate_taskmaster_injected_tag verifier-feedback) @@ -105,16 +104,16 @@ Token alone is insufficient when a verifier is configured. Fix the failures and exit 0 fi fi - rm -f "$COUNTER_FILE" + taskmaster_state_update "$SESSION_ID" '.stop_count = 0' exit 0 fi +taskmaster_state_increment_stop_count "$SESSION_ID" NEXT=$((COUNT + 1)) -echo "$NEXT" > "$COUNTER_FILE" # Optional escape hatch after MAX continuations. if [ "$MAX" -gt 0 ] && [ "$NEXT" -ge "$MAX" ]; then - rm -f "$COUNTER_FILE" + taskmaster_state_update "$SESSION_ID" '.stop_count = 0' exit 0 fi diff --git a/docs/SPEC.md b/docs/SPEC.md index 60189d9..e0d5a5b 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -95,6 +95,31 @@ tooling) detect injected prompts via `is_taskmaster_injected_prompt` from `taskmaster-prompt-detect.sh`. Legacy substring detection is preserved for prompts emitted before this version. +### 3.6 Session state file + +Path: `${TASKMASTER_STATE_DIR:-${TMPDIR:-/tmp}/taskmaster/state}/.json` + +Schema (v1): + +```json +{ + "schema_version": 1, + "session_id": "", + "created_at": "", + "updated_at": "", + "stop_count": 0, + "latest_user_prompt": null, + "last_verifier_run": null, + "metadata": {} +} +``` + +All writes go through `flock` on `.lock` and atomic tmp+mv. + +**Legacy migration:** on first read per session, the hook absorbs any +existing counter file at `${TMPDIR}/taskmaster/` into +`stop_count` and deletes the legacy file. Idempotent. + ## 4. Installation Behavior `install.sh` auto-detects Codex and/or Claude and installs matching targets. diff --git a/hooks/check-completion.sh b/hooks/check-completion.sh index 4923d25..06626c1 100755 --- a/hooks/check-completion.sh +++ b/hooks/check-completion.sh @@ -17,6 +17,8 @@ source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh" source "$SCRIPT_DIR/../taskmaster-verify-command.sh" # shellcheck disable=SC1091 source "$SCRIPT_DIR/../taskmaster-prompt-detect.sh" +# shellcheck disable=SC1091 +source "$SCRIPT_DIR/../taskmaster-state.sh" INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id') @@ -34,16 +36,13 @@ if [ -f "$TRANSCRIPT" ]; then fi fi -# --- counter --- -COUNTER_DIR="${TMPDIR:-/tmp}/taskmaster" -mkdir -p "$COUNTER_DIR" -COUNTER_FILE="${COUNTER_DIR}/${SESSION_ID}" -MAX=${TASKMASTER_MAX:-0} +# --- counter (state-file backed) --- +taskmaster_state_migrate_legacy_counter "$SESSION_ID" +taskmaster_state_init "$SESSION_ID" -COUNT=0 -if [ -f "$COUNTER_FILE" ]; then - COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0") -fi +MAX=${TASKMASTER_MAX:-0} +COUNT="$(taskmaster_state_jq "$SESSION_ID" '.stop_count')" +[[ "$COUNT" =~ ^[0-9]+$ ]] || COUNT=0 transcript_has_done_signal() { local transcript_path="$1" @@ -88,7 +87,7 @@ fi if [ "$HAS_DONE_SIGNAL" = true ]; then if [ -n "${TASKMASTER_VERIFY_COMMAND:-}" ]; then if taskmaster_run_verify_command; then - rm -f "$COUNTER_FILE" + taskmaster_state_update "$SESSION_ID" '.stop_count = 0' exit 0 else VERIFY_REASON="$(generate_taskmaster_injected_tag verifier-feedback) @@ -102,16 +101,16 @@ Token alone is insufficient when a verifier is configured. Fix the failures and exit 0 fi fi - rm -f "$COUNTER_FILE" + taskmaster_state_update "$SESSION_ID" '.stop_count = 0' exit 0 fi +taskmaster_state_increment_stop_count "$SESSION_ID" NEXT=$((COUNT + 1)) -echo "$NEXT" > "$COUNTER_FILE" # Optional escape hatch. Default is infinite (0) so hook keeps firing. if [ "$MAX" -gt 0 ] && [ "$NEXT" -ge "$MAX" ]; then - rm -f "$COUNTER_FILE" + taskmaster_state_update "$SESSION_ID" '.stop_count = 0' exit 0 fi diff --git a/hooks/inject-continue-codex.sh b/hooks/inject-continue-codex.sh index be7b845..cb08eca 100755 --- a/hooks/inject-continue-codex.sh +++ b/hooks/inject-continue-codex.sh @@ -17,6 +17,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../taskmaster-compliance-prompt.sh" # shellcheck disable=SC1091 source "$(dirname "${BASH_SOURCE[0]}")/../taskmaster-prompt-detect.sh" +# shellcheck disable=SC1091 +source "$(dirname "${BASH_SOURCE[0]}")/../taskmaster-state.sh" usage() { cat <<'USAGE' @@ -218,6 +220,9 @@ inject_prompt() { printf '%s' "$prompt" > "$prompt_file" INJECTION_COUNT=$((INJECTION_COUNT + 1)) + if [[ -n "${SESSION_ID:-}" ]]; then + taskmaster_state_increment_stop_count "$SESSION_ID" 2>/dev/null || true + fi log_runtime "queued continuation prompt turn=${turn_id:-} count=${INJECTION_COUNT} file=${prompt_file}" if [[ "$QUIET" -eq 0 ]]; then echo "[TASKMASTER] queued continuation prompt for turn ${turn_id:-} (count=${INJECTION_COUNT}, file=${prompt_file})." >&2 diff --git a/install.sh b/install.sh index ac73e48..eb84c31 100755 --- a/install.sh +++ b/install.sh @@ -54,6 +54,7 @@ copy_skill_files() { safe_copy "$SCRIPT_DIR/taskmaster-compliance-prompt.sh" "$skill_dir/taskmaster-compliance-prompt.sh" safe_copy "$SCRIPT_DIR/taskmaster-verify-command.sh" "$skill_dir/taskmaster-verify-command.sh" safe_copy "$SCRIPT_DIR/taskmaster-prompt-detect.sh" "$skill_dir/taskmaster-prompt-detect.sh" + safe_copy "$SCRIPT_DIR/taskmaster-state.sh" "$skill_dir/taskmaster-state.sh" safe_copy "$SCRIPT_DIR/run-taskmaster-codex.sh" "$skill_dir/run-taskmaster-codex.sh" safe_copy "$SCRIPT_DIR/check-completion.sh" "$skill_dir/check-completion.sh" @@ -66,6 +67,7 @@ copy_skill_files() { chmod +x "$skill_dir/taskmaster-compliance-prompt.sh" chmod +x "$skill_dir/taskmaster-verify-command.sh" chmod +x "$skill_dir/taskmaster-prompt-detect.sh" + chmod +x "$skill_dir/taskmaster-state.sh" chmod +x "$skill_dir/run-taskmaster-codex.sh" chmod +x "$skill_dir/check-completion.sh" chmod +x "$skill_dir/hooks/check-completion.sh" @@ -236,6 +238,7 @@ install_claude() { ln -sf "$CLAUDE_SKILL_DIR/taskmaster-compliance-prompt.sh" "$CLAUDE_HOOKS_DIR/taskmaster-compliance-prompt.sh" ln -sf "$CLAUDE_SKILL_DIR/taskmaster-verify-command.sh" "$CLAUDE_HOOKS_DIR/taskmaster-verify-command.sh" ln -sf "$CLAUDE_SKILL_DIR/taskmaster-prompt-detect.sh" "$CLAUDE_HOOKS_DIR/taskmaster-prompt-detect.sh" + ln -sf "$CLAUDE_SKILL_DIR/taskmaster-state.sh" "$CLAUDE_HOOKS_DIR/taskmaster-state.sh" chmod +x "$CLAUDE_HOOK_LINK" echo " Claude: installed skill files to $CLAUDE_SKILL_DIR" diff --git a/uninstall.sh b/uninstall.sh index 6b8c288..abb5151 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -204,6 +204,7 @@ uninstall_claude() { rm -f "$CLAUDE_ROOT/hooks/taskmaster-compliance-prompt.sh" rm -f "$CLAUDE_ROOT/hooks/taskmaster-verify-command.sh" rm -f "$CLAUDE_ROOT/hooks/taskmaster-prompt-detect.sh" + rm -f "$CLAUDE_ROOT/hooks/taskmaster-state.sh" remove_dir_if_exists "$CLAUDE_SKILL_DIR" } From 0b07183c590dd4c4ddd549f572c40731640e46b3 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:35:26 -0700 Subject: [PATCH 32/41] fix: gate mv on jq exit; lock-protect additive legacy migration (T1.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code review on Phase C flagged two Critical issues: 1. Bash truncates $tmp before jq runs; if $path is unparseable, jq fails but mv unconditionally clobbers $path with the empty tmp file. Now each write helper (init/update/capture_prompt/record_verifier_run) gates mv on jq's exit code and rms the tmp on failure. 2. Migration race could rewind stop_count when two hooks fire concurrently on a session with a stale legacy file. Migration now (a) holds the flock across the entire cat → update → rm-legacy sequence, (b) re-checks the legacy file under lock, (c) absorbs additively (.stop_count = .stop_count + N) so a peer that already incremented past N is not rewound. Also adds: 12-digit length cap on legacy counter (prevents int64 overflow on downstream NEXT=$((COUNT + 1))); validation that record_verifier_run's $complete is literal true|false (rejects string "true" with EX_USAGE 64). 8 new regression tests. --- taskmaster-state.sh | 129 ++++++++++++++++++++++++++++++-------------- tests/state.test.sh | 61 ++++++++++++++++++++- 2 files changed, 148 insertions(+), 42 deletions(-) diff --git a/taskmaster-state.sh b/taskmaster-state.sh index cee77b2..6071363 100755 --- a/taskmaster-state.sh +++ b/taskmaster-state.sh @@ -50,20 +50,25 @@ taskmaster_state_init() { exec 9>"$lock" flock 9 if [[ ! -f "$path" ]]; then - jq -n \ - --arg sid "$sid" \ - --arg now "$now" \ - '{ - schema_version: 1, - session_id: $sid, - created_at: $now, - updated_at: $now, - stop_count: 0, - latest_user_prompt: null, - last_verifier_run: null, - metadata: {} - }' >"$tmp" - mv "$tmp" "$path" + if jq -n \ + --arg sid "$sid" \ + --arg now "$now" \ + '{ + schema_version: 1, + session_id: $sid, + created_at: $now, + updated_at: $now, + stop_count: 0, + latest_user_prompt: null, + last_verifier_run: null, + metadata: {} + }' >"$tmp"; then + mv "$tmp" "$path" + else + rm -f "$tmp" + exec 9>&- + return 1 + fi fi exec 9>&- } @@ -89,8 +94,13 @@ taskmaster_state_update() { exec 9>"$lock" flock 9 - jq --arg now "$now" "$expr | .updated_at = \$now" "$path" >"$tmp" - mv "$tmp" "$path" + if jq --arg now "$now" "$expr | .updated_at = \$now" "$path" >"$tmp"; then + mv "$tmp" "$path" + else + rm -f "$tmp" + exec 9>&- + return 1 + fi exec 9>&- } @@ -111,19 +121,28 @@ taskmaster_state_capture_prompt() { exec 9>"$lock" flock 9 - jq \ - --arg now "$now" \ - --arg turn "$turn_id" \ - --arg prompt "$prompt" \ - '.latest_user_prompt = {captured_at: $now, turn_id: $turn, prompt: $prompt} - | .updated_at = $now' \ - "$path" >"$tmp" - mv "$tmp" "$path" + if jq \ + --arg now "$now" \ + --arg turn "$turn_id" \ + --arg prompt "$prompt" \ + '.latest_user_prompt = {captured_at: $now, turn_id: $turn, prompt: $prompt} + | .updated_at = $now' \ + "$path" >"$tmp"; then + mv "$tmp" "$path" + else + rm -f "$tmp" + exec 9>&- + return 1 + fi exec 9>&- } taskmaster_state_record_verifier_run() { local sid="$1" input_hash="$2" complete="$3" reason="$4" next_action="$5" + case "$complete" in + true|false) ;; + *) return 64 ;; + esac local path tmp lock now path="$(taskmaster_state_path "$sid")" taskmaster_state_init "$sid" @@ -134,21 +153,26 @@ taskmaster_state_record_verifier_run() { exec 9>"$lock" flock 9 - jq \ - --arg now "$now" \ - --arg hash "$input_hash" \ - --argjson complete "$complete" \ - --arg reason "$reason" \ - --arg next "$next_action" \ - '.last_verifier_run = { - ran_at: $now, - input_hash: $hash, - complete: $complete, - reason: $reason, - next_action: $next - } | .updated_at = $now' \ - "$path" >"$tmp" - mv "$tmp" "$path" + if jq \ + --arg now "$now" \ + --arg hash "$input_hash" \ + --argjson complete "$complete" \ + --arg reason "$reason" \ + --arg next "$next_action" \ + '.last_verifier_run = { + ran_at: $now, + input_hash: $hash, + complete: $complete, + reason: $reason, + next_action: $next + } | .updated_at = $now' \ + "$path" >"$tmp"; then + mv "$tmp" "$path" + else + rm -f "$tmp" + exec 9>&- + return 1 + fi exec 9>&- } @@ -163,8 +187,31 @@ taskmaster_state_migrate_legacy_counter() { local count count="$(cat "$legacy" 2>/dev/null || echo 0)" [[ "$count" =~ ^[0-9]+$ ]] || count=0 + # Cap length to prevent int overflow on downstream NEXT=$((COUNT + 1)). + [[ ${#count} -le 12 ]] || count=0 taskmaster_state_init "$sid" - taskmaster_state_update "$sid" ".stop_count = $count" - rm -f "$legacy" + + local path tmp lock now + path="$(taskmaster_state_path "$sid")" + lock="${path}.lock" + tmp="${path}.tmp.$$" + now="$(taskmaster_state_now)" + + exec 9>"$lock" + flock 9 + # Re-check legacy file under lock — if a peer migrated already, no-op. + if [[ -f "$legacy" ]]; then + if jq --arg now "$now" --argjson n "$count" \ + '.stop_count = (.stop_count + $n) | .updated_at = $now' \ + "$path" >"$tmp"; then + mv "$tmp" "$path" + rm -f "$legacy" + else + rm -f "$tmp" + exec 9>&- + return 1 + fi + fi + exec 9>&- } diff --git a/tests/state.test.sh b/tests/state.test.sh index 596eaba..4f1bdb8 100644 --- a/tests/state.test.sh +++ b/tests/state.test.sh @@ -9,7 +9,7 @@ LIB="$REPO_ROOT/taskmaster-state.sh" # Isolate state under a temp dir TEST_HOME="$(mktemp -d "${TMPDIR:-/tmp}/taskmaster-state-test.XXXXXX")" -trap 'rm -rf "$TEST_HOME"' EXIT +trap 'rm -rf "$TEST_HOME"; rm -f "${TMPDIR:-/tmp}/taskmaster/sess-legacy-$$" "${TMPDIR:-/tmp}/taskmaster/sess-migrate-additive-$$" "${TMPDIR:-/tmp}/taskmaster/sess-overflow-$$"' EXIT export TASKMASTER_STATE_DIR="$TEST_HOME/state" # shellcheck disable=SC1090 @@ -83,5 +83,64 @@ VAL="$(taskmaster_state_jq "$SID4" '.latest_user_prompt.prompt' 2>/dev/null || e [[ "$VAL" == "null" || -z "$VAL" || "$VAL" == "MISSING" ]] && ok "nonexistent path read is safe" \ || fail "nonexistent path read is safe (got $VAL)" +# --- Critical #1 regression: corrupted state file is preserved, not clobbered --- +SID5="sess-corrupt-$$" +PATH5="$(taskmaster_state_path "$SID5")" +mkdir -p "$(dirname "$PATH5")" +printf 'this is not json' > "$PATH5" +PRE_BYTES=$(wc -c < "$PATH5") +set +e +taskmaster_state_update "$SID5" '.stop_count = 99' 2>/dev/null +RC=$? +set -e +POST_BYTES=$(wc -c < "$PATH5") +[[ "$RC" != "0" ]] && ok "corrupted file: update returns non-zero" \ + || fail "corrupted file: update returns non-zero (got $RC)" +[[ "$POST_BYTES" -gt 0 ]] && ok "corrupted file: not clobbered to empty" \ + || fail "corrupted file: not clobbered to empty (size=$POST_BYTES)" +[[ ! -f "${PATH5}.tmp"* ]] && ok "corrupted file: tmp file cleaned up" \ + || fail "corrupted file: tmp file leaked" + +# --- Critical #2 regression: migrate is additive (doesn't rewind a peer increment) --- +LEGACY_DIR="${TMPDIR:-/tmp}/taskmaster" +mkdir -p "$LEGACY_DIR" +SID6="sess-migrate-additive-$$" +PATH6="$(taskmaster_state_path "$SID6")" +# Pre-populate state as if a peer had already migrated AND incremented +taskmaster_state_init "$SID6" +taskmaster_state_update "$SID6" '.stop_count = 100' +# Plant a legacy file simulating a stale handle +echo "5" > "$LEGACY_DIR/$SID6" +# Now migrate — should absorb additively and not rewind +taskmaster_state_migrate_legacy_counter "$SID6" +SC="$(jq -r .stop_count <"$PATH6")" +[[ "$SC" == "105" ]] && ok "migrate is additive (100 + 5 = 105)" \ + || fail "migrate is additive — expected 105, got $SC" +[[ ! -f "$LEGACY_DIR/$SID6" ]] && ok "additive migrate still removes legacy file" \ + || fail "additive migrate did not remove legacy file" + +# --- Important #3 regression: oversize legacy counter is capped to 0 --- +SID7="sess-overflow-$$" +echo "999999999999999999999999999999" > "$LEGACY_DIR/$SID7" +taskmaster_state_migrate_legacy_counter "$SID7" +SC="$(jq -r .stop_count <"$(taskmaster_state_path "$SID7")")" +[[ "$SC" == "0" ]] && ok "oversize legacy counter capped to 0" \ + || fail "oversize legacy counter not capped (got $SC)" + +# --- Important #6 regression: record_verifier_run rejects non-boolean complete --- +SID8="sess-verifier-$$" +taskmaster_state_init "$SID8" +set +e +taskmaster_state_record_verifier_run "$SID8" "hash1" '"true"' "ok" "next" 2>/dev/null +RC=$? +set -e +[[ "$RC" == "64" ]] && ok "record_verifier_run rejects string complete with EX_USAGE" \ + || fail "record_verifier_run accepted string complete (rc=$RC)" +# Sanity: bare true is accepted +taskmaster_state_record_verifier_run "$SID8" "hash2" 'true' "ok" "next" +COMPLETE="$(jq -r .last_verifier_run.complete <"$(taskmaster_state_path "$SID8")")" +[[ "$COMPLETE" == "true" ]] && ok "record_verifier_run accepts bare true" \ + || fail "record_verifier_run rejected bare true (got $COMPLETE)" + printf '\n%d passed, %d failed\n' "$PASS" "$FAIL" [[ "$FAIL" == 0 ]] From 96ccb0c6e9b98149c9b9fcd7dfbd9c3ed7c6c918 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:39:57 -0700 Subject: [PATCH 33/41] test: fix false-positive in tmp-file-leak assertion (T1.2) Re-review caught that [[ ! -f "${PATH}.tmp"* ]] doesn't expand the glob and effectively never reports a leak. Replaced with `compgen -G` so the test actually fails when a tmp file is left behind. Also tightened the bytes-preserved assertion from >0 to byte-exact. --- tests/state.test.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/state.test.sh b/tests/state.test.sh index 4f1bdb8..e2dc1cb 100644 --- a/tests/state.test.sh +++ b/tests/state.test.sh @@ -96,10 +96,13 @@ set -e POST_BYTES=$(wc -c < "$PATH5") [[ "$RC" != "0" ]] && ok "corrupted file: update returns non-zero" \ || fail "corrupted file: update returns non-zero (got $RC)" -[[ "$POST_BYTES" -gt 0 ]] && ok "corrupted file: not clobbered to empty" \ - || fail "corrupted file: not clobbered to empty (size=$POST_BYTES)" -[[ ! -f "${PATH5}.tmp"* ]] && ok "corrupted file: tmp file cleaned up" \ - || fail "corrupted file: tmp file leaked" +[[ "$POST_BYTES" == "$PRE_BYTES" ]] && ok "corrupted file: bytes preserved exactly" \ + || fail "corrupted file: bytes changed (pre=$PRE_BYTES, post=$POST_BYTES)" +if compgen -G "${PATH5}.tmp.*" >/dev/null; then + fail "corrupted file: tmp file leaked: $(echo "${PATH5}".tmp.*)" +else + ok "corrupted file: tmp file cleaned up" +fi # --- Critical #2 regression: migrate is additive (doesn't rewind a peer increment) --- LEGACY_DIR="${TMPDIR:-/tmp}/taskmaster" From 718fce7fa041763dc216a04ce4d045bb5c49ce82 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:43:33 -0700 Subject: [PATCH 34/41] release v4.3.0: T1 fork-pattern adoption (verify-command, tag, state-file) See CHANGELOG.md for the full entry. Three independent additions ported from the fork-network review (mickn/taskmaster), each opt-in or backward-compatible: - T1.1: TASKMASTER_VERIFY_COMMAND shell-verifier gate - T1.3: Tagged hook-injected prompts + detection lib - T1.2: JSON state file with flock + legacy migration --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ SKILL.md | 2 +- docs/SPEC.md | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a86ca32..a9eb0d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to Taskmaster are documented here. +## v4.3.0 — 2026-04-28 + +### Added +- `TASKMASTER_VERIFY_COMMAND` env var: opt-in shell verifier that gates + stop after the done token is seen. Pairs with test suites, type-checkers, + or any repo-local check. Companion knobs: `TASKMASTER_VERIFY_TIMEOUT` + (default 60s), `TASKMASTER_VERIFY_MAX_OUTPUT` (default 4000 bytes), + `TASKMASTER_VERIFY_CWD`. (T1.1) +- Tagged hook-injected prompts: every prompt the hook injects starts + with `[taskmaster:injected v=1 kind=]`. New + `taskmaster-prompt-detect.sh` lib lets downstream consumers + distinguish injected reprompts from real user goals. Legacy substring + detection preserved for back-compat. (T1.3) +- JSON session state file at + `${TASKMASTER_STATE_DIR:-${TMPDIR}/taskmaster/state}/.json`, + flock-protected, atomic writes. Schema v1 with `stop_count`, + `latest_user_prompt`, `last_verifier_run`, `metadata` fields ready for + T2/T3. (T1.2) + +### Changed +- Stop-count tracking moved from the bare counter file at + `${TMPDIR}/taskmaster/` to the new JSON state file. + Legacy counter files are absorbed on first read and deleted — + no user action required. The migration is flock-protected and + additive (a peer's increments are not rewound). + +### References +- Design: `docs/designs/2026-04-28-072245-fork-pattern-adoption.md` +- Plan: `docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md` +- Source review: `docs/upstream-reviews/blader-taskmaster-forks.md` + ## [2.3.0] - 2026-02-25 ### Changed diff --git a/SKILL.md b/SKILL.md index e1616e4..9221cb0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -4,7 +4,7 @@ description: | Codex wrapper plus same-process expect PTY injector that keeps work moving until an explicit parseable done signal is emitted. author: blader -version: 4.2.0 +version: 4.3.0 --- # Taskmaster diff --git a/docs/SPEC.md b/docs/SPEC.md index e0d5a5b..1e0848a 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1,7 +1,7 @@ # Taskmaster ## Product & Technical Specification -**Version**: 4.2.0 +**Version**: 4.3.0 **Scope**: - `taskmaster/check-completion.sh` - `taskmaster/taskmaster-compliance-prompt.sh` From ee3887a8b3378c68b23d4581458bf14db0f8625b Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:48:27 -0700 Subject: [PATCH 35/41] fix: drop set -e from check-completion.sh to match hooks/ mirror Final cross-cutting review flagged that the canonical hook uses 'set -euo pipefail' while the hooks/ mirror uses only 'set -u'. Under set -e, a non-zero from a state-lib helper (e.g. on a corrupt state file) would abort the hook before emitting its 'decision: block' JSON, dropping the user back into the agent with no continuation prompt. Aligning to set -uo pipefail makes the two hooks behave identically and ensures the decision is always emitted. --- check-completion.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/check-completion.sh b/check-completion.sh index 6b23dea..4f52084 100755 --- a/check-completion.sh +++ b/check-completion.sh @@ -8,7 +8,11 @@ # Optional env vars: # TASKMASTER_MAX Max number of blocks before allowing stop (default: 100) # -set -euo pipefail +# errexit (-e) deliberately omitted — matches hooks/check-completion.sh. +# Hook MUST emit a decision JSON; a non-zero from a helper lib (e.g. +# taskmaster_state_update on a corrupt state file) shouldn't abort before +# the final jq -n write. Per-call error handling lives at the call sites. +set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck disable=SC1091 From de971e403cb3175c29252c671fd0b7ec59c0d2eb Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 28 Apr 2026 03:52:11 -0700 Subject: [PATCH 36/41] docs: add session summary for T1 fork-pattern adoption (v4.3.0 shipped) --- ...104902-t1-fork-pattern-adoption-shipped.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/session-summaries/2026-04-28-104902-t1-fork-pattern-adoption-shipped.md diff --git a/docs/session-summaries/2026-04-28-104902-t1-fork-pattern-adoption-shipped.md b/docs/session-summaries/2026-04-28-104902-t1-fork-pattern-adoption-shipped.md new file mode 100644 index 0000000..9ce7131 --- /dev/null +++ b/docs/session-summaries/2026-04-28-104902-t1-fork-pattern-adoption-shipped.md @@ -0,0 +1,82 @@ +# Session Summary — T1 Fork-Pattern Adoption Shipped (v4.3.0) + +**Date**: 2026-04-28 +**Outcome**: v4.3.0 released and tagged. Three Tier-1 fork-pattern features ported from `mickn/taskmaster` and shipped on `main`. + +## Summary + +End-to-end SDLC of T1 from fork-network research → design → plan → subagent-driven TDD execution → annotated tag, all within one session. Used `superpowers:writing-plans` to author the plan, `superpowers:subagent-driven-development` to execute it, and beads for cross-session tracking. + +## Completed Work + +| Phase | What | Commits | Beads | +|---|---|---|---| +| Pre-T1 | Fork-network review (32 forks → 1 substantive: mickn) | `7c364b4` | — | +| Pre-T1 | Design doc covering T1+T2+T3 tiers | `bd48770` | — | +| Pre-T1 | Implementation plan for T1 (1509 lines, 4 phases, TDD) | `dc503c9` | — | +| A — T1.1 | `TASKMASTER_VERIFY_COMMAND` shell-verifier gate | `e073665`, `fff29d1`, `17a5240`, `c15f541` | bd-v0or | +| B — T1.3 | Tagged hook-injected prompts + detection lib | `beabd15`, `c2e9f87`, `e350c0d`, `d4c2e5b`, `6db9f14` | bd-vsq0 | +| C — T1.2 | JSON state file with flock + legacy migration | `df1fbe1`, `26392cc`, `55bb54d`, `0b07183`, `96ccb0c` | bd-jmzj | +| D | Release: SKILL.md + SPEC.md to 4.3.0, CHANGELOG, v4.3.0 tag | `718fce7` | bd-he02 | +| Post-D | Final cross-cutting fix (errexit divergence) | `ee3887a` | — | + +**Git tag**: `v4.3.0` (annotated) → `718fce7` (release commit; final HEAD `ee3887a`) +**Branch divergence from origin/main**: 50 ahead, 15 behind (NOT pushed) + +## Key Changes + +**New files (8)**: +- `taskmaster-verify-command.sh` — opt-in shell verifier gate (token-then-verify) +- `taskmaster-prompt-detect.sh` — `[taskmaster:injected v=1 kind=...]` tag generator + detector with legacy substring fallback +- `taskmaster-state.sh` — JSON session state lib (flock + atomic tmp+mv + idempotent additive legacy migration) +- `tests/verify-command.test.sh` (12 assertions) +- `tests/prompt-detect.test.sh` (18 assertions) +- `tests/state.test.sh` (20 assertions) +- `docs/upstream-reviews/blader-taskmaster-forks.md` +- `docs/designs/2026-04-28-072245-fork-pattern-adoption.md` +- `docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md` + +**Modified**: +- `check-completion.sh` (root) and `hooks/check-completion.sh` (mirror) — wired into all three new libs; aligned to `set -uo pipefail` +- `hooks/inject-continue-codex.sh` — additive write of stop_count to JSON state (guarded with `|| true`) +- `install.sh`, `uninstall.sh` — three new files copied/chmoded/symlinked + cleanup +- `docs/SPEC.md` (§3.5 prompt tag, §3.6 state file, §5.1 verifier env vars) +- `SKILL.md` (version 4.2.0 → 4.3.0; added "note on the injected-prompt tag") +- `CHANGELOG.md` (v4.3.0 entry above v2.3.0) + +## Test counts at HEAD + +- `tests/state.test.sh`: 20/20 +- `tests/prompt-detect.test.sh`: 18/18 +- `tests/verify-command.test.sh`: 12/12 +- **Total**: 50 passing + +Pre-existing macOS-hardcoded test failures (`tests/install.test.sh`, `tests/inject-continue-codex.test.sh`, `tests/run-codex-expect-bridge.test.sh`, `tests/run-taskmaster-codex.test.sh`) fail in identical fashion to base — not introduced by T1. Filed as `bd-d9d6`. + +## Workflow notes (for future reference) + +- **Subagent-driven development worked well.** Each phase: implementer → spec reviewer → code quality reviewer → fix loop → close. The fix loops caught: + - Phase A: tmpfile leak on signal (RETURN trap) + - Phase B: missing legacy substring assertion + readonly re-source guard + - Phase C: 2 Critical issues (jq-exit gate + lock-protected additive migration), plus a test-only false-positive (`[[ ! -f X* ]]` glob doesn't expand) + - Final: errexit divergence between canonical and mirror hooks (pre-existing, surfaced by composition) +- **Per-phase + final cross-cutting reviewer pattern caught issues at each granularity** — the per-phase reviews caught implementation defects; the cross-cutting one caught composition issues that no single phase could have. +- **TDD discipline (failing tests committed standalone, then implementation)** kept commits bisect-friendly across all 4 phases. + +## Pending / Blocked + +Nothing blocking. Five follow-up beads issues filed for v4.3.x polish: +- `bd-d3rl` — T1.1 polish (uninstall symlink validation, SPEC empty-string note, lib header comments) +- `bd-jlyy` — T1.2 polish (boundary tests, log_runtime in injector, taskmaster_state_jq contract, lockfile GC) +- `bd-4wuw` — install.sh stale-file pruning on upgrade +- `bd-ekd6` — Schema lock-in test (`jq keys` exhaustive check) +- `bd-mr30` — TASKMASTER_MAX default divergence (100 vs 0 between hook entry points) +- `bd-d9d6` — macOS-hardcoded path failures in pre-existing tests + +The T2 epic `bd-eguw` (port mickn's native Codex hooks) and T3 (semantic verifier) remain open per the design doc tier ordering. + +## Next Session Context + +If resuming T1 follow-up polish: pull `bd-d3rl` and `bd-jlyy` first — they're the items already-known to be desirable. If proceeding to T2: the schema fields `latest_user_prompt` and `last_verifier_run` are already shaped correctly for T2.2 / T3.1 consumption (verified by the cross-cutting review). + +Branch is **not pushed**. The user has 50 commits ahead of origin/main — pushing is the user's call. From 46618c3cb86bdbaa41d956fe7c023f0725f12487 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Wed, 29 Apr 2026 11:30:34 -0700 Subject: [PATCH 37/41] docs: preserve learning seeds from session --- .claude/learning-seeds.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .claude/learning-seeds.md diff --git a/.claude/learning-seeds.md b/.claude/learning-seeds.md new file mode 100644 index 0000000..9641ba1 --- /dev/null +++ b/.claude/learning-seeds.md @@ -0,0 +1,20 @@ +# Learning Seeds (auto-generated before compaction) +# Generated: 2026-04-28T03:49 +# Trigger: auto compaction + +## Context +This session had debugging/error-resolution activity that wasn't captured in LESSONS.md. +Run `/learn` to document the lessons while context is still available. + +## Signals Detected +### Recent Fix Commits +- ee3887a fix: drop set -e from check-completion.sh to match hooks/ mirror +- 96ccb0c test: fix false-positive in tmp-file-leak assertion (T1.2) +- 0b07183 fix: gate mv on jq exit; lock-protect additive legacy migration (T1.2) +### Debugging Workflow Used +Systematic debugging markers found in transcript +### Error Resolution Pattern +Errors were encountered and subsequently resolved + +## Action +Run `/learn` to capture the debugging lessons from this session. From 90df643d8101e0b4de40c8afa4f40b010a0f6675 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Mon, 30 Mar 2026 09:51:48 -0700 Subject: [PATCH 38/41] Install hook to ~/.claude/hooks/ for consistency with standard Claude Code layout The installer now copies check-completion.sh to ~/.claude/hooks/taskmaster-check-completion.sh alongside the existing skill directory install. Existing installs with the old skills/taskmaster/hooks/ path are automatically migrated. Uninstall cleans up both locations. --- CHANGELOG.md | 8 ++++++++ install.sh | 25 ++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9eb0d2..28478a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,14 @@ All notable changes to Taskmaster are documented here. - Plan: `docs/plans/2026-04-28-083546-t1-fork-pattern-adoption.md` - Source review: `docs/upstream-reviews/blader-taskmaster-forks.md` +## [2.4.0] - 2026-03-30 + +### Changed +- Install script now also copies hook to `~/.claude/hooks/taskmaster-check-completion.sh` + (user-level hooks directory), consistent with standard Claude Code hook layout. +- Settings.json registration now points to `~/.claude/hooks/` path by default. +- Uninstall script updated to clean up from both locations. + ## [2.3.0] - 2026-02-25 ### Changed diff --git a/install.sh b/install.sh index eb84c31..17d9911 100755 --- a/install.sh +++ b/install.sh @@ -125,6 +125,25 @@ if not isinstance(stop_hooks, list): stop_hooks = [] container["Stop"] = stop_hooks +# Migrate stale entries pointing at the old in-skill hook path +# (~/.claude/skills/taskmaster/hooks/check-completion.sh) to the +# user-level $HOME/.claude/hooks/ path. Idempotent. +migrated = 0 +stale_marker = "skills/taskmaster/hooks/check-completion.sh" +for entry in stop_hooks: + if not isinstance(entry, dict): + continue + hooks = entry.get("hooks") + if not isinstance(hooks, list): + continue + for hook in hooks: + if not isinstance(hook, dict): + continue + cmd = hook.get("command") + if hook.get("type") == "command" and isinstance(cmd, str) and stale_marker in cmd: + hook["command"] = hook_command + migrated += 1 + exists = False for entry in stop_hooks: if not isinstance(entry, dict): @@ -159,8 +178,12 @@ with open(settings_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) f.write("\n") +if migrated: + print(f" Claude: migrated {migrated} stale Stop hook entr{'y' if migrated == 1 else 'ies'} to {hook_command}") + if exists: - print(" Claude: Stop hook already configured") + if not migrated: + print(" Claude: Stop hook already configured") else: print(" Claude: added Stop hook to settings") PY From 2c497ce55c41e8114913e52c69660bd96832434b Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Mon, 30 Mar 2026 09:52:00 -0700 Subject: [PATCH 39/41] docs: add session summary (install-hook-to-claudehooks-fo) --- ...0-095200-install-hook-to-claudehooks-fo.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/session-summaries/2026-03-30-095200-install-hook-to-claudehooks-fo.md diff --git a/docs/session-summaries/2026-03-30-095200-install-hook-to-claudehooks-fo.md b/docs/session-summaries/2026-03-30-095200-install-hook-to-claudehooks-fo.md new file mode 100644 index 0000000..256cb6b --- /dev/null +++ b/docs/session-summaries/2026-03-30-095200-install-hook-to-claudehooks-fo.md @@ -0,0 +1,37 @@ +# Session Summary + +**Date:** 2026-03-30 +**Time:** 09:52 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 1 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `d10b39e` - Install hook to ~/.claude/hooks/ for consistency with standard Claude Code layout + +## Key Changes + +### Files Modified +- `CHANGELOG.md` +- `LESSONS.md` +- `README.md` +- `SKILL.md` +- `check-completion.sh` +- `docs/blog/2026-02-25-taskmaster-hook-cleanup.md` +- `docs/session-summaries/2026-02-25-043407-hide-verbose-checklist-from-us.md` +- `docs/upstream-reviews/2026-02-25-blader-taskmaster-main.md` +- `hooks/check-completion.sh` +- `install.sh` +- `uninstall.sh` + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From 1ff30f69fff96fd6e4dfe68545ae8e796f3a88fe Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Sat, 2 May 2026 08:11:20 -0700 Subject: [PATCH 40/41] docs: add session summary (docs-add-session-summary-insta) --- ...2-081120-docs-add-session-summary-insta.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/session-summaries/2026-05-02-081120-docs-add-session-summary-insta.md diff --git a/docs/session-summaries/2026-05-02-081120-docs-add-session-summary-insta.md b/docs/session-summaries/2026-05-02-081120-docs-add-session-summary-insta.md new file mode 100644 index 0000000..b350056 --- /dev/null +++ b/docs/session-summaries/2026-05-02-081120-docs-add-session-summary-insta.md @@ -0,0 +1,40 @@ +# Session Summary + +**Date:** 2026-05-02 +**Time:** 08:11 +**Focus:** [Auto-generated - please review and complete] + +## Summary + +Session with 2 commits. Please add context about what was accomplished. + +## Completed Work + +### Commits +- `2c497ce` - docs: add session summary (install-hook-to-claudehooks-fo) +- `90df643` - Install hook to ~/.claude/hooks/ for consistency with standard Claude Code layout + +## Key Changes + +### Files Modified +- `.claude/learning-seeds.md` +- `CHANGELOG.md` +- `SKILL.md` +- `check-completion.sh` +- `docs/SPEC.md` +- `docs/session-summaries/2026-03-30-095200-install-hook-to-claudehooks-fo.md` +- `docs/session-summaries/2026-04-28-104902-t1-fork-pattern-adoption-shipped.md` +- `hooks/check-completion.sh` +- `hooks/inject-continue-codex.sh` +- `install.sh` +- `taskmaster-state.sh` +- `tests/state.test.sh` +- `uninstall.sh` + +## Pending/Blocked + +[TODO: Any tasks started but not finished] + +## Next Session Context + +[TODO: What the next session should know] From 5d846e40fcc24dfd78af24ba575ac3c1642a69f9 Mon Sep 17 00:00:00 2001 From: "@micahstubbs" Date: Tue, 5 May 2026 06:03:02 -0700 Subject: [PATCH 41/41] docs(lessons): capture --force-with-lease verification + conflict-marker grep --- LESSONS.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/LESSONS.md b/LESSONS.md index abb2a7d..9c4c221 100644 --- a/LESSONS.md +++ b/LESSONS.md @@ -62,3 +62,60 @@ fi ``` **Prevention**: Always check `last_assistant_message` before falling back to transcript file parsing in stop hooks. + +## 2026-05-05T06:02 - --force-with-lease rejection saves remote work after rebase divergence + +**Problem**: After rebasing local `main` to ship v4.3.0, `git push --force-with-lease origin main` was rejected with "stale info" — even though the local view of origin/main looked correct based on the session summary noting "50 ahead, 15 behind." A blind retry with `--force` would have silently destroyed two commits and a `v2.4.0` annotated tag pushed to origin from a parallel session in the intervening hours. + +**Root Cause**: `--force-with-lease` compares the *local view* of the remote ref against the *actual remote* at push time. When another session pushes between fetch and push, the lease is stale. The reflexive response is to fetch and retry — but fetching alone updates the remote-tracking ref without showing what changed. Without an explicit `git log origin/main --not main`, the new commits get absorbed into the local view and then overwritten by the next force-push. + +**Lesson**: Treat `--force-with-lease` rejection as a signal to *enumerate* what's on the remote, not as friction to bypass. Run `git log origin/main --not main --oneline` after every fetch in a divergence-resolution flow, and classify each commit: +1. Earlier-SHA versions of commits already in local history (rebase artifacts — safe to discard) +2. Genuinely new work from another session (must cherry-pick before force-push) + +When in doubt, cherry-pick. The cost of an unnecessary cherry-pick is one extra commit; the cost of a wrong force-push is unrecoverable lost work. + +**Workflow**: +```bash +# 1. Fetch latest +git fetch origin + +# 2. Enumerate what's on remote-not-local +git log origin/main --not main --oneline + +# 3. For each commit: classify as rebase artifact vs new work +# - Rebase artifact: same author + same message + similar timestamp = safe +# - New work: different message or post-rebase timestamp = cherry-pick + +# 4. Cherry-pick the new work onto local +git cherry-pick + +# 5. Resolve conflicts mindfully — when an upstream commit predates a +# major refactor, the conflict markers usually show obsolete machinery. +# Take HEAD on the conflict and port only the genuinely-new behavior +# (intent, not mechanics) into the equivalent post-refactor function. + +# 6. Now push with lease +git push --force-with-lease origin main +``` + +**Prevention**: +- Never bypass `--force-with-lease` rejection with bare `--force` without first running the `--not` log enumeration. +- Treat tags pushed by other sessions (e.g. `[new tag] v2.4.0`) as load-bearing signals — they document a decision point that local history doesn't know about. +- When rebasing onto a base that's diverged from the public head, plan for cherry-pick reconciliation as part of the workflow, not as an exception. + +## 2026-05-05T06:02 - Editing git conflict markers requires removing all three sigils + +**Problem**: Resolving a CHANGELOG.md conflict via the Edit tool: replaced the `<<<<<<< HEAD` line and the `>>>>>>>` line, but left a stray `=======` separator a few lines below in the same hunk. The next `bash -n` on a sibling install.sh passed, but `git status` still showed the file as conflicted, and a follow-up grep revealed the orphan separator. + +**Root Cause**: Git conflict blocks have three sigils — `<<<<<<<`, `=======`, `>>>>>>>` — and a partial edit that addresses only the open/close markers leaves the separator behind, which git still treats as an unresolved conflict marker. With nested or stacked conflicts in one file, an Edit-tool replacement can match the outer pair and miss inner separators. + +**Lesson**: After every conflict resolution edit, grep for *all three* sigils as a single check, not just for `<<<<<<<`: + +```bash +grep -n '<<<<<<<\|=======\|>>>>>>>' +``` + +Pair this with `bash -n` (or language-equivalent syntax check) for the affected files before staging — the syntax check catches cases where conflict residue corrupts script structure even when the markers technically remain. + +**Prevention**: Make the three-sigil grep a reflexive post-edit step in any conflict-resolution flow, exactly the same way `bash -n` is the post-edit step for shell-script changes.