Add pickup and prepare subcommands to bin/handoff #1542

Closed
opened 2026-11-04 16:23:16 +00:00 by jared · 2 comments
Owner

Problem

/os:pickup spends tool calls on work that a script can do. A pickup reads the file, edits the log line, runs bin/handoff complete, and sends an Explore agent to check Progress rows. The user asked on 2026-11-04 to move this work into dynamic context injection, so that the facts are in the skill text before the model starts.

/os:handoff has a smaller cost of the same kind. Its injected status output is 44 lines and 4.9 KB in cc-os. The writer uses three of those lines: now, new_path, and default_project.

Reproduce

n/a: feature ticket. Measured on main 21b5ebde: bin/handoff status --project-root . | wc -lc prints 44 lines and 4903 bytes.

Approach

Add two subcommands to bin/handoff. Each prints all that its skill needs. Each skill calls its subcommand once by injection.

Facts about injection, tested with claude -p scratch skills on 2026-11-04:

  • "$ARGUMENTS[0]" expands to the first word, and to an empty string with no arguments. An apostrophe in a later word does no harm.
  • "$0" stays unsubstituted with no arguments, so the shell expands it to /usr/bin/zsh. Do not use it.
  • ${CLAUDE_PROJECT_DIR} and ${CLAUDE_SESSION_ID} expand inside the command.
  • A nonzero exit aborts the whole skill. Keep 2>&1 || true on each injected line, so that the model sees the error text.
  • The body must hold $ARGUMENTS, or the notes after the path are lost.
  • os-backlog is on PATH during injection.

handoff pickup [PATH] --project-root ROOT --session ID:

  • An empty or absent PATH prints only the active lines and changes no file. It does not expire files.
  • A PATH that does not exist prints error: <path>: no such file, then the active lines, and exits 1.
  • Otherwise it claims the file, prints the file text, then prints live state.
  • Claim: stamp completed_at when it is empty, and add - <now> picked up by <ID> at the end of ## Session log. Add that section if it is absent. A second run with the same ID changes no file.
  • Live state reads references from the ## Progress section, or from the whole file when that section is absent.

Live state output, one line per reference, then the repository lines:

