mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
[ai] Use Start and Approve lifecycle.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user