[ai] Use Start and Approve lifecycle.

This commit is contained in:
John Preston
2026-07-20 10:56:39 +04:00
parent 2eaffaef6c
commit 19f9732418
10 changed files with 390 additions and 350 deletions

View File

@@ -67,10 +67,10 @@ Early-escalation rule: if two consecutive ASSESS rounds produce the **same failu
the attempt budget chasing it.
UNRECOVERABLE conditions: the app reaches a login screen / `AUTH_KEY_DUPLICATED` and re-copying the
test account does not recover it; `test_TelegramForcePortable` is missing when SETUP runs; or a crash
has no usable diagnostic after one retry. A file-lock build error (`LNK1104`, `C1041`, access denied,
file in use) is a repository hard stop: do not retry or work around it; ask the user to close the app
and debugger.
test account does not recover it, or a crash has no usable diagnostic after one retry. Missing
`test_TelegramForcePortable` is a global environment hard stop, not a task `Block`. A file-lock build
error (`LNK1104`, `C1041`, access denied, file in use) is likewise a repository hard stop: do not
retry or work around it; ask the user to close the app and debugger.
## Handoff tokens
@@ -81,7 +81,7 @@ and debugger.
`GREEN_REF` to the resulting commit.
- **Test report** (`test.md`) is the only fix-agent handoff. Give it the latest Attempt/Run section,
especially Root cause / Fix hint and Failure signature. Reserve wrapper-owned `result.md` for the
published attempt-boundary result; never create `result<n>.md`.
final AI result or exceptional blocked boundary; never create `result<n>.md`.
## Commit message

View File

