Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
ae0266e
default tries 100
micahstubbs Feb 19, 2026
67bd2c4
docs: add session summary (default-tries-100)
micahstubbs Feb 19, 2026
a7542fe
docs: add session summary (make-installsh-posix-portable-)
micahstubbs Feb 19, 2026
689d830
docs: add session summary (docs-add-session-summary-make-)
micahstubbs Feb 23, 2026
6a4bd8a
docs: add session summary (hide-verbose-checklist-from-us)
micahstubbs Feb 25, 2026
8eb006c
release v2.3.0: minimal hook output, TASKMASTER_DONE signal detection
micahstubbs Feb 25, 2026
9a17abf
Add blog post: taskmaster hook cleanup (3 versions)
micahstubbs Feb 25, 2026
daeed44
manual blog post edits
micahstubbs Feb 25, 2026
566d780
add docs link at the end
micahstubbs Feb 25, 2026
bdf29fb
further edits. the agent --> Claude; it --> he
micahstubbs Feb 25, 2026
9997f36
docs(lessons): hook reason dual-use, last_assistant_message detection
micahstubbs Feb 25, 2026
c600b86
fix: expand tilde in transcript_path; add upstream review docs
micahstubbs Feb 25, 2026
cfc01ec
docs: add session summary (fix-expand-tilde-in-transcript)
micahstubbs Apr 28, 2026
ad5729d
docs: add session summary (docs-add-session-summary-fix-e)
micahstubbs Apr 28, 2026
7c364b4
docs: add fork-network review of blader/taskmaster
micahstubbs Apr 28, 2026
bd48770
docs: add design for fork-pattern adoption (T1-T3)
micahstubbs Apr 28, 2026
4751e4b
Add report: Codex native hooks verification before mickn port
micahstubbs Apr 28, 2026
dc503c9
docs: add implementation plan for T1 fork-pattern adoption
micahstubbs Apr 28, 2026
1d82430
docs: add session summary (docs-add-implementation-plan-f)
micahstubbs Apr 28, 2026
e073665
test: add failing tests for taskmaster-verify-command lib (T1.1)
micahstubbs Apr 28, 2026
fff29d1
feat: add taskmaster-verify-command lib for shell-verifier gate (T1.1)
micahstubbs Apr 28, 2026
17a5240
feat: gate stop on TASKMASTER_VERIFY_COMMAND when token present (T1.1)
micahstubbs Apr 28, 2026
c15f541
fix: add RETURN trap so verify-command tmpfile is removed on signal (…
micahstubbs Apr 28, 2026
beabd15
test: add failing tests for taskmaster-prompt-detect lib (T1.3)
micahstubbs Apr 28, 2026
c2e9f87
feat: add taskmaster-prompt-detect lib with tag + legacy detection (T…
micahstubbs Apr 28, 2026
e350c0d
feat: tag every hook-injected prompt with [taskmaster:injected v=1 ki…
micahstubbs Apr 28, 2026
d4c2e5b
fix: cover Completion-check-before-stopping legacy match; mark reserv…
micahstubbs Apr 28, 2026
6db9f14
fix: idempotent re-source guard in taskmaster-prompt-detect (T1.3)
micahstubbs Apr 28, 2026
df1fbe1
test: add failing tests for taskmaster-state lib (T1.2)
micahstubbs Apr 28, 2026
26392cc
feat: add taskmaster-state JSON state lib with flock + atomic writes …
micahstubbs Apr 28, 2026
55bb54d
feat: replace counter file with JSON state file + flock + migration (…
micahstubbs Apr 28, 2026
0b07183
fix: gate mv on jq exit; lock-protect additive legacy migration (T1.2)
micahstubbs Apr 28, 2026
96ccb0c
test: fix false-positive in tmp-file-leak assertion (T1.2)
micahstubbs Apr 28, 2026
718fce7
release v4.3.0: T1 fork-pattern adoption (verify-command, tag, state-…
micahstubbs Apr 28, 2026
ee3887a
fix: drop set -e from check-completion.sh to match hooks/ mirror
micahstubbs Apr 28, 2026
de971e4
docs: add session summary for T1 fork-pattern adoption (v4.3.0 shipped)
micahstubbs Apr 28, 2026
46618c3
docs: preserve learning seeds from session
micahstubbs Apr 29, 2026
90df643
Install hook to ~/.claude/hooks/ for consistency with standard Claude…
micahstubbs Mar 30, 2026
2c497ce
docs: add session summary (install-hook-to-claudehooks-fo)
micahstubbs Mar 30, 2026
1ff30f6
docs: add session summary (docs-add-session-summary-insta)
micahstubbs May 2, 2026
5d846e4
docs(lessons): capture --force-with-lease verification + conflict-mar…
micahstubbs May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/learning-seeds.md
Original file line number Diff line number Diff line change
@@ -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.
90 changes: 90 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Changelog

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=<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}/<session_id>.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/<session_id>` 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.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
- 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::<session_id>` 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.
121 changes: 121 additions & 0 deletions LESSONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# 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.

## 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 <sha>

# 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 '<<<<<<<\|=======\|>>>>>>>' <file>
```

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.
8 changes: 7 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
57 changes: 41 additions & 16 deletions check-completion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@
# TASKMASTER_DONE::<session_id>
#
# 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
# 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
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"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/taskmaster-state.sh"

INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
Expand All @@ -31,16 +41,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:-100}
COUNT="$(taskmaster_state_jq "$SESSION_ID" '.stop_count')"
[[ "$COUNT" =~ ^[0-9]+$ ]] || COUNT=0

transcript_has_done_signal() {
local transcript_path="$1"
Expand Down Expand Up @@ -85,16 +92,32 @@ if [ -f "$TRANSCRIPT" ]; then
fi

if [ "$HAS_DONE_SIGNAL" = true ]; then
rm -f "$COUNTER_FILE"
if [ -n "${TASKMASTER_VERIFY_COMMAND:-}" ]; then
if taskmaster_run_verify_command; then
taskmaster_state_update "$SESSION_ID" '.stop_count = 0'
exit 0
else
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}

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
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.
# 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

Expand All @@ -112,7 +135,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}"

Expand Down
Loading