# Compact-proof TODO injection

Audience: AI coding agents first. Status: SPEC — approved for immediate build (user order 2026-08-05).

## Goal

SessionStart MUST inject open `TaskCreate`/`TaskUpdate` tasks from disk. Compaction, resume, and crash recovery MUST NOT depend on agent remembering to call `TaskList`.

## Implementation boundary

- Edit `modules/workstation/claude/hooks/compact-context-restore.mjs` and `modules/workstation/claude/hooks/test-compact-context-restore.sh` only.
- Keep one SessionStart hook and one `additionalContext` block. NEVER add another hook.
- Default task root: `~/.claude/tasks`. Tests override with `CCR_TASKS_ROOT`.
- Task shape: `{id, subject, description, activeForm, status, blocks[], blockedBy[]}`. Files have numeric names and no cwd or task timestamp.

## Lookup contract

1. Read hook stdin `session_id`, `source`, and optional `transcript_path`.
2. Try `<tasks-root>/<session_id>` first. Exact-session lookup MUST work when transcript file does not exist yet.
3. Exact directory exists and is readable:
   - Render valid open tasks.
   - Zero open tasks with no malformed/unreadable task files = valid empty store; emit nothing. NEVER fall back.
   - Skip individual malformed/unreadable files and render valid tasks. If none remain and any file was malformed/unreadable, emit fallback hint.
4. Exact directory missing:
   - Fallback requires `transcript_path` with a readable parent directory.
   - Candidate session IDs come only from sibling `*.jsonl` transcript basenames in that parent. Ignore `.live.jsonl` sidecars.
   - Candidate transcript mtime MUST be older than current transcript mtime. When current transcript does not exist, use hook start time as upper bound.
   - Candidate task directory MUST be readable and contain at least one valid open task.
   - Exactly one candidate = use it and name its session ID in header.
   - Zero or multiple candidates = do not guess; emit fallback hint.
5. Task files contain no cwd. NEVER attempt cwd matching or merge task sets.

This conservative fallback prevents an unrelated concurrent or stale session from being resurrected. Explicit predecessor metadata is the future replacement; do not invent it in this change.

## Render contract

Header for exact session:

```text
STRUCTURED TASK LIST (compact-proof, from disk — source of truth; TaskUpdate to change):
```

Header for fallback:

```text
STRUCTURED TASK LIST (compact-proof, recovered from <source-session-id> — source of truth; TaskUpdate to change):
```

Task line:

```text
#<id> [<status>] <subject> (blocked by #<a>,#<b>)
```

Omit blocked suffix when empty. For each selected `in_progress` task, append:

```text
  description: <description>
```

Rules:

- Include only `in_progress` and `pending`. NEVER render completed/deleted.
- Normalize subject and description before rendering: replace all control characters and whitespace runs, including newlines, with one space; trim.
- UTF-8-safe truncate subject to 160 bytes and in-progress description to 300 bytes; append `…` within that bound.
- Selection priority: all `in_progress` tasks first, newest file mtime first within status; then pending newest file mtime first. Select at most 40 and only entries fitting byte budget.
- Rendering order after selection: `in_progress` then `pending`, numeric ID ascending within status.
- Whole task block MUST be at most 4096 UTF-8 bytes, including header, descriptions, separators, and overflow line.
- Reserve overflow-line bytes before accepting another task. Omitted tasks produce final line `…<n> more: call TaskList`. NEVER truncate silently.
- If one normalized task cannot fit, omit it and count it in overflow.

Fallback hint constant:

```text
STRUCTURED TASK LIST: your TaskCreate/TaskUpdate tasks persist on disk across compaction — call TaskList to see current open tasks; do NOT assume they were lost.
```

## SessionStart sources

- `compact`: preserve existing recovered prompts, decisions, TodoWrite snapshot, and journal pointer. Replace current structured-task hint with rendered task block or fallback hint. Valid empty exact store adds neither.
- `startup` and `resume`: emit task block alone when non-empty; emit fallback hint only for missing/unreadable/ambiguous recovery; valid empty exact store emits nothing.
- `clear` and every other source: emit nothing.
- Missing transcript MUST NOT suppress exact-session tasks.

## Error contract

- SessionStart MUST exit 0.
- Bad stdin or unexpected top-level error emits nothing.
- Missing/unreadable task store with no safe fallback emits fallback hint.
- One malformed task file never suppresses valid siblings.
- No task content may forge a header, task line, or newline.

## Tests

Extend `test-compact-context-restore.sh`:

1. Exact open tasks render on `compact`, `startup`, and `resume`, including when transcript is absent.
2. Ordering: in-progress before pending; numeric ID rendering; blocked-by; description byte truncation.
3. Completed/deleted excluded. Valid exact zero-open store emits nothing and does not fall back.
4. Missing exact directory + exactly one older sibling candidate recovers and names source session.
5. Multiple eligible candidates fail closed to hint. Newer sibling candidate is ignored. No cwd assumptions.
6. Malformed file is skipped while valid siblings render; all-malformed emits hint; exit 0.
7. Control characters/newlines are normalized and cannot forge output lines.
8. More than 40 tasks and multibyte text stay at or below 4096 UTF-8 bytes and report exact omitted count.
9. `clear` emits nothing. Existing compact prompt/decision/TodoWrite recovery remains unchanged.

## Dispatch and deployment

- Implement through `modules/workstation/codex/skills/cursor-orchestrator/ca.sh`; main thread reviews and runs tests.
- Verify `~/.claude/hooks/compact-context-restore.mjs` resolves to repo-managed hook before deployment. NEVER edit deployed path directly.
- Land repo change, apply existing module deployment path, and run shell test against deployed symlink.

## Non-goals

- No new persistence layer.
- No task-store schema change or predecessor pointer in this change.
- No cross-project/session merging.
- No completed-history rendering.