@@ -1,6 +1,6 @@
---
name: continue
description: Continue autonomous Telegram Desktop development from the shared ai-tdesktop repository. Use when the user invokes $continue or /continue, asks Codex to keep working through the AI queue, or wants one command to process the local inbox, resume active or previously blocked unfinished work, consume this checkout's claimed queue, and claim new work until nothing eligible remains.
description: Continue autonomous Telegram Desktop development from the shared ai-tdesktop repository. Use when the user invokes $continue or /continue, asks Codex to keep working through the AI queue, or wants one command to process the local inbox, resume this checkout's unfinished work, and start ready shared tasks until nothing eligible remains.
---
# Continue AI Work
@@ -11,8 +11,8 @@ plan or implement Telegram changes in this scheduler session.
This is the default development command and the successor to the old `task` and
`implement` workflows. Inbox processing owns request splitting and project
routing; `perform-task` owns all mature context, planning, implementation,
review, Debug build, test-loop, evidence, and publication behavior.
routing; `perform-task` owns context, planning, implementation, review, Debug
build, test-loop, evidence, and final publication.
## Resolve the workspace
@@ -27,150 +27,130 @@ Use `python` or `py -3` when appropriate. Save `checkout_tag`, `ai_main`,
`slot_worktree`, and `source_root` from its JSON. Read `ai_main/AGENTS.md`.
Stop before mutating anything when `violations` is nonempty, AI master is
dirty, or the AI slot has changes outside its one active task. Never clean,
stash, reset, or absorb unrelated changes. Unpublished AI slot commits are
resumable state: retry the helper's `publish` command before selecting new
work.
dirty, or AI slot changes are not wholly inside its one active task. Never
clean, stash, reset, or absorb unrelated changes. Dirty task-scoped AI files
are the active checkout's local resumable phase state and are expected.
Unpublished clean AI slot commits are incomplete publication; run the helper's
`publish` command before selecting work.
`status` and `claimed_by` are orthogonal:
The canonical lifecycle is deliberately small:
- `todo` plus `claimed_by: null` is shared unreserved work;
- `todo` plus this `checkout_tag` is this checkout's reserved queue;
- `in-progress` plus this tag is the one active task;
- `blocked` plus this tag is paused unfinished work that the next invocation
retries once;
- `approved` is the only completed terminal state;
- work claimed by another checkout is invisible to this scheduler.
- `todo` with `claimed_by: null` is ready shared work;
- `in-progress` with this `checkout_tag` is this checkout's one active task;
- `blocked` with this tag is a rare published unfinished boundary;
- `approved` is the only completed terminal state.
Do not infer a claim from who processed an inbox receipt. Do not steal or
expire another checkout's claim.
`Start` atomically assigns an unclaimed task and changes it to `in-progress`.
Normal phase artifacts remain local and uncommitted in the slot worktree.
`Approve` publishes all final AI artifacts and state in one commit. `Block` is
permitted only for a genuine exhausted implementation or verification blocker,
not for an interrupted agent session. Never publish `Claim`, phase checkpoint,
or `Resume` commits. Existing claimed `todo` records from the older workflow
remain startable but do not justify creating new reservations.
Never infer ownership from an inbox receipt. Do not steal work from another
checkout. Moving an unfinished task to another checkout is a rare explicit
human reassignment that may restart the task and discard checkout-local phase
artifacts; it is never automatic scheduler behavior.
## Interpret scope hints
Treat the invoking request after `$continue` or `/continue` as optional natural
language scheduling guidance, not a required command grammar.
Treat text after `$continue` or `/continue` as optional natural-language
priority guidance. It may prefer tasks from one receipt, project, or explicit
list, but it does not reserve a batch. Start only one shared task at a time,
finish or exceptionally block it, refresh canonical state, then choose again.
A plain invocation claims one shared task at a time. This minimizes abandoned
reservations and lets parallel checkouts distribute the backlog.
When the user expressly asks to reserve or claim a group, claim the matching
ordered set in one operation. Common scopes include:
- all tasks created from the inbox receipt processed by this invocation;
- all unclaimed tasks for a named project;
- an explicit list of friendly titles or task identifiers.
Preserve receipt order, project index order, or explicit user order when one is
available; otherwise use creation date and task identifier. A batch receives
one `claimed_at` value and ascending `claim_order`. Dependencies do not prevent
reservation, but they do prevent a task from starting.
Scope hints filter new claims only. Always resume this checkout's existing
`in-progress` task, previously blocked work, and already claimed queue before
taking more shared work, unless the user explicitly asks to stop or reassign
them.
Always resume this checkout's `in-progress` task and previously blocked work
before applying a priority hint to new shared work, unless the user expressly
asks to stop or reassign it.
## Main loop
Create an empty invocation-local `attempted_blocked` set, then repeat these
steps. Refresh queue JSON after every delegated operation and state transition;
do not rely on a stale snapshot. The set is scheduler memory only and is never
written to task state.
Create an empty invocation-local `attempted_blocked` set, then repeat. Refresh
queue JSON after every delegated operation and state transition.
### 1. Process the inbox
### 1. Resume active work
When `inbox_nonempty` is true, spawn one inbox worker with `fork_turns: "none"`.
Give it the source checkout path and instruct it to read and use
`.agents/skills/process-inbox/SKILL.md` completely. It owns exactly one inbox
transaction, may use the bounded planner delegation required by that skill,
must not implement tasks, and must return the receipt and created task ids.
If this checkout has an `in-progress` task, select it and spawn one performer.
Its task-scoped dirty AI artifacts are the resumption handoff. There must be at
most one active task. Resume it before inbox processing because the inbox worker
requires a clean slot; after the task finishes, the next loop iteration handles
the inbox.
### 2. Process the inbox
When no task is active and `inbox_nonempty` is true, spawn one inbox worker
with `fork_turns: "none"`. Give it the source checkout path and instruct it to
read and use `.agents/skills/process-inbox/SKILL.md` completely. It owns exactly
one inbox transaction, may use the bounded planner delegation required by that
skill, must not implement tasks, and must return the receipt and created ids.
Wait in intervals no longer than 60 seconds. A timeout is not failure. Inspect
the saved target after every wake and validate the receipt plus refreshed queue
before proceeding. Never launch a second inbox worker for the same transaction.
before proceeding. Never launch a second worker for the same transaction. If
it cannot publish durable AI state, stop with the inbox transaction recoverable.
If inbox processing cannot publish durable AI state, stop. The inbox skill must
leave the input or active transaction recoverable.
### 3. Retry rare blocked work
### 2. Resume active work
If this checkout has an `in-progress` task, select it. There must be at most
one. Spawn one stateful performer as described below.
### 3. Retry paused unfinished work
Otherwise select the first ready task in this checkout's `own_blocked` queue
whose id is not in `attempted_blocked`. Readiness means every `depends_on` task
is `approved`. Add its id to `attempted_blocked` before changing state, then
transition it atomically:
Otherwise select the first ready task in `own_blocked` whose id is not in
`attempted_blocked`. Readiness means every dependency is `approved`. Add its id
to the set, then reopen it locally:
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py retry \
--task <YYYY/MM/DD/slug>
```
The retry preserves the claim and all source, work, test, result, and evidence
artifacts, changes the task back to `in-progress`, and resumes from the first
incomplete validated boundary. It clears only `work/discovered-routed.md`, when
present, so discoveries in the next result are deduplicated and routed again.
Never discard or redo completed work merely because the prior run ended
`blocked`.
This preserves its ownership, source recovery refs, plans, reviews, tests,
result, and evidence while changing the slot worktree back to local
`in-progress`. It publishes no `Resume` commit. Spawn its performer at the
first incomplete validated boundary.
If a performer ends `blocked` during this invocation, add that task id to
`attempted_blocked`. Do not retry it again in the same invocation; continue
with other independent work. A later `$continue` or `/continue` starts with a
fresh set and tries it again.
If it blocks again, leave the new canonical `Block` boundary and do not retry
it again in this invocation. Independent work may continue; the next
invocation gets a fresh retry set.
### 4. Start reserved work
### 4. Start older reserved work
Otherwise select the first ready task in this checkout's claimed `todo` queue.
Readiness means every `depends_on` task is `approved`. Transition it atomically:
Otherwise select the first ready legacy `todo` task already owned by this
checkout and start it:
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py start \
--task <YYYY/MM/DD/slug>
```
Then spawn its performer. Leave claimed tasks with unfinished prerequisites as
`todo` and consider later ready tasks.
The resulting canonical `Start` commit changes it to `in-progress`. Leave
legacy reservations with unfinished dependencies untouched and consider later
ready work.
### 5. Claim shared work
### 5. Start shared work
Otherwise inspect unclaimed `todo` work matching the scope. For a plain
invocation select only the first ready task. For an explicit batch reservation,
reserve every matching task in the chosen order, including later tasks whose
dependencies are not approved yet, and pass one `--task` argument per task:
Otherwise select the first ready unclaimed `todo` task matching the priority
hint, or the first ready task in normal queue order. Start it with the same
helper command. `start` atomically assigns and activates the task, then
publishes its canonical `Start` commit before source work begins.
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py claim \
--task <first-id> \
--task <second-id>
```
The claim is committed and published before source work starts. Refresh the
queue; then return to step 3. A publish race may mean another checkout won the
task. Do not resolve that by overwriting shared state; refresh and choose again.
A concurrent start may mean another checkout won the task. Never overwrite
shared state; refresh and choose again.
### 6. Stop normally
Stop when the inbox is empty and none of these exist:
- this checkout's active task;
- a ready task in this checkout's blocked queue that is not in
`attempted_blocked`;
- a ready task in this checkout's claimed queue;
- a ready unclaimed task for a plain run, or any unclaimed task matching an
explicit batch-reservation scope.
- a ready blocked task not attempted in this invocation;
- a ready legacy reserved task;
- a ready unclaimed task matching the invocation's priority scope.
Blocked or claimed tasks belonging to other checkouts do not keep this run
alive. Claimed tasks waiting on prerequisites and blocked tasks already tried
in this invocation remain visible in the final summary but do not cause a busy
loop.
Work owned by another checkout does not keep this run alive. A blocked task
already attempted in this invocation remains visible in the final summary but
does not cause a busy loop.
## Spawn one performer
Spawn exactly one performer for the selected task with `fork_turns: "none"` and
a unique tool-valid name. Tell it:
Spawn exactly one performer for the selected task with `fork_turns: "none"`
and a unique tool-valid name. Tell it:
```text
Use .agents/skills/perform-task/SKILL.md completely.
@@ -178,74 +158,57 @@ Source checkout: <source_root>
AI slot worktree: <slot_worktree>
Checkout tag: <checkout_tag>
Task: <task-id>
Own this task until this attempt reaches approved or blocked. You may use the
bounded leaf delegation required by the skill. Do not select or claim another
task.
Own this task until it is approved, genuinely blocked, or reaches a global
hard stop. You may use the bounded leaf delegation required by the skill. Do
not select or start another task.
```
The performer is stateful. Never duplicate it. Poll at no more than 60-second
intervals, distinguish progress from completion using its task artifacts, and
send a follow-up to the same target if it becomes idle without an
attempt-boundary state.
follow up with the same target if it becomes idle before a valid boundary.
After it returns, require:
After it returns, require one of:
- source checkout clean at the performer's recorded run tip, with its retained
implementation commit in current history when one exists;
- task `state.yaml` at an attempt boundary (`approved` or `blocked`) and
published to AI master; or
- a clearly reported global hard stop that makes further work unsafe.
- source checkout clean at the recorded run tip and task `approved` on
canonical AI master;
- source checkout clean and task exceptionally `blocked` on canonical master,
with exact unverified behavior;
- a clearly reported global hard stop, leaving the task `in-progress` and all
task-scoped local state recoverable for the next invocation.
A clean `blocked` attempt does not complete the task and does not stop the
scheduler. Add it to `attempted_blocked` and continue with independent work;
the next invocation retries it before reserved or shared work. A dirty
checkout, file-lock build failure, missing test account, unresolved AI
publication conflict, or other global environment failure stops the loop.
An interruption or environment stop never becomes a convenience `Block`.
After a genuine `Block`, add the task id to `attempted_blocked` and continue
with independent work. A dirty source checkout, file-lock build failure,
missing test account, unsafe publication conflict, or comparable global safety
failure stops the loop.
The missing `test_TelegramForcePortable` golden account is the only
portable-folder state that is a global stop. Live and real portable folders
coexisting, either one being absent, or any ownership-marker state must be
reconciled by `perform-task` according to the shared test-loop protocol and
must never stop `/continue`.
portable-folder global stop. All live/real folder combinations must be
reconciled by `perform-task` according to the shared test-loop protocol.
## Route discovered follow-ups
After every performer attempt ending `approved` or `blocked`, read its published
`work/result.md`. If it says
`Discovered: present` and has no `work/discovered-routed.md`, route the complete
blocks under `## Discovered tasks` before selecting more shared work.
After every canonical `Approve` or `Block`, read `work/result.md`. If it says
`Discovered: present` and lacks `work/discovered-routed.md`, route the complete
blocks before selecting more shared work.
Spawn one disposable routing worker with `fork_turns: "none"`. Tell it to read
the routing, splitting, task-path, artifact, validation, and publication rules
in `.agents/skills/process-inbox/SKILL.md`, but not to call `prepare`,
`finalize`, or `abort`: its immutable input is the published result, not the
human inbox. It must not edit Telegram source, claim, or implement work.
`finalize`, or `abort`. Its immutable input is the result, not the inbox. It
must not edit Telegram source, start tasks, or implement work.
The worker must:
1. deduplicate against existing tasks and discovery receipts;
2. preserve each independently testable follow-up, its provenance, and valid
dependencies;
3. create dated unclaimed `todo` tasks and any justified project/index updates;
4. create `receipts/YYYY/MM/DD/discovered-<source-slug>.md` mapping every block;
5. write the source task's `work/discovered-routed.md` with the receipt, new or
reused task ids;
6. stage only those explicit paths, commit with
`Route follow-ups from <source-task-id>`, then run the workspace helper's
`publish` command.
The worker must retry an ordinary concurrent-master race using the helper. A
semantic conflict or unavailable remote preserves the slot commit and stops
the scheduler. The routing marker makes later `/continue` runs idempotent.
Refresh queue JSON after routing, then resume the main loop.
The worker must deduplicate existing tasks, create independently testable
unclaimed `todo` tasks and justified project updates, write a discovery
receipt, and write the source task's routing marker. It stages only those
paths, commits `Route follow-ups from <source-task-id>`, and publishes with the
workspace helper. Retry ordinary concurrent-master races; preserve a semantic
conflict or unavailable-remote slot commit and stop.
## Report
Return one compact run summary: inbox receipt if processed, tasks approved,
unfinished blocked tasks with exact unverified behavior and whether they were
retried in this invocation, tasks newly claimed or left queued, routed
discoveries, elapsed time, and why the loop stopped. Make any global hard stop,
retained unsafe state, or incomplete verification visually unmistakable. The
human should not need to invoke another command merely to advance to the next
eligible task. Never include source or AI commit hashes; task ids are the only
durable locators.
Return one compact summary: inbox receipt if processed, tasks approved,
exceptionally blocked tasks with exact unverified behavior and retry status,
tasks started or left queued, routed discoveries, elapsed time, and why the
loop stopped. Make any global hard stop or unsafe state unmistakable. Never
include source or AI commit hashes; task ids are the only durable locators.

