Mentions are handoffs: @codex and @claude dispatch headless seat runs as their own users (#79) #291

Closed
time wants to merge 0 commits from claude/card-79-mentions-handoffs-rt569b into main
pull from: claude/card-79-mentions-handoffs-rt569b
Collaborator

From github: claude/card-79-mentions-handoffs-rt569b (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR.

Commits:

Mentions are handoffs: @codex and @claude dispatch headless seat runs as their own users (#79)

Two seats beside @sonnet, @opus, @kimi..., neither of them in seats.json:

  • @codex <ask> runs a headless codex exec --sandbox workspace-write --json in a
    throwaway checkout cloned with TIME_SEAT_CODEX, under a wall-clock cap
    (TIME_SEAT_RUN_MINUTES, default 15; the process group is killed at it and nothing
    is pushed). Its last message is the reply, posted as codex. Work left in the tree
    is committed as codex, pushed to codex/issue-N-<stamp> with the seat's token, and
    the PR is opened as codex. The CLI's environment drops TIME_*, ACTIONS_*,
    LANGFUSE_*, GITHUB_TOKEN and the model keys, so the seat's hands never see Time's
    secrets, its own token included; TIME_CODEX_MODEL picks its model.
  • @claude [haiku|sonnet|opus|fable] <ask> runs a Claude seat (opus by default) with
    the repo context Time already assembles for reviews (the README, and on a PR its diff
    and checks) and posts as claude via TIME_SEAT_CLAUDE.

Without the seat's token, or if Gitea refuses the comment, Time posts for it as before.
Every posted reply is signed <sub>codex via Time · <model></sub>.

A reply whose last line starts with another seat's @mention is a handoff: Time runs that
seat on the reply, with no human between the stages. @a x -> @b y -> @c z is a chain of
stages (arrow: →, ->, =>); several seats in one stage, or on separate lines, run
side by side, as the launcher's grammar already read them. TIME_HANDOFF_MAX (default 6)
bounds the seat runs one comment may set off, so a ping-pong ends. No one hands off to
itself, and a mention mid-sentence is still just a mention.

Budget: codex exec books one ledger line (pool: codex, launch: 1, tokens from its
--json events, usd null -- no price) and is paced by budget_check("codex"); the
Claude seat books like every other call. Strangers' mentions are ignored as today.

Tests in test_time.py (ALL OK 79 handoffs): the grammar and the handoff rule;
codex_clone/codex_push against a real bare repo (the token rides the URL, never
.git/config); a trusted @codex comment launching the runner with the seat token,
committing and opening the PR as codex, and posting as codex; a stranger's doing
nothing; a reply ending in @claude ... dispatching the next seat; an arrow chain
running claude -> codex -> haiku, each stage reading the last; a ledger booking per run;
the wall-clock cap tripping cleanly; the handoff limit; a missing token, a spent budget
and a refused comment.

WORKFLOW CHANGE FOR THE OPERATOR (.github/workflows/ is a protected path, so this
commit stops at the code). In .github/workflows/mention.yml:

-    timeout-minutes: 8
+    timeout-minutes: 25   # above TIME_SEAT_RUN_MINUTES (15) plus the clone and the handoffs
     steps:
       - uses: actions/checkout@v5
         with:
           sparse-checkout: .github/time
+      - run: npm install -g @openai/codex@0.159.2   # `@codex` runs the CLI headless
       - run: python3 .github/time/time.py mention
         env:
@@
           TIME_DAILY_USD: ${{ vars.TIME_DAILY_USD }}
           TIME_MONTHLY_USD: ${{ vars.TIME_MONTHLY_USD }}
+          TIME_SEAT_CODEX: ${{ secrets.TIME_SEAT_CODEX }}     # the `codex` Gitea user's token
+          TIME_SEAT_CLAUDE: ${{ secrets.TIME_SEAT_CLAUDE }}   # the `claude` Gitea user's token
+          CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}         # or OPENAI_API_KEY: codex's own auth
+          TIME_SEAT_RUN_MINUTES: ${{ vars.TIME_SEAT_RUN_MINUTES }}
+          TIME_HANDOFF_MAX: ${{ vars.TIME_HANDOFF_MAX }}
+          TIME_CODEX_MODEL: ${{ vars.TIME_CODEX_MODEL }}

Until that lands, @codex answers "no TIME_SEAT_CODEX secret on this repo yet" or
"codex is not installed where this workflow runs", and @claude posts as Time.

Closes #79

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FAqjv4VTrzR61LLc4ifsjZ


sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.

From github: `claude/card-79-mentions-handoffs-rt569b` (git@github.com:teahubxyz/time.git), brought in by sync.py. Time's council reviews it like any PR. Commits: Mentions are handoffs: @codex and @claude dispatch headless seat runs as their own users (#79) Two seats beside @sonnet, @opus, @kimi..., neither of them in seats.json: - `@codex <ask>` runs a headless `codex exec --sandbox workspace-write --json` in a throwaway checkout cloned with `TIME_SEAT_CODEX`, under a wall-clock cap (`TIME_SEAT_RUN_MINUTES`, default 15; the process group is killed at it and nothing is pushed). Its last message is the reply, posted as `codex`. Work left in the tree is committed as `codex`, pushed to `codex/issue-N-<stamp>` with the seat's token, and the PR is opened as `codex`. The CLI's environment drops `TIME_*`, `ACTIONS_*`, `LANGFUSE_*`, `GITHUB_TOKEN` and the model keys, so the seat's hands never see Time's secrets, its own token included; `TIME_CODEX_MODEL` picks its model. - `@claude [haiku|sonnet|opus|fable] <ask>` runs a Claude seat (opus by default) with the repo context Time already assembles for reviews (the README, and on a PR its diff and checks) and posts as `claude` via `TIME_SEAT_CLAUDE`. Without the seat's token, or if Gitea refuses the comment, Time posts for it as before. Every posted reply is signed `<sub>codex via Time · <model></sub>`. A reply whose last line starts with another seat's @mention is a handoff: Time runs that seat on the reply, with no human between the stages. `@a x -> @b y -> @c z` is a chain of stages (arrow: `→`, `->`, `=>`); several seats in one stage, or on separate lines, run side by side, as the launcher's grammar already read them. `TIME_HANDOFF_MAX` (default 6) bounds the seat runs one comment may set off, so a ping-pong ends. No one hands off to itself, and a mention mid-sentence is still just a mention. Budget: `codex exec` books one ledger line (`pool: codex`, `launch: 1`, tokens from its `--json` events, `usd` null -- no price) and is paced by `budget_check("codex")`; the Claude seat books like every other call. Strangers' mentions are ignored as today. Tests in test_time.py (`ALL OK 79 handoffs`): the grammar and the handoff rule; `codex_clone`/`codex_push` against a real bare repo (the token rides the URL, never .git/config); a trusted `@codex` comment launching the runner with the seat token, committing and opening the PR as codex, and posting as codex; a stranger's doing nothing; a reply ending in `@claude ...` dispatching the next seat; an arrow chain running claude -> codex -> haiku, each stage reading the last; a ledger booking per run; the wall-clock cap tripping cleanly; the handoff limit; a missing token, a spent budget and a refused comment. WORKFLOW CHANGE FOR THE OPERATOR (.github/workflows/ is a protected path, so this commit stops at the code). In `.github/workflows/mention.yml`: - timeout-minutes: 8 + timeout-minutes: 25 # above TIME_SEAT_RUN_MINUTES (15) plus the clone and the handoffs steps: - uses: actions/checkout@v5 with: sparse-checkout: .github/time + - run: npm install -g @openai/codex@0.159.2 # `@codex` runs the CLI headless - run: python3 .github/time/time.py mention env: @@ TIME_DAILY_USD: ${{ vars.TIME_DAILY_USD }} TIME_MONTHLY_USD: ${{ vars.TIME_MONTHLY_USD }} + TIME_SEAT_CODEX: ${{ secrets.TIME_SEAT_CODEX }} # the `codex` Gitea user's token + TIME_SEAT_CLAUDE: ${{ secrets.TIME_SEAT_CLAUDE }} # the `claude` Gitea user's token + CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} # or OPENAI_API_KEY: codex's own auth + TIME_SEAT_RUN_MINUTES: ${{ vars.TIME_SEAT_RUN_MINUTES }} + TIME_HANDOFF_MAX: ${{ vars.TIME_HANDOFF_MAX }} + TIME_CODEX_MODEL: ${{ vars.TIME_CODEX_MODEL }} Until that lands, `@codex` answers "no TIME_SEAT_CODEX secret on this repo yet" or "`codex` is not installed where this workflow runs", and `@claude` posts as Time. Closes #79 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAqjv4VTrzR61LLc4ifsjZ --- <sub>sync.py: edit this branch on github; it is the copy's to write, and the next sync brings the change here.</sub>
time added 1 commit 2026-10-29 19:24:31 +00:00
Two seats beside @sonnet, @opus, @kimi..., neither of them in seats.json:

- `@codex <ask>` runs a headless `codex exec --sandbox workspace-write --json` in a
  throwaway checkout cloned with `TIME_SEAT_CODEX`, under a wall-clock cap
  (`TIME_SEAT_RUN_MINUTES`, default 15; the process group is killed at it and nothing
  is pushed). Its last message is the reply, posted as `codex`. Work left in the tree
  is committed as `codex`, pushed to `codex/issue-N-<stamp>` with the seat's token, and
  the PR is opened as `codex`. The CLI's environment drops `TIME_*`, `ACTIONS_*`,
  `LANGFUSE_*`, `GITHUB_TOKEN` and the model keys, so the seat's hands never see Time's
  secrets, its own token included; `TIME_CODEX_MODEL` picks its model.
- `@claude [haiku|sonnet|opus|fable] <ask>` runs a Claude seat (opus by default) with
  the repo context Time already assembles for reviews (the README, and on a PR its diff
  and checks) and posts as `claude` via `TIME_SEAT_CLAUDE`.

Without the seat's token, or if Gitea refuses the comment, Time posts for it as before.
Every posted reply is signed `<sub>codex via Time · <model></sub>`.

A reply whose last line starts with another seat's @mention is a handoff: Time runs that
seat on the reply, with no human between the stages. `@a x -> @b y -> @c z` is a chain of
stages (arrow: `→`, `->`, `=>`); several seats in one stage, or on separate lines, run
side by side, as the launcher's grammar already read them. `TIME_HANDOFF_MAX` (default 6)
bounds the seat runs one comment may set off, so a ping-pong ends. No one hands off to
itself, and a mention mid-sentence is still just a mention.

Budget: `codex exec` books one ledger line (`pool: codex`, `launch: 1`, tokens from its
`--json` events, `usd` null -- no price) and is paced by `budget_check("codex")`; the
Claude seat books like every other call. Strangers' mentions are ignored as today.

Tests in test_time.py (`ALL OK 79 handoffs`): the grammar and the handoff rule;
`codex_clone`/`codex_push` against a real bare repo (the token rides the URL, never
.git/config); a trusted `@codex` comment launching the runner with the seat token,
committing and opening the PR as codex, and posting as codex; a stranger's doing
nothing; a reply ending in `@claude ...` dispatching the next seat; an arrow chain
running claude -> codex -> haiku, each stage reading the last; a ledger booking per run;
the wall-clock cap tripping cleanly; the handoff limit; a missing token, a spent budget
and a refused comment.

WORKFLOW CHANGE FOR THE OPERATOR (.github/workflows/ is a protected path, so this
commit stops at the code). In `.github/workflows/mention.yml`:

    -    timeout-minutes: 8
    +    timeout-minutes: 25   # above TIME_SEAT_RUN_MINUTES (15) plus the clone and the handoffs
         steps:
           - uses: actions/checkout@v5
             with:
               sparse-checkout: .github/time
    +      - run: npm install -g @openai/codex@0.159.2   # `@codex` runs the CLI headless
           - run: python3 .github/time/time.py mention
             env:
    @@
               TIME_DAILY_USD: ${{ vars.TIME_DAILY_USD }}
               TIME_MONTHLY_USD: ${{ vars.TIME_MONTHLY_USD }}
    +          TIME_SEAT_CODEX: ${{ secrets.TIME_SEAT_CODEX }}     # the `codex` Gitea user's token
    +          TIME_SEAT_CLAUDE: ${{ secrets.TIME_SEAT_CLAUDE }}   # the `claude` Gitea user's token
    +          CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}         # or OPENAI_API_KEY: codex's own auth
    +          TIME_SEAT_RUN_MINUTES: ${{ vars.TIME_SEAT_RUN_MINUTES }}
    +          TIME_HANDOFF_MAX: ${{ vars.TIME_HANDOFF_MAX }}
    +          TIME_CODEX_MODEL: ${{ vars.TIME_CODEX_MODEL }}

Until that lands, `@codex` answers "no TIME_SEAT_CODEX secret on this repo yet" or
"`codex` is not installed where this workflow runs", and `@claude` posts as Time.

Closes #79

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv4VTrzR61LLc4ifsjZ
time added 1 commit 2026-10-29 20:05:23 +00:00
main brought the steward tick (#292/#80), the planner's 4-6 coarse cards with
their `Depends on:` lines (#284/#244), the loud-no-op router (#283/#211) and the
"a card branch is never rewound to its base" guard (#289/#277). This branch
brings `@codex`/`@claude` as seats with hands and the handoff chain (#79).

Only README.md conflicted, in the feature list: the Mentions bullet is this
branch's (codex, claude, the handoff chain), the Hands-off bullet is main's
(4 to 6 cards, each with what it waits on). Both bullets are kept.

CHANGELOG.md merged under the union driver, so both sides' `### Added` lines
stand. `time.py` and `test_time.py` auto-merged in disjoint regions and were
checked for integration rather than text: `codex_push` pushes with the seat's
own token through its own git calls, so main's new `create=False` gate on
`git_commit` (and `Hands._commit`) does not touch it; `book(model, usage,
**extra)` still takes main's callers unchanged while codex books `pool`/`launch`;
main's new `pull_cord`/`park`/`BranchGone` sit clear of the seat code; and no
definition from either side was dropped.

test_time.py (both sides' markers, `ALL OK 79 handoffs` and `ALL OK 277` /
`ALL OK steward` among them), test_seed.py (3) and test_housekeeping.py (10)
all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAqjv1AEsy9dHGKjivMsvS
Owner

Landed in main via the integration branch (#294, merge 830f0de): all five batch PRs merged in sequence with conflicts resolved, and test_time.py, test_seed.py and test_housekeeping.py passing.

— steward (Claude)

Landed in `main` via the integration branch (#294, merge `830f0de`): all five batch PRs merged in sequence with conflicts resolved, and test_time.py, test_seed.py and test_housekeeping.py passing. — steward (Claude)
teahubxyz closed this pull request 2026-10-29 21:54:38 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.