diff --git a/TaskFlowDocs/2026-09-22-todo-intake-insert/plan.md b/TaskFlowDocs/2026-09-22-todo-intake-insert/plan.md
new file mode 100644
index 0000000..2eb2a47
--- /dev/null
+++ b/TaskFlowDocs/2026-09-22-todo-intake-insert/plan.md
@@ -0,0 +1,126 @@
+# Plan — `task intake` inserts a new entry into the previous section instead of `## Items`
+
+> Task version: v1
+> Status: ready
+
+No spec required — small, self-contained task.
+
+## Reference Pointers
+
+- `hooks/task` `intake` branch — the only production edit.
+- `hooks/task` `remove` — how `## Removed` is placed; must stay compatible.
+- `hooks/todo-check` `removed_at` (`sed -n '/^## Removed$/,/^## /p'`) — the
+ reader the insertion must not poison.
+- `TaskFlowDocs/todo.md` — ``.
+- `hooks/smoke-test` — new section after the parallel-intake block.
+
+## Related Tasks
+
+- `TF-20260921-337ab8` — this task's Todo item.
+- `TF-20260918-985164` — duplicate ID + false Removed record; **not fixed here**
+ (data cleanup, separate).
+- Promote title mismatch / Goal alignment — deferred per PRD Q1.
+
+## Skills / Tools Used
+
+- `Unaided — no capability applied to this phase; considered: shell/awk text
+ processing, POSIX-portability references, and the repository's own hook
+ conventions, all read directly.`
+
+## Preconditions
+
+- [x] Applicable repository documents and personal rules inspected;
+ precedence/conflicts recorded. — `CONTRIBUTING.md`, `CODE_STYLE.md`,
+ `TaskFlowDocs/todo.md`, `hooks/task`, `hooks/todo-check`, `hooks/smoke-test`
+ read in this worktree.
+- [x] Worktree isolation: `.worktrees/fix-todo-intake-insert` on
+ `fix/todo-intake-insert` from `main` = `895e102`.
+
+## Approval
+
+- Status: approved
+- Approved by: user
+- Approved at: 2026-09-22
+- Approved version: v1
+- Approved scope: R1–R5; intake insertion only — no promote/title alignment
+ (PRD Open Question 1), no new refusal path when Items is missing (Q2).
+
+## Steps
+
+### Step 1 — `intake` insertion index
+
+- Goal: new block lands at top of `## Items` (R1), or just before
+ `## Removed` when Items is missing (R2), or EOF only when neither exists
+ (R3).
+- Dependencies: none.
+- Files: `hooks/task`.
+- Implementation checklist:
+ - [x] In the `intake` branch, compute an insertion line index instead of
+ always emitting after `line[n]`.
+ - [x] Scan for `^## Items` then skip blanks / one leading HTML comment;
+ insert there. Else scan for `^## Removed`; insert there. Else keep
+ current EOF path (rstrip + append).
+ - [x] Keep Goal/ID duplicate checks, title derivation, and field template
+ unchanged (R4) — fields now go through the shared `emit()` path.
+ - [x] `bash -n hooks/task`.
+- Acceptance: A, B, D.
+- Verification: four manual fixtures (Items+trailing Removed; bare `# Todo`;
+ no Items + Removed; Removed-above-first-entry). Order
+ `Items < new < seed < Removed`; new ID absent from the `removed_at` range;
+ Goal-derived `##
` written.
+- Rollback: revert this commit; no other file depends on the index.
+- Status: done
+
+### Step 2 — smoke assertions
+
+- Goal: a regression of the insertion point fails the suite (R5).
+- Dependencies: Step 1.
+- Files: `hooks/smoke-test`.
+- Implementation checklist:
+ - [x] Fixture with `## Items` + seed entry + trailing `## Removed`; intake;
+ assert new heading is after `## Items` and before both the seed
+ heading and `## Removed`.
+ - [x] Assert new ID absent from the `removed_at` sed range.
+ - [x] Bare `# Todo` still intakes (R3); no-Items+Removed lands before
+ Removed (R2).
+- Acceptance: C.
+- Verification: one full `bash hooks/smoke-test` → `ALL SMOKE PASSED`.
+- Rollback: drop the block; Step 1 still stands alone.
+- Status: done
+
+## Checkpoints
+
+- After Step 1: manual fixtures + `bash -n` before touching smoke — done.
+- After Step 2: one full `bash hooks/smoke-test` — done; no chained mutation
+ runs; `pkill` confirmed 0 leftover processes.
+
+## Verification / Review
+
+| Check | Result |
+|---|---|
+| `bash -n hooks/task` | pass |
+| `bash -n hooks/smoke-test` | pass |
+| `bash hooks/smoke-test` | `ALL SMOKE PASSED` (rc=0) |
+| `git diff --check` | clean |
+| PRD reproduction fixture | order + removed-range + Goal title OK |
+| No interpreter / bash 3.2 parse section | pass (suite) |
+
+## Change Log
+
+- v1 — approved and implemented; scope from `TF-20260921-337ab8` Notes
+ (insertion point + Removed pollution + smoke acceptance). Title/promote
+ alignment explicitly out of scope.
+
+## Follow-ups
+
+- `TF-20260918-985164` — remove the false tombstone / restore or cancel the
+ live duplicate.
+- `TF-20260921-4d8ae2` — close the stale inbox entry (fix already on
+ `fix/repository-docs-reader`, merged via #46).
+- Promote-title vs Goal — decide in the todo Notes' open question when that
+ item is picked up.
+
+## Version History
+
+- v1 — planning.
+- v1 — approved (user, 2026-09-22); Steps 1–2 done; suite green.
diff --git a/TaskFlowDocs/2026-09-22-todo-intake-insert/prd.md b/TaskFlowDocs/2026-09-22-todo-intake-insert/prd.md
new file mode 100644
index 0000000..eb54e96
--- /dev/null
+++ b/TaskFlowDocs/2026-09-22-todo-intake-insert/prd.md
@@ -0,0 +1,138 @@
+# `task intake` inserts a new entry into the previous section instead of `## Items`
+
+> Task version: v1
+> Status: ready
+
+## Goal
+
+`hooks/task intake` writes the new entry at the top of `## Items` — after the
+section heading and its comment, before any other `## ` heading — so an intake
+can never land in `## Removed` or under another entry's title.
+
+## Background / Confirmed Facts
+
+All measured on `main` = `895e102` (PR #47 merged), 2026-09-22, in worktree
+`.worktrees/fix-todo-intake-insert`. Nothing here is inferred.
+
+**Root cause (`hooks/task` `intake`, lines 193–216).** The branch:
+
+1. rejects duplicate Goal / ID against every section;
+2. derives `title` from Goal (`rstrip "."` then truncate 80);
+3. strips trailing blank lines from the whole file;
+4. reprints the file;
+5. appends `"\n\n## \n\n- ID: …"` at EOF.
+
+There is no scan for `## Items`, no insertion index, and no awareness of
+`## Removed`. The insertion point is always end-of-file.
+
+**Live reproduction** (fixture: preamble + `## Items` + one seed entry +
+`## Removed` as last section), `bash hooks/task intake "New goal…" --root $tmp`:
+
+| Line | Content |
+|---|---|
+| 7 | `## Items` |
+| 10 | `## Seed entry` |
+| 23 | `## Removed` |
+| 25 | tombstone `- ID: TF-20260101-bb (removed …)` |
+| **27** | **`## New goal lands in the wrong section`** |
+| **29** | **new live `- ID: TF-20260922-5fd629`** |
+
+The new live entry is the last section of the file, **after** `## Removed`.
+`todo-check`'s `removed_at` is `sed -n '/^## Removed$/,/^## /p'`, so the range
+ends at line 27; the live ID is not currently swallowed as a tombstone when the
+new heading is present — but the entry is outside `## Items`'s intended top,
+and any future writer that appends fields *without* a fresh `## ` heading (or a
+reader that treats “after `## Removed`” as removed territory) sees a live entry
+in the deleted region. The todo item also recorded a promote that derived its
+task directory from the **previous** entry's heading when the new heading was
+not a clean section break.
+
+**Documented convention the code contradicts** — `TaskFlowDocs/todo.md` line 11:
+
+```text
+
+```
+
+**What is *not* broken:**
+
+- `task get` / `task next` / `task edit` locate by `- ID:` via `findsec`, not by
+ “am I under `## Items`”.
+- `task remove` already places `## Removed` deliberately: above the first entry
+ that has an `- ID:` (lines 321–338), or appends the section when no entry is
+ left.
+- Duplicate Goal/ID refusal works (smoke: `duplicate goal accepted`).
+- Parallel-intake merge driver smoke passes; this fix does not touch merge.
+
+**Same-root secondary issue (out of scope, recorded in the todo Notes):**
+`promote` derives the task directory name from the section heading returned by
+`get`, and ~23 extra `## ` template headings plus ~30 hand-written titles do
+not match Goal. Only `promote` errors when the heading is wrong; `get`/`findsec`
+do not care. Deferred as an open question below.
+
+## Requirements
+
+- **R1.** `intake` inserts the new `## ` block immediately after the
+ `## Items` heading, its blank lines, and an optional leading ``
+ comment — i.e. before the next `## ` line (or EOF if Items has no children).
+- **R2.** The new entry never appears after a `## Removed` heading. If
+ `## Items` is absent but `## Removed` exists, insert immediately before
+ `## Removed` (still not at EOF).
+- **R3.** If neither `## Items` nor `## Removed` exists (bare `# Todo inbox`
+ fixtures), keep the current append-at-EOF behaviour so existing smoke
+ fixtures do not need structural rewrites.
+- **R4.** Duplicate Goal/ID refusal, title derivation, and the field template
+ stay byte-identical — only the insertion index changes.
+- **R5.** `bash hooks/smoke-test` gains at least: (a) with Items+Removed
+ fixture, new entry's heading line number is `< Removed`'s and the entry sits
+ before the first existing entry heading; (b) `removed_at` range does not
+ contain the new ID; (c) bare fixture still accepts intake.
+
+## Acceptance Criteria
+
+- **A.** On the reproduction fixture above, after intake the line order is
+ `## Items` → **new `## ` + new `- ID:`** → `## Seed entry` →
+ `## Removed`; new ID not inside `sed -n '/^## Removed$/,/^## /p'`.
+- **B.** `hooks/task get ` returns the Goal-derived title (section
+ heading intake wrote), not the previous entry's title.
+- **C.** `bash hooks/smoke-test` → `ALL SMOKE PASSED`.
+- **D.** `bash -n hooks/task` clean; no change to `hooks/task` outside the
+ `intake` branch's emission (plus any shared helper it needs).
+
+## In Scope
+
+- `hooks/task` — `intake` insertion index only.
+- `hooks/smoke-test` — assertions in R5.
+- `TaskFlowDocs/todo.md` — promote/status bookkeeping for this task.
+
+## Out of Scope
+
+- `promote` title / Goal alignment (open question in the todo Notes).
+- `## Item template` and historical heading cleanup.
+- `hooks/todo-check`, `hooks/merge-todo`, remove's tombstone placement.
+- The other open todos (`985164` duplicate ID, `4d8ae2` stale close, `b7821b`
+ Approval shape).
+
+## Risks / Deferred Items
+
+- Fixtures that intentionally placed an entry at EOF after a non-Items
+ structure will keep working via R3; if a fixture has `## Items` *and*
+ expected EOF placement, the smoke run will surface it.
+- R2's “before `## Removed`” when Items is missing is a narrower rule than
+ R1; a file with Removed but no Items is already malformed relative to the
+ documented layout.
+
+## Open Questions
+
+1. Should `promote` re-derive or rewrite the section title from Goal so
+ directory names stop depending on hand-written headings? — **Recommend: no
+ in this PR** (separate todo already covers the title mismatch; changing
+ promote here widens blast radius for no intake benefit).
+2. Should `intake` refuse a todo.md that has `## Removed` but no `## Items`?
+ — **Recommend: no**; R2 inserts before Removed and moves on (ponytail: one
+ insertion rule, no new refusal path).
+
+## Version History
+
+- v1 — planning.
+- v1 — ready; approved by user 2026-09-22 (R1–R5; Q1/Q2 recommendations
+ accepted: no promote-title change, no new refusal when Items is missing).
diff --git a/TaskFlowDocs/todo.md b/TaskFlowDocs/todo.md
index 47d1b6e..426d831 100644
--- a/TaskFlowDocs/todo.md
+++ b/TaskFlowDocs/todo.md
@@ -966,15 +966,15 @@ Every direct request or imported requireme
## `task intake` inserts a new entry into the previous section instead of `## Items`
- ID: TF-20260921-337ab8
-- Status: inbox
+- Status: promoted
- Priority: normal
- Owner: Codex
- Source: user request
- Added: 2026-09-21
-- Updated: 2026-09-21
+- Updated: 2026-09-22
- Goal: Fix task intake inserting a new entry into the previous section instead of the Items section, and its insertion point landing inside the Removed section.
-- Task: Not promoted.
-- Next action: Clarify and promote when ready.
+- Task: `TaskFlowDocs/2026-09-22-todo-intake-insert/`
+- Next action: Complete PRD / Spec / Plan and request approval.
- Notes: **2026-09-21 用户指定:单独开一条。**
**问题**:`hooks/task intake` 把新条目写进文件里的上一个 `## ` 节,而不是插到 `## Items` 下。新条目因此挂到别人的标题下——那次它挂在了「Stop `TaskFlowDocs/todo.md` from growing without bound…」节里,`promote` 又拿那行当标题生出 task 文档(本轮我用假标题「State in the Skill…」跑出了 plan/prd 的首行)。
**另一半**:`intake` 的插入点固定在文件末尾,所以它落在 `## Removed` 之后;如果 `## Removed` 不是最后一节,新条目会插进 `## Removed` 节内,`todo-check` 读 `## Removed` 时会把在途条目当成已删除记录(该节与 `TF-20260918-985164` 的删除语义直接冲突)。
diff --git a/hooks/smoke-test b/hooks/smoke-test
index 5f63692..937fa4c 100755
--- a/hooks/smoke-test
+++ b/hooks/smoke-test
@@ -1109,6 +1109,81 @@ git -C "$par" merge -q --no-edit right
grep -q '^<<<<<<<' "$par/TaskFlowDocs/todo.md" && { echo "FAIL parallel intake conflicted"; exit 1; }
echo ok
+echo "== intake inserts at the top of ## Items, never after ## Removed =="
+# The old intake always appended at EOF. With `## Removed` last, the new live
+# entry landed in the deleted region — outside `## Items` and inside the range
+# `todo-check` reads for authorized deletions.
+ins_root="$tmp/intake-insert"
+mkdir -p "$ins_root/TaskFlowDocs"
+cat > "$ins_root/TaskFlowDocs/todo.md" <<'EOF'
+# Todo inbox
+
+## Status flow
+
+`inbox → clarified → promoted → in_progress → done/cancelled`
+
+## Items
+
+
+## Seed entry
+
+- ID: TF-20260101-aa
+- Status: clarified
+- Priority: normal
+- Owner: smoke
+- Source: fixture
+- Added: 2026-01-01
+- Updated: 2026-01-01
+- Goal: seed
+- Task: Not promoted.
+- Next action: none
+
+## Removed
+
+- ID: TF-20260101-bb (removed 2026-01-01: fixture)
+EOF
+ins_out="$tmp/intake-insert-out"
+ins_id="$(bash "$HERE/task" intake "New entry must sit under Items." "fixture" --root "$ins_root" > "$ins_out" && awk '{ print $NF }' "$ins_out")"
+ins_todo="$ins_root/TaskFlowDocs/todo.md"
+ins_items="$(grep -n '^## Items$' "$ins_todo" | cut -d: -f1)"
+ins_new="$(grep -n '^## New entry must sit under Items$' "$ins_todo" | cut -d: -f1)"
+ins_seed="$(grep -n '^## Seed entry$' "$ins_todo" | cut -d: -f1)"
+ins_rem="$(grep -n '^## Removed$' "$ins_todo" | cut -d: -f1)"
+[ -n "$ins_items" ] && [ -n "$ins_new" ] && [ -n "$ins_seed" ] && [ -n "$ins_rem" ] \
+ || { echo "FAIL insert fixture missing a section heading"; cat "$ins_todo"; exit 1; }
+[ "$ins_items" -lt "$ins_new" ] && [ "$ins_new" -lt "$ins_seed" ] && [ "$ins_seed" -lt "$ins_rem" ] \
+ || { echo "FAIL new entry not between Items and Removed (items=$ins_items new=$ins_new seed=$ins_seed rem=$ins_rem)"; grep -n '^## ' "$ins_todo"; exit 1; }
+sed -n '/^## Removed$/,/^## /p' "$ins_todo" | grep -q "^- ID: $ins_id$" \
+ && { echo "FAIL new ID appears inside ## Removed: $ins_id"; exit 1; }
+# Goal-derived heading is what `promote` will use as the task directory name.
+grep -qx -- '## New entry must sit under Items' "$ins_todo" \
+ || { echo "FAIL new heading is not the Goal-derived title"; exit 1; }
+
+# R2: no `## Items`, but `## Removed` exists — insert before Removed, not after.
+ins2="$tmp/intake-no-items"
+mkdir -p "$ins2/TaskFlowDocs"
+cat > "$ins2/TaskFlowDocs/todo.md" <<'EOF'
+# Todo inbox
+
+## Removed
+
+- ID: TF-20260101-bb (removed 2026-01-01: fixture)
+EOF
+bash "$HERE/task" intake "Before removed when Items is absent." "fixture" --root "$ins2" >/dev/null
+ins2_new="$(grep -n '^## Before removed when Items is absent$' "$ins2/TaskFlowDocs/todo.md" | cut -d: -f1)"
+ins2_rem="$(grep -n '^## Removed$' "$ins2/TaskFlowDocs/todo.md" | cut -d: -f1)"
+[ -n "$ins2_new" ] && [ -n "$ins2_rem" ] && [ "$ins2_new" -lt "$ins2_rem" ] \
+ || { echo "FAIL entry did not land before ## Removed when Items is absent"; cat "$ins2/TaskFlowDocs/todo.md"; exit 1; }
+
+# R3: bare `# Todo` keeps append-at-EOF so existing minimal fixtures stay valid.
+ins3="$tmp/intake-bare"
+mkdir -p "$ins3/TaskFlowDocs"
+printf '# Todo\n' > "$ins3/TaskFlowDocs/todo.md"
+bash "$HERE/task" intake "Bare file still accepts intake." "fixture" --root "$ins3" >/dev/null
+grep -qx -- '## Bare file still accepts intake' "$ins3/TaskFlowDocs/todo.md" \
+ || { echo "FAIL bare fixture did not get a heading"; cat "$ins3/TaskFlowDocs/todo.md"; exit 1; }
+echo ok
+
echo "== task next writes the deterministic Todo fields and lands Notes before Updated =="
# The point of the command is tokens: the fields it writes are templated from
# state the Agent already has, and hand-writing them costs a read of the whole
diff --git a/hooks/task b/hooks/task
index 4bb7975..db4baed 100755
--- a/hooks/task
+++ b/hooks/task
@@ -203,16 +203,66 @@ END {
t = goal
sub(/\.+$/, "", t)
title = substr(t, 1, 80)
- while (n > 0 && is_blank(line[n])) n--
- # text.rstrip() drops every trailing space and newline, so the last retained
- # line is trimmed and emitted without its own newline before the blank pair.
- if (n > 0) {
- line[n] = rtrim(line[n])
- for (i = 1; i < n; i++) printf "%s\n", line[i]
- printf "%s", line[n]
+
+ # Insertion index: top of `## Items` (after its heading, blanks, and a
+ # leading HTML comment), else just before `## Removed`, else EOF. The old
+ # code always appended at EOF, so a file ending in `## Removed` put the new
+ # live entry in the deleted region and outside the documented "add at the
+ # top" convention in `todo.md`.
+ ins = 0
+ for (i = 1; i <= n; i++) {
+ if (at(line[i], "## Items")) {
+ ins = i + 1
+ while (ins <= n && (is_blank(line[ins]) || line[ins] ~ /^[ \t]*