View File

@@ -1,4 +1,4 @@
interface:
display_name: "Continue AI Work"
short_description: "Resume unfinished work and drain the AI queue"
default_prompt: "Use $continue to process the inbox, retry paused unfinished tasks, and continue queued Telegram Desktop work."
default_prompt: "Use $continue to process the inbox, resume unfinished work, and start ready Telegram Desktop tasks."

View File

@@ -1,14 +1,13 @@
---
name: perform-task
description: Resolve, claim, resume, implement, commit, and verify exactly one existing ai-tdesktop task by short slug or full dated id, including previously blocked unfinished work. Use when the user invokes $perform-task or /perform-task with a known task name, or when the continue scheduler delegates one selected task. Runs the complete mature context, planning, assessment, Debug build, review, test-loop, Computer Use, resume, and publication pipeline without selecting any additional work.
description: Resolve, start or resume, implement, commit, and verify exactly one existing ai-tdesktop task by short slug or full dated id, including rare blocked unfinished work. Use when the user invokes $perform-task or /perform-task with a known task name, or when the continue scheduler delegates one selected task. Runs the complete context, planning, assessment, Debug build, review, test-loop, Computer Use, recovery, and final publication pipeline without selecting additional work.
---
# Perform One AI Task
Own exactly one task through a Telegram commit and a published attempt-boundary
AI commit. Do not
process the inbox, split the task, drain the queue, or select a follow-up after
this attempt reaches `approved` or `blocked`.
Own exactly one task through a Telegram commit and a canonical AI `Approve` or
exceptional `Block`. Do not process the inbox, split the task, drain the queue,
or select a follow-up afterward.
## Read the complete engine
@@ -34,92 +33,77 @@ python3 .agents/skills/process-inbox/scripts/workspace.py resolve \
```
Use `python` or `py -3` when appropriate. The helper reads the ignored machine
tag, derives the checkout tag, synchronizes clean AI state, and resolves:
tag, derives the checkout tag, synchronizes clean AI state, and resolves an
exact full id, exact final path slug, or exact normalized friendly title.
Prefer a unique unfinished match over approved history. Never guess among
several unfinished matches; report their full ids.
1. an exact full id such as `2026/07/19/fix-community-forward`;
2. an exact final path slug such as `fix-community-forward`;
3. an exact normalized friendly task title.
Prefer a unique unfinished match when approved history has the same slug.
Never guess among several unfinished matches; report their full ids so the
human can choose one.
An interactive `/perform-task` or `$perform-task` invocation requires a
nonempty name. If none was supplied, ask for the friendly short name or full
id and do not select from the queue. A `continue` delegation always supplies
the full id.
When invoked by `continue`, accept its explicit `source_root`, `slot_worktree`,
`checkout_tag`, and full `task_id`, but still run `resolve` and verify they
match local discovery.
An interactive invocation requires a nonempty name. If none was supplied, ask
for the friendly short name or full id. A `continue` delegation always supplies
the full id and explicit workspace values; still resolve and verify them.
If `commits.slot_only` is nonzero and the slot is clean, run the helper's
`publish` command and resolve again before changing ownership. If the slot is
dirty, permit it only for an already `in-progress` task owned by this checkout
and only within that task's allowed paths; the pipeline preflight will validate
resumption ownership. Any other dirty or divergent state is a hard stop. Never
discard an unpublished checkpoint.
`publish` command and resolve again. A dirty slot is valid only when every
change belongs to this checkout's one `in-progress` task. Those files are local
resumable phase state; never discard them. Any unrelated dirty or divergent
state is a hard stop.
## Acquire exactly this task
Inspect the resolved task, its `ready` value, `other_active_task`, state, and
owner before source work.
Inspect the resolved task, readiness, `other_active_task`, status, and owner.
- If another task is already `in-progress` for this checkout, stop. Never
abandon or supersede it implicitly.
- If another task is already `in-progress` for this checkout, stop.
- If this task is `approved`, report its completed result and stop.
- If it is claimed by another checkout, stop without touching it.
- If it is unclaimed but its dependencies are unfinished, report those
dependencies and stop without claiming it.
- If it is ready and unclaimed, claim only this full id:
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py claim \
--task <full-task-id>
```
- If it is `todo` and owned by this checkout, start it:
- If it is owned by another checkout, stop. Cross-checkout restart is a rare
explicit human reassignment, never an implicit steal.
- If its dependencies are unfinished, report them and stop without starting.
- If it is `todo` and either unclaimed or owned by this checkout, atomically
assign and activate it:
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py start \
--task <full-task-id>
```
- If it is `blocked` and owned by this checkout, retry it:
- If it is `blocked` and owned by this checkout, reopen it locally:
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py retry \
--task <full-task-id>
```
Preserve its claim, implementation, plans, reviews, tests, result, and
evidence. Treat the prior blocked result as the exact resumption handoff and
continue from the first incomplete validated boundary rather than starting
over.
Preserve all source recovery, plans, reviews, tests, result, and evidence.
Continue from the first incomplete validated boundary. This creates no
`Resume` commit.
- If it is already `in-progress` and owned by this checkout, resume it without
another claim, start, or retry commit.
another state commit.
Refresh with `resolve` after every state mutation. The source pipeline starts
only after canonical AI state shows this task `in-progress` for this checkout.
Refresh with `resolve` after each mutation. The source pipeline begins only
after the slot state shows this task `in-progress` for this checkout. For a new
task, canonical master must already contain its `Start` commit.
## Run and publish
Execute `references/pipeline.md` exactly. The task must normally produce:
Execute `references/pipeline.md` exactly. A normal task produces:
1. one or more tested Telegram implementation-attempt commits, each with an
exact one-line subject, blank line, and `Task: <full-task-id>`;
2. tracked resumable AI checkpoints during phase work;
3. an attempt-boundary AI slot commit containing final result/state, rebased and
published to canonical AI master without force.
2. local tracked phase artifacts and progress in the AI slot worktree, without
phase commits;
3. one canonical `Approve <full-task-id>` commit containing all final AI
artifacts and state.
Do not report success from a source commit alone. The attempt-boundary AI
commit must also be canonical. On a retryable concurrent-master race, keep
fetching, rebasing, and publishing until it succeeds. On a semantic conflict,
unsafe checkout, or unreachable remote, preserve resumable state and report a
hard stop.
Only a genuine exhausted implementation or verification blocker produces a
canonical `Block <full-task-id>` commit. Agent interruption, tool loss, and
global environment stops leave the task `in-progress` with its task-scoped
local state intact for the next invocation.
Return a compact result with the full task id, attempt status or hard stop,
attempts, touched files, canonical-publication confirmation, and exact evidence
or unverified behavior. Never persist or report commit hashes; the full task id
is the only cross-repository link.
Do not report success from a source commit alone. The final AI commit must be
canonical. Retry ordinary concurrent-master publication races until success.
On a semantic conflict, unsafe checkout, or unreachable remote, preserve
resumable state and report a hard stop.
Return a compact result with the full task id, status or hard stop, attempts,
touched files, canonical final-publication confirmation, and exact evidence or
unverified behavior. Never persist or report commit hashes; the full task id is
the only cross-repository link.

