Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
126 changes: 126 additions & 0 deletions TaskFlowDocs/2026-09-22-todo-intake-insert/plan.md
Original file line number Diff line number Diff line change
@@ -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` — `<!-- Add new items at the top … -->`.
- `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 `## <title>` 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.
138 changes: 138 additions & 0 deletions TaskFlowDocs/2026-09-22-todo-intake-insert/prd.md
Original file line number Diff line number Diff line change
@@ -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## <title>\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
<!-- Add new items at the top using the template below. -->
```

**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 `## <title>` 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 `## <title>` + new `- ID:`** → `## Seed entry` →
`## Removed`; new ID not inside `sed -n '/^## Removed$/,/^## /p'`.
- **B.** `hooks/task get <new-id>` 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).
8 changes: 4 additions & 4 deletions TaskFlowDocs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 的删除语义直接冲突)。
Expand Down
75 changes: 75 additions & 0 deletions hooks/smoke-test
Original file line number Diff line number Diff line change
Expand Up @@ -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

<!-- Add new items at the top using the template below. -->
## 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
Expand Down
Loading
Loading