now: 2026-11-04 16:19:25
ticket #1541: closed | Expire handoffs that no session picks up | category/code-change, plugin/os
ticket #9999: unknown | os-backlog: <first line of its error>
commit 21b5ebde: on main | feat(os): expire handoffs that no session picks up (#1541)
commit deadbee: not found
path plugins/os/lib/handoff/cli.rb: exists
branch: main
status: M plugins/os/skills/pickup/SKILL.md
worktree: /home/jared/dev/cc-os 21b5ebde [main]

Tickets match (?<![\w/-])#(\d+). Commits match \b[0-9a-f]{7,40}\b and print only when git knows the token as a commit, or when the token holds a digit. Paths are backticked tokens that hold a /. Run os-backlog issue-show N once per ticket from the project root.

handoff prepare --project-root ROOT --session ID:

  • Expire files as status does.
  • Print now, new_path, default_project, branch, one status: line per changed file, and the five latest commits as commit: lines.
  • When a file holds picked up by <ID>, also print parent: <path> and one parent_log: <line> for each line of its ## Session log.
  • Print no active or completed lines.

status and complete do not change.

Required work:

  1. Add the pickup subcommand — check: present def pickup in plugins/os/lib/handoff/cli.rb
  2. Add the prepare subcommand — check: present def prepare in plugins/os/lib/handoff/cli.rb
  3. In /os:pickup, inject the subcommand output and remove sections 1 to 3 (read, claim, Explore). The skill keeps $ARGUMENTS in its body, keeps the no-handoff branch (print the active lines, ask which, end the turn), prints the table, and acts. A row with no fact gets unknown. — check: absent Explore in plugins/os/skills/pickup/SKILL.md
  4. In /os:handoff, inject prepare and use its parent lines in If this session began from a handoff. Remove the complete step: pickup already stamps the file. — check: present bin/handoff prepare in plugins/os/skills/handoff/SKILL.md
  5. Update the pickup and handoff lines in plugins/os/CLAUDE.md — check: present handoff prepare in plugins/os/CLAUDE.md
  6. Add the two commands to the Behavior text in plugins/os/references/frontmatter-registry.md — check: present bin/handoff pickup in plugins/os/references/frontmatter-registry.md
  7. Add one History row to docs/implementation-status/os.md — check: present handoff prepare in docs/implementation-status/os.md

Rejected: keep the Explore agent — each fact it reads is a fixed command, so the agent adds cost and no judgment. A row about a named session has no fixed command. The model decides that row from the ticket and commit lines, or prints unknown.

Rejected: only print live state and leave the claim to the model — the claim needs no judgment, and it costs one Read, one Edit, and one Bash call.

Acceptance criteria

  1. Given an active handoff, when pickup PATH --session S runs, then the file has a completed_at stamp and a picked up by S log line, and the output holds the file text with that line.
  2. Given a handoff with no ## Session log section, when pickup runs, then the file ends with that section and the log line.
  3. Given a handoff already claimed by session S, when pickup PATH --session S runs again, then the file bytes do not change and the output still holds the file text and live state.
  4. Given a handoff that another session claimed, when pickup runs, then the first completed_at stays and the file gains a second log line.
  5. Given no PATH, when pickup runs, then the output holds the active lines, no completed lines, and no file changes.
  6. Given a PATH with no file, when pickup runs, then the output names the path, holds the active lines, and the exit status is 1.
  7. Given a ## Progress row that names #7, a known commit, and a `lib/a.rb` path, when pickup runs, then the output has one live-state line for each.
  8. Given a row that names a ticket and the ticket lookup fails, when pickup runs, then that ticket's line says unknown, the claim still happens, and the exit status is 0.
  9. Given a hex word that git does not know and that has no digit, when pickup runs, then the output has no line for that word.
  10. Given a session that picked up a handoff, when prepare --session S runs, then the output names that file as parent and prints its log lines.
  11. Given a session that picked up nothing, when prepare runs, then the output has no parent line, and it holds now, new_path, and default_project.
  12. Given a handoff past retention, when prepare runs, then the file is deleted.

Owner sketch

class Handoff::PickupCommand
  def run(path)
end

class Handoff::PrepareCommand
  def run
end

class Handoff::LiveState
  def lines(text)
end

class Handoff::Document
  def claim(now, session)
  def session_log
  def picked_up_by?(session)
end

Non-goals

/os:wrap changes. Cross-repo references such as decision-model#11. Any change to the file shape.

Origin

  • Trigger: user request in this session, after session 208fbb3f added /os:pickup
  • Improvised this session: none
  • Chain: pickup spends tool calls ← read, claim, and live-state steps are model work ← DESIGN (plugins/os/skills/pickup/SKILL.md, commit 3c1a12ac)
  • Root candidate: none found; searched handoff, pickup, prepare, Handoff::Document, bin/handoff, injection, os:pickup
  • Where: plugins/os/skills/pickup/SKILL.md; plugins/os/lib/handoff/cli.rb
  • Session: 914a3148-8417-4934-9c2d-748527da22e6
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/914a3148-8417-4934-9c2d-748527da22e6.jsonl

VERDICT: CREATE

## Problem `/os:pickup` spends tool calls on work that a script can do. A pickup reads the file, edits the log line, runs `bin/handoff complete`, and sends an `Explore` agent to check `Progress` rows. The user asked on 2026-11-04 to move this work into dynamic context injection, so that the facts are in the skill text before the model starts. `/os:handoff` has a smaller cost of the same kind. Its injected `status` output is 44 lines and 4.9 KB in cc-os. The writer uses three of those lines: `now`, `new_path`, and `default_project`. ## Reproduce n/a: feature ticket. Measured on main 21b5ebde: `bin/handoff status --project-root . | wc -lc` prints 44 lines and 4903 bytes. ## Approach Add two subcommands to `bin/handoff`. Each prints all that its skill needs. Each skill calls its subcommand once by injection. Facts about injection, tested with `claude -p` scratch skills on 2026-11-04: - `"$ARGUMENTS[0]"` expands to the first word, and to an empty string with no arguments. An apostrophe in a later word does no harm. - `"$0"` stays unsubstituted with no arguments, so the shell expands it to `/usr/bin/zsh`. Do not use it. - `${CLAUDE_PROJECT_DIR}` and `${CLAUDE_SESSION_ID}` expand inside the command. - A nonzero exit aborts the whole skill. Keep `2>&1 || true` on each injected line, so that the model sees the error text. - The body must hold `$ARGUMENTS`, or the notes after the path are lost. - `os-backlog` is on `PATH` during injection. `handoff pickup [PATH] --project-root ROOT --session ID`: - An empty or absent PATH prints only the `active` lines and changes no file. It does not expire files. - A PATH that does not exist prints `error: <path>: no such file`, then the `active` lines, and exits 1. - Otherwise it claims the file, prints the file text, then prints live state. - Claim: stamp `completed_at` when it is empty, and add `- <now> picked up by <ID>` at the end of `## Session log`. Add that section if it is absent. A second run with the same ID changes no file. - Live state reads references from the `## Progress` section, or from the whole file when that section is absent. Live state output, one line per reference, then the repository lines: ``` now: 2026-11-04 16:19:25 ticket #1541: closed | Expire handoffs that no session picks up | category/code-change, plugin/os ticket #9999: unknown | os-backlog: <first line of its error> commit 21b5ebde: on main | feat(os): expire handoffs that no session picks up (#1541) commit deadbee: not found path plugins/os/lib/handoff/cli.rb: exists branch: main status: M plugins/os/skills/pickup/SKILL.md worktree: /home/jared/dev/cc-os 21b5ebde [main] ``` Tickets match `(?<![\w/-])#(\d+)`. Commits match `\b[0-9a-f]{7,40}\b` and print only when git knows the token as a commit, or when the token holds a digit. Paths are backticked tokens that hold a `/`. Run `os-backlog issue-show N` once per ticket from the project root. `handoff prepare --project-root ROOT --session ID`: - Expire files as `status` does. - Print `now`, `new_path`, `default_project`, `branch`, one `status:` line per changed file, and the five latest commits as `commit:` lines. - When a file holds `picked up by <ID>`, also print `parent: <path>` and one `parent_log: <line>` for each line of its `## Session log`. - Print no `active` or `completed` lines. `status` and `complete` do not change. Required work: 1. Add the `pickup` subcommand — check: present `def pickup` in `plugins/os/lib/handoff/cli.rb` 2. Add the `prepare` subcommand — check: present `def prepare` in `plugins/os/lib/handoff/cli.rb` 3. In `/os:pickup`, inject the subcommand output and remove sections 1 to 3 (read, claim, Explore). The skill keeps `$ARGUMENTS` in its body, keeps the no-handoff branch (print the `active` lines, ask which, end the turn), prints the table, and acts. A row with no fact gets `unknown`. — check: absent `Explore` in `plugins/os/skills/pickup/SKILL.md` 4. In `/os:handoff`, inject `prepare` and use its `parent` lines in `If this session began from a handoff`. Remove the `complete` step: pickup already stamps the file. — check: present `bin/handoff prepare` in `plugins/os/skills/handoff/SKILL.md` 5. Update the `pickup` and `handoff` lines in `plugins/os/CLAUDE.md` — check: present `handoff prepare` in `plugins/os/CLAUDE.md` 6. Add the two commands to the Behavior text in `plugins/os/references/frontmatter-registry.md` — check: present `bin/handoff pickup` in `plugins/os/references/frontmatter-registry.md` 7. Add one History row to `docs/implementation-status/os.md` — check: present `handoff prepare` in `docs/implementation-status/os.md` Rejected: keep the `Explore` agent — each fact it reads is a fixed command, so the agent adds cost and no judgment. A row about a named session has no fixed command. The model decides that row from the ticket and commit lines, or prints `unknown`. Rejected: only print live state and leave the claim to the model — the claim needs no judgment, and it costs one Read, one Edit, and one Bash call. ## Acceptance criteria 1. Given an active handoff, when `pickup PATH --session S` runs, then the file has a `completed_at` stamp and a `picked up by S` log line, and the output holds the file text with that line. 2. Given a handoff with no `## Session log` section, when `pickup` runs, then the file ends with that section and the log line. 3. Given a handoff already claimed by session S, when `pickup PATH --session S` runs again, then the file bytes do not change and the output still holds the file text and live state. 4. Given a handoff that another session claimed, when `pickup` runs, then the first `completed_at` stays and the file gains a second log line. 5. Given no PATH, when `pickup` runs, then the output holds the `active` lines, no `completed` lines, and no file changes. 6. Given a PATH with no file, when `pickup` runs, then the output names the path, holds the `active` lines, and the exit status is 1. 7. Given a `## Progress` row that names `#7`, a known commit, and a `` `lib/a.rb` `` path, when `pickup` runs, then the output has one live-state line for each. 8. Given a row that names a ticket and the ticket lookup fails, when `pickup` runs, then that ticket's line says `unknown`, the claim still happens, and the exit status is 0. 9. Given a hex word that git does not know and that has no digit, when `pickup` runs, then the output has no line for that word. 10. Given a session that picked up a handoff, when `prepare --session S` runs, then the output names that file as `parent` and prints its log lines. 11. Given a session that picked up nothing, when `prepare` runs, then the output has no `parent` line, and it holds `now`, `new_path`, and `default_project`. 12. Given a handoff past retention, when `prepare` runs, then the file is deleted. ## Owner sketch ```ruby class Handoff::PickupCommand def run(path) end class Handoff::PrepareCommand def run end class Handoff::LiveState def lines(text) end class Handoff::Document def claim(now, session) def session_log def picked_up_by?(session) end ``` ## Non-goals `/os:wrap` changes. Cross-repo references such as `decision-model#11`. Any change to the file shape. ## Origin - Trigger: user request in this session, after session 208fbb3f added `/os:pickup` - Improvised this session: none - Chain: pickup spends tool calls ← read, claim, and live-state steps are model work ← DESIGN (`plugins/os/skills/pickup/SKILL.md`, commit 3c1a12ac) - Root candidate: none found; searched handoff, pickup, prepare, Handoff::Document, bin/handoff, injection, os:pickup - Where: plugins/os/skills/pickup/SKILL.md; plugins/os/lib/handoff/cli.rb - Session: 914a3148-8417-4934-9c2d-748527da22e6 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/914a3148-8417-4934-9c2d-748527da22e6.jsonl VERDICT: CREATE
Author
Owner

Work started on branch ticket-1542.

Work started on branch ticket-1542.
Author
Owner

Resolution

Done: bin/handoff gains pickup and prepare; /os:pickup and /os:handoff inject them; pickup no longer uses Read, Edit, complete, or an Explore agent.

Evidence: main 02640283; plugins/os suite 216 runs, 0 failures; rubocop clean; live claude -p /os:pickup claimed the file and printed the table with 0 tool calls. Hand-finished after implementation 228 ended verdict:error; one design audit run, findings fixed or answered on this ticket's report.

Follow-ups: contract-auditor verdict gap captured separately through /os-backlog:capture

## Resolution **Done:** bin/handoff gains pickup and prepare; /os:pickup and /os:handoff inject them; pickup no longer uses Read, Edit, complete, or an Explore agent. **Evidence:** main 02640283; plugins/os suite 216 runs, 0 failures; rubocop clean; live claude -p /os:pickup claimed the file and printed the table with 0 tool calls. Hand-finished after implementation 228 ended verdict:error; one design audit run, findings fixed or answered on this ticket's report. **Follow-ups:** contract-auditor verdict gap captured separately through /os-backlog:capture
jared closed this issue 2026-11-04 16:56:56 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1542
No description provided.