View File

@@ -1,4 +1,4 @@
interface:
display_name: "Perform AI Task"
short_description: "Implement or resume one Telegram AI task"
default_prompt: "Use $perform-task to resolve, resume when blocked, implement, verify, and publish one named ai-tdesktop task."
default_prompt: "Use $perform-task to start or resume, implement, verify, and publish one named ai-tdesktop task."

View File

@@ -4,12 +4,12 @@
- [Contract and inputs](#contract-and-inputs)
- [Preflight](#preflight)
- [Artifacts and resumption](#artifacts-and-resumption)
- [Local artifacts and resumption](#local-artifacts-and-resumption)
- [Delegation](#delegation)
- [Implementation phases](#implementation-phases)
- [Telegram commits](#telegram-commits)
- [Test loop adapter](#test-loop-adapter)
- [Attempt-boundary state](#attempt-boundary-state)
- [Final AI state](#final-ai-state)
- [Failure handling](#failure-handling)
## Contract and inputs
@@ -103,7 +103,7 @@ Do not stash. Do not reset, restore, stage, commit, or delete an unexpected
path. Invocation authorizes recovery only for paths proven to belong to this
task and only back to `RUN_REF` or `BASE_REF`, as appropriate.
## Artifacts and resumption
## Local artifacts and resumption
Use tracked, resumable task artifacts:
@@ -142,17 +142,20 @@ repeat an approved phase merely because the prior agent session disappeared.
Treat a compact subagent reply as a notification; the artifact and repository
state are proof.
At each stable boundary update `work/progress.md` and publish a checkpoint:
At each stable boundary update `work/progress.md` and record the current phase
locally:
```bash
python3 SOURCE_ROOT/.agents/skills/process-inbox/scripts/workspace.py \
checkpoint --source-root SOURCE_ROOT --task TASK_ID --phase PHASE
```
Checkpoint after context, assessed plan, each completed implementation phase
when useful for recovery, the retained implementation commit, review, and each
material test attempt. Never checkpoint a half-written artifact. A checkpoint
commit must be rebased and published to AI master before later phase work.
Record progress after context, assessed plan, each completed implementation
phase when useful for recovery, the retained implementation commit, review,
and each material test attempt. Never mark a half-written artifact complete.
The helper changes only task-scoped files in the slot worktree and publishes no
checkpoint commit. Keep this local dirty state until the final `Approve` or
exceptional `Block` commit captures the whole task record.
## Delegation
@@ -313,14 +316,14 @@ Skip runtime testing only for a task with no runnable behavior. Record
`NOT_APPLICABLE` and exact file-level validation. Configuration alone is not a
reason to skip.
## Attempt-boundary state
## Final AI state
Before publishing an approved or blocked attempt, require a clean Telegram
checkout at `RUN_REF`, with `GREEN_REF` in its history when an implementation
is retained, no overlay in source, no owned live test copy, and no
overlay-bearing executable. For implementation-blocked work with no retained
commit, restore only proven owned paths to `BASE_REF`. For test-blocked work
retain the latest implementation commit and state the exact unverified
Before publishing an approved result or genuine blocked boundary, require a
clean Telegram checkout at `RUN_REF`, with `GREEN_REF` in its history when an
implementation is retained, no overlay in source, no owned live test copy, and
no overlay-bearing executable. For implementation-blocked work with no
retained commit, restore only proven owned paths to `BASE_REF`. For test-blocked
work retain the latest implementation commit and state the exact unverified
behavior.
Write `work/result.md` with exactly one value for every field:
@@ -348,8 +351,7 @@ For approved project work, promote `work/project.proposed.md` to the project's
`project.md` immediately before final AI publication. For blocked work, retain
the proposal only as a task artifact.
Publish the attempt-boundary AI state only after the Telegram commit and result
are final:
Publish final AI state only after the Telegram commit and result are final:
```bash
python3 SOURCE_ROOT/.agents/skills/process-inbox/scripts/workspace.py \
@@ -359,13 +361,14 @@ python3 SOURCE_ROOT/.agents/skills/process-inbox/scripts/workspace.py \
The helper verifies a clean source checkout, local task refs, current `HEAD`,
and the retained implementation's exact three-line commit message. It commits
the task result and state in the AI slot, fetches newer canonical state when
configured, rebases the slot, publishes without force, and fast-forwards local
AI master. It deletes all local task refs after approval; after a block it
deletes only `RUN_REF` and retains implementation recovery refs for the next
invocation. Do not report an attempt boundary until that AI commit reaches
canonical master. Preserve an unpublished slot commit on a semantic conflict
or remote outage and hard-stop instead of pretending completion.
all task-scoped local artifacts and final state as `Approve <TASK_ID>` or the
exceptional `Block <TASK_ID>`, fetches newer canonical state when configured,
rebases the slot, publishes without force, and fast-forwards local AI master.
It deletes all local task refs after approval; after a block it deletes only
`RUN_REF` and retains implementation recovery refs for the next invocation.
Do not report final state until that AI commit reaches canonical master.
Preserve an unpublished final slot commit on a semantic conflict or remote
outage and hard-stop instead of pretending completion.
When `Discovered: present`, preserve complete task blocks in `result.md`. The
`continue` scheduler must route them through the same independent-testability
@@ -374,12 +377,14 @@ planner into new unclaimed dated tasks before selecting more shared work.
## Failure handling
- A disposable phase may be retried once through the wait ladder. Never fresh
retry the performer within the same attempt; a later `continue` invocation
creates one new performer to resume a published blocked task.
retry the performer within the same attempt. An interruption leaves local
task state `in-progress`; a later `continue` invocation resumes it. A later
invocation reopens a published blocked task locally without a `Resume`
commit.
- A clean `blocked` attempt leaves the task unfinished. It lets `continue`
proceed with independent work, but the next invocation retries it once before
reserved or shared work. A dirty/non-buildable checkout or global environment
problem stops the current invocation.
starting new shared work. A dirty/non-buildable checkout or global
environment problem stops the current invocation.
- A file-lock build error always stops immediately and asks the human to close
this checkout's Telegram/debugger.
- Missing optional screenshots or mockups never block.

View File

@@ -630,64 +630,6 @@ def command_resolve(args):
}, indent=2, sort_keys=True))
def command_claim(args):
config = worktree_config(args, create=True)
sync_canonical(config)
slot = Path(config["slot_worktree"])
states = load_states(slot)
tasks = []
for task_id in args.task:
if task_id in (task["id"] for task in tasks):
raise WorkspaceError(f"Task was listed twice: {task_id}")
task = states.get(task_id)
if task is None:
raise WorkspaceError(f"Task does not exist: {task_id}")
if task["status"] != "todo" or task["claimed_by"] is not None:
raise WorkspaceError(f"Task is not unclaimed todo work: {task_id}")
tasks.append(task)
claimed_at = datetime.datetime.now().astimezone().isoformat()
paths = []
for order, task in enumerate(tasks, 1):
path = state_path(slot, task["id"])
update_state(path, {
"claimed_by": config["checkout_tag"],
"claimed_at": claimed_at,
"claim_order": order,
"lease_until": None,
})
paths.append(str(path.relative_to(slot)))
subject = (
f"Claim {tasks[0]['id']} for {config['checkout_tag']}"
if len(tasks) == 1
else f"Claim {len(tasks)} tasks for {config['checkout_tag']}"
)
try:
commit = commit_paths(config, paths, subject)
except WorkspaceError as error:
main_states = load_states(Path(config["ai_main"]))
lost = [
task["id"] for task in tasks
if task["id"] in main_states
and (
main_states[task["id"]]["status"] != "todo"
or main_states[task["id"]]["claimed_by"] is not None
)
]
counts = unpublished_counts(config)
if lost and counts["slot_only"] == 1:
head = run_git(slot, "rev-parse", "HEAD").stdout.strip()
run_git(slot, "rebase", "--onto", "master", head)
raise WorkspaceError(
"Claim lost to newer master for " + ", ".join(lost)
) from error
raise
print(json.dumps({
"checkout_tag": config["checkout_tag"],
"claimed": [task["id"] for task in tasks],
"published": bool(commit),
}, indent=2, sort_keys=True))
def command_start(args):
config = worktree_config(args, create=True)
sync_canonical(config)
@@ -696,8 +638,13 @@ def command_start(args):
task = states.get(args.task)
if task is None:
raise WorkspaceError(f"Task does not exist: {args.task}")
if task["status"] != "todo" or task["claimed_by"] != config["checkout_tag"]:
raise WorkspaceError(f"Task is not claimed todo work for this checkout: {args.task}")
if task["status"] != "todo" or task["claimed_by"] not in (
None,
config["checkout_tag"],
):
raise WorkspaceError(
f"Task is not available todo work for this checkout: {args.task}"
)
if not task_ready(task, states):
raise WorkspaceError(f"Task has unfinished dependencies: {args.task}")
active = [
@@ -708,12 +655,44 @@ def command_start(args):
if active:
raise WorkspaceError("Another task is already in progress: " + ", ".join(active))
path = state_path(slot, args.task)
update_state(path, {"status": "in-progress", "phase": "setup"})
commit = commit_paths(
config,
[str(path.relative_to(slot))],
f"Start {args.task} on {config['checkout_tag']}",
)
update_state(path, {
"status": "in-progress",
"claimed_by": config["checkout_tag"],
"claimed_at": (
task["claimed_at"]
or datetime.datetime.now().astimezone().isoformat()
),
"claim_order": task["claim_order"] or 1,
"lease_until": None,
"phase": "setup",
})
try:
commit = commit_paths(
config,
[str(path.relative_to(slot))],
f"Start {args.task} on {config['checkout_tag']}",
)
except WorkspaceError as error:
main_states = load_states(Path(config["ai_main"]))
main_task = main_states.get(args.task)
lost = (
main_task is not None
and (
main_task["status"] != "todo"
or main_task["claimed_by"] not in (
None,
config["checkout_tag"],
)
)
)
counts = unpublished_counts(config)
if lost and counts["slot_only"] == 1:
head = run_git(slot, "rev-parse", "HEAD").stdout.strip()
run_git(slot, "rebase", "--onto", "master", head)
raise WorkspaceError(
f"Start lost to newer master for {args.task}"
) from error
raise
print(json.dumps({
"task": args.task,
"status": "in-progress",
@@ -751,20 +730,14 @@ def command_retry(args):
"phase": "resume",
"lease_until": None,
})
paths = [str(path.relative_to(slot))]
routed = path.parent / "work" / "discovered-routed.md"
if routed.is_file():
routed.unlink()
paths.append(str(routed.relative_to(slot)))
commit = commit_paths(
config,
paths,
f"Resume {args.task} on {config['checkout_tag']}",
)
print(json.dumps({
"task": args.task,
"status": "in-progress",
"published": bool(commit),
"local": True,
"published": False,
}, indent=2, sort_keys=True))
@@ -785,18 +758,14 @@ def task_action_config(args, require_status="in-progress", allow_project=False):
def command_checkpoint(args):
config, slot = task_action_config(args)
_, slot = task_action_config(args)
path = state_path(slot, args.task)
update_state(path, {"phase": args.phase})
commit = commit_paths(
config,
[task_relative_dir(args.task)],
f"Checkpoint {args.task}: {args.phase}",
)
print(json.dumps({
"task": args.task,
"phase": args.phase,
"published": bool(commit),
"local": True,
"published": False,
}, indent=2, sort_keys=True))
@@ -1247,11 +1216,6 @@ def parse_args():
resolve.add_argument("--name", required=True)
resolve.set_defaults(handler=command_resolve)
claim = subparsers.add_parser("claim")
add_common_arguments(claim)
claim.add_argument("--task", action="append", required=True)
claim.set_defaults(handler=command_claim)
start = subparsers.add_parser("start")
add_common_arguments(start)
start.add_argument("--task", required=True)

View File

@@ -31,23 +31,29 @@ def task_state(status, claimed_by="macbook-twork"):
}
def write_task(slot, status="blocked"):
def write_task(slot, status="blocked", claimed_by="macbook-twork"):
directory = slot / "tasks" / TASK_ID
(directory / "work").mkdir(parents=True)
(directory / "task.md").write_text(
"# Correct recent-search peer actions\n",
encoding="utf-8",
)
owner = claimed_by or "null"
claimed_at = "2026-07-19T14:28:01+04:00" if claimed_by else "null"
claim_order = "1" if claimed_by else "null"
phase = "blocked" if status == "blocked" else (
"setup" if status == "in-progress" else "null"
)
(directory / "state.yaml").write_text(
f"""status: {status}
created: 2026-07-19
project: null
depends_on: []
claimed_by: macbook-twork
claimed_at: 2026-07-19T14:28:01+04:00
claim_order: 1
claimed_by: {owner}
claimed_at: {claimed_at}
claim_order: {claim_order}
lease_until: null
phase: {status}
phase: {phase}
inbox_receipt: receipts/2026/07/19/test.md
""",
encoding="utf-8",
@@ -91,7 +97,7 @@ class WorkspaceTest(unittest.TestCase):
with (
mock.patch.object(workspace, "worktree_config", return_value=config),
mock.patch.object(workspace, "sync_canonical"),
mock.patch.object(workspace, "commit_paths", return_value=True) as commit,
mock.patch.object(workspace, "commit_paths") as commit,
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_retry(SimpleNamespace(task=TASK_ID))
@@ -100,9 +106,127 @@ class WorkspaceTest(unittest.TestCase):
self.assertEqual(state["status"], "in-progress")
self.assertEqual(state["phase"], "resume")
self.assertFalse(routed.exists())
paths = commit.call_args.args[1]
self.assertIn(f"tasks/{TASK_ID}/state.yaml", paths)
self.assertIn(f"tasks/{TASK_ID}/work/discovered-routed.md", paths)
commit.assert_not_called()
def test_start_atomically_assigns_unclaimed_todo(self):
with tempfile.TemporaryDirectory() as temporary:
slot = Path(temporary)
directory = write_task(slot, status="todo", claimed_by=None)
config = {
"checkout_tag": "macbook-twork",
"slot_worktree": str(slot),
}
with (
mock.patch.object(workspace, "worktree_config", return_value=config),
mock.patch.object(workspace, "sync_canonical"),
mock.patch.object(workspace, "commit_paths", return_value=True) as commit,
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_start(SimpleNamespace(task=TASK_ID))
state = workspace.load_state(slot, directory / "state.yaml")
self.assertEqual(state["status"], "in-progress")
self.assertEqual(state["claimed_by"], "macbook-twork")
self.assertIsNotNone(state["claimed_at"])
self.assertEqual(state["claim_order"], 1)
self.assertEqual(state["phase"], "setup")
self.assertEqual(
commit.call_args.args[2],
f"Start {TASK_ID} on macbook-twork",
)
def test_checkpoint_updates_only_local_task_state(self):
with tempfile.TemporaryDirectory() as temporary:
slot = Path(temporary)
directory = write_task(slot, status="in-progress")
config = {"checkout_tag": "macbook-twork"}
with (
mock.patch.object(
workspace,
"task_action_config",
return_value=(config, slot),
),
mock.patch.object(workspace, "commit_paths") as commit,
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_checkpoint(
SimpleNamespace(task=TASK_ID, phase="review")
)
state = workspace.load_state(slot, directory / "state.yaml")
self.assertEqual(state["phase"], "review")
commit.assert_not_called()
def test_normal_lifecycle_publishes_only_start_and_approve(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
slot = root / "slot"
source = root / "source"
source.mkdir()
directory = write_task(slot, status="todo", claimed_by=None)
config = {
"ai_main": str(root / "main"),
"checkout_tag": "macbook-twork",
"slot_worktree": str(slot),
"source_root": str(source),
}
subjects = []
def record_commit(_config, _paths, subject):
subjects.append(subject)
return True
with (
mock.patch.object(workspace, "worktree_config", return_value=config),
mock.patch.object(workspace, "sync_canonical"),
mock.patch.object(workspace, "commit_paths", side_effect=record_commit),
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_start(SimpleNamespace(task=TASK_ID))
with (
mock.patch.object(
workspace,
"task_action_config",
return_value=(config, slot),
),
mock.patch.object(workspace, "commit_paths", side_effect=record_commit),
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_checkpoint(
SimpleNamespace(task=TASK_ID, phase="review")
)
(directory / "work" / "result.md").write_text(
f"""# Task result: {TASK_ID}
STATUS: DONE
Verdict: APPROVED
Checkout: clean-buildable
""",
encoding="utf-8",
)
with (
mock.patch.object(
workspace,
"task_action_config",
return_value=(config, slot),
),
mock.patch.object(workspace, "ensure_clean"),
mock.patch.object(workspace, "validate_source_state"),
mock.patch.object(workspace, "delete_source_refs"),
mock.patch.object(workspace, "commit_paths", side_effect=record_commit),
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_finish(
SimpleNamespace(task=TASK_ID, status="approved")
)
self.assertEqual(subjects, [
f"Start {TASK_ID} on macbook-twork",
f"Approve {TASK_ID}",
])
state = workspace.load_state(slot, directory / "state.yaml")
self.assertEqual(state["status"], "approved")
def test_retry_refuses_to_compete_with_active_task(self):
with tempfile.TemporaryDirectory() as temporary:

View File

@@ -16,7 +16,7 @@ in its initial prompt to read `.claude/ai-workflow-adapter.md` completely before
the applicable shared skill or reference. Use the Agent tool for those workers;
do not start Claude subprocesses through Bash.
Arguments, when present, are natural-language scheduling or claim-scope hints:
Arguments, when present, are natural-language scheduling or priority hints:
```text
$ARGUMENTS

View File

@@ -7,8 +7,8 @@ allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Agent, AskUserQuestion, Todo
Read `.claude/ai-workflow-adapter.md` and
`.agents/skills/perform-task/SKILL.md` completely. Follow the shared skill with
the Claude adapter's delegation and text-handling substitutions. Resolve,
claim when needed, implement, verify, and publish only the named task. Do not
the Claude adapter's delegation and text-handling substitutions. Resolve, start
or resume, implement, verify, and publish only the named task. Do not
continue with other queue work afterward.
Tell every phase Agent to read `.claude/ai-workflow-adapter.md` before its