[ai] Resume blocked tasks on continue.

This commit is contained in:
John Preston
2026-07-19 18:12:53 +04:00
parent 61da040370
commit 2eaffaef6c
9 changed files with 581 additions and 123 deletions
+1 -1
View File
@@ -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
terminal task result; never create `result<n>.md`.
published attempt-boundary result; never create `result<n>.md`.
## Commit message
+67 -28
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 this checkout's active task, consume its 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 active or previously blocked unfinished work, consume this checkout's claimed queue, and claim new work until nothing eligible remains.
---
# Continue AI Work
@@ -37,7 +37,9 @@ work.
- `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;
- `approved` and `blocked` are terminal;
- `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.
Do not infer a claim from who processed an inbox receipt. Do not steal or
@@ -64,13 +66,16 @@ 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 and then its already claimed queue before taking more shared
work, unless the user explicitly asks to stop or reassign them.
`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.
## Main loop
Repeat these steps. Refresh queue JSON after every delegated operation and
state transition; do not rely on a stale snapshot.
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.
### 1. Process the inbox
@@ -92,7 +97,31 @@ leave the input or active transaction recoverable.
If this checkout has an `in-progress` task, select it. There must be at most
one. Spawn one stateful performer as described below.
### 3. Start reserved work
### 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:
```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`.
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.
### 4. Start 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:
@@ -105,7 +134,7 @@ python3 .agents/skills/process-inbox/scripts/workspace.py start \
Then spawn its performer. Leave claimed tasks with unfinished prerequisites as
`todo` and consider later ready tasks.
### 4. Claim shared work
### 5. Claim 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,
@@ -122,18 +151,21 @@ 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.
### 5. Stop normally
### 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.
Claimed tasks belonging to other checkouts do not keep this run alive.
Claimed tasks waiting on prerequisites remain visible in the final summary but
do not cause a busy loop.
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.
## Spawn one performer
@@ -146,24 +178,29 @@ Source checkout: <source_root>
AI slot worktree: <slot_worktree>
Checkout tag: <checkout_tag>
Task: <task-id>
Own this task until it 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 this attempt reaches approved or blocked. You may use the
bounded leaf delegation required by the skill. Do not select or claim 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 a terminal state.
send a follow-up to the same target if it becomes idle without an
attempt-boundary state.
After it returns, require:
- source checkout clean and at the performer's retained commit;
- task `state.yaml` terminal and published to AI master; or
- 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.
A clean terminal `blocked` task does not stop the scheduler; continue with
independent work. A dirty checkout, file-lock build failure, missing test
account, unresolved AI publication conflict, or other global environment
failure stops the loop.
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.
The missing `test_TelegramForcePortable` golden account is the only
portable-folder state that is a global stop. Live and real portable folders
@@ -173,7 +210,8 @@ must never stop `/continue`.
## Route discovered follow-ups
After every terminal performer, read its published `work/result.md`. If it says
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.
@@ -204,9 +242,10 @@ Refresh queue JSON after routing, then resume the main loop.
## Report
Return one compact run summary: inbox receipt if processed, tasks approved,
tasks blocked with exact unverified behavior, 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.
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.
+2 -2
View File
@@ -1,4 +1,4 @@
interface:
display_name: "Continue AI Work"
short_description: "Drain the shared Telegram AI work queue"
default_prompt: "Use $continue to process the inbox and continue queued Telegram Desktop 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."
+25 -11
View File
@@ -1,13 +1,14 @@
---
name: perform-task
description: Resolve, claim, implement, commit, and verify exactly one existing ai-tdesktop task by short slug or full dated id. 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, 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.
---
# Perform One AI Task
Own exactly one task through a Telegram commit and terminal AI commit. Do not
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 task reaches a terminal state.
this attempt reaches `approved` or `blocked`.
## Read the complete engine
@@ -66,7 +67,7 @@ owner before source work.
- If another task is already `in-progress` for this checkout, stop. Never
abandon or supersede it implicitly.
- If this task is `approved` or `blocked`, report its terminal result and 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.
@@ -84,8 +85,20 @@ owner before source work.
--task <full-task-id>
```
- If it is `blocked` and owned by this checkout, retry it:
```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.
- If it is already `in-progress` and owned by this checkout, resume it without
another claim or start commit.
another claim, start, or retry 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.
@@ -97,15 +110,16 @@ Execute `references/pipeline.md` exactly. The task must normally produce:
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. a terminal AI slot commit containing final result/state, rebased and
3. an attempt-boundary AI slot commit containing final result/state, rebased and
published to canonical AI master without force.
Do not report success from a source commit alone. The final 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.
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.
Return a compact result with the full task id, terminal status or hard stop,
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.
@@ -1,4 +1,4 @@
interface:
display_name: "Perform AI Task"
short_description: "Implement one Telegram AI task by name"
default_prompt: "Use $perform-task to resolve, implement, verify, and publish one named ai-tdesktop 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."
@@ -9,7 +9,7 @@
- [Implementation phases](#implementation-phases)
- [Telegram commits](#telegram-commits)
- [Test loop adapter](#test-loop-adapter)
- [Terminal state](#terminal-state)
- [Attempt-boundary state](#attempt-boundary-state)
- [Failure handling](#failure-handling)
## Contract and inputs
@@ -41,6 +41,7 @@ PROJECT_FILE = AI_SLOT/projects/<project>/project.md, or none
PREVIOUS_CONTEXT = latest approved project task's work/context.md, or none
BASE_REF = refs/ai-tasks/TASK_ID/base
GREEN_REF = refs/ai-tasks/TASK_ID/green
RUN_REF = refs/ai-tasks/TASK_ID/run
```
Capture a wall-clock start time for the final elapsed-time report.
@@ -89,16 +90,18 @@ Before planning or editing:
source-begin --source-root SOURCE_ROOT --task TASK_ID
```
On resume, run the same command. It verifies the local refs and reconciles
them from contiguous tip commits with this task's exact `Task:` line after a
source rebase. Never resolve or record a ref's object name in an artifact.
On resume, run the same command. It verifies the local refs, rediscovers the
latest exact `Task:` commit from current first-parent history when needed,
and records current `HEAD` in `RUN_REF`. Later task commits may remain above
the retained implementation. Never resolve or record a ref's object name in
an artifact.
6. For an interrupted run, allow dirty Telegram paths only when every one is
listed in `work/owned-paths.txt` and completed phase artifacts prove this
task owns them. Otherwise hard-stop without cleaning them.
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 `GREEN_REF` or `BASE_REF`, as appropriate.
task and only back to `RUN_REF` or `BASE_REF`, as appropriate.
## Artifacts and resumption
@@ -248,10 +251,12 @@ attribution, or any other trailer. Track rationale in the AI task. If a short
durable explanation will help source-history readers, write
`SOURCE_ROOT/tasks/TASK_ID.md` and include it in the same commit.
Record only the attempt number. Use `BASE_REF` as the old behavioral baseline
across all attempts and `GREEN_REF` as the current retained implementation.
These refs are local recovery mechanics: never copy their resolved object names
into AI artifacts, source notes, reports, chat, or commit messages.
Record only the attempt number. Use `BASE_REF` as the original local baseline,
`GREEN_REF` as the latest retained exact task commit, and `RUN_REF` as the
current clean source tip on which this resumed run operates. Later tasks may
make `GREEN_REF` an ancestor of `RUN_REF`. These refs are local recovery
mechanics: never copy their resolved object names into AI artifacts, source
notes, reports, chat, or commit messages.
## Test loop adapter
@@ -266,7 +271,9 @@ rules, with these external-task safety adaptations:
in `work/test-overlay.paths`; never introduce an untracked source file.
- Save the overlay with `git diff --binary HEAD > work/test-overlay.patch`,
verify it is nonempty and reapplicable, then restore only inventoried overlay
paths to `GREEN_REF`. Do not run a repository-wide hard reset.
paths to `RUN_REF`. Do not run a repository-wide hard reset. After an
implementation-fix commit, move both `GREEN_REF` and `RUN_REF` to the new
clean tip before reapplying the overlay.
- Reapply with `git apply --3way`; re-author a conflicting hunk from `test.md`
rather than leaving conflict markers.
- Missing `test_TelegramForcePortable` is the only portable-account setup
@@ -290,26 +297,31 @@ rules, with these external-task safety adaptations:
- Delete the overlay-bearing Debug executable on every terminal test exit so
the user cannot launch it accidentally.
The test author must read both the full task specification and the complete
`BASE_REF..GREEN_REF` diff. It writes checks before running, covers every
acceptance surface, declares a falsifiable oracle for each, and never reuses a
generic navigate-and-screenshot scenario. Missing or ambiguous evidence is
`TEST_FLAW`; no expected delta from `BASE_REF` is `IMPL_BUG`. Two identical
consecutive failure signatures block early. A known implementation bug at the
attempt cap is implementation-blocked, not a successful retained commit.
The test author must read the full task specification and every current-branch
commit whose message has this task's exact `Task:` line. For an uninterrupted
contiguous run this is the `BASE_REF..GREEN_REF` diff; for a resumed older task,
combine the exact task commits and inspect their current code at `RUN_REF`
without treating intervening tasks as this task's changes. It writes checks
before running, covers every acceptance surface, declares a falsifiable oracle
for each, and never reuses a generic navigate-and-screenshot scenario. Missing
or ambiguous evidence is `TEST_FLAW`; no expected task delta is `IMPL_BUG`. Two
identical consecutive failure signatures block early. A known implementation
bug at the attempt cap is implementation-blocked, not a successful retained
commit.
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.
## Terminal state
## Attempt-boundary state
Before terminal publication, require a clean Telegram checkout at `GREEN_REF`
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.
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
behavior.
Write `work/result.md` with exactly one value for every field:
@@ -336,7 +348,8 @@ 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 terminal AI state only after the Telegram commit and result are final:
Publish the attempt-boundary AI state only after the Telegram commit and result
are final:
```bash
python3 SOURCE_ROOT/.agents/skills/process-inbox/scripts/workspace.py \
@@ -345,12 +358,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 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, fast-forwards local AI master, and then deletes the
local task refs. Do not report a terminal task until that final AI commit
reaches canonical master. Preserve an unpublished slot commit on a semantic
conflict or remote outage and hard-stop instead of pretending completion.
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.
When `Discovered: present`, preserve complete task blocks in `result.md`. The
`continue` scheduler must route them through the same independent-testability
@@ -359,13 +374,16 @@ 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.
- A clean terminal `blocked` task lets `continue` proceed with independent
work. A dirty/non-buildable checkout or global environment problem stops it.
retry the performer within the same attempt; a later `continue` invocation
creates one new performer to resume a published blocked task.
- 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.
- 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.
- Never silently pass unverified behavior. Surface every blocked or partially
verified task with exact `work/test.md`, `work/result.md`, and evidence paths.
- In Goal mode, report terminal blocked state without claiming achievement;
- In Goal mode, report blocked state without claiming achievement;
complete the goal only when every selected task is approved.
+158 -44
View File
@@ -514,7 +514,7 @@ def resolve_task(states, value):
]
unfinished = [
task for task in exact
if task["status"] in ("todo", "in-progress")
if task["status"] in ("todo", "in-progress", "blocked")
]
candidates = unfinished or exact
if not candidates:
@@ -541,15 +541,31 @@ def command_queue(args):
tag = config["checkout_tag"]
values = [task_summary(task, states) for task in states.values()]
own_in_progress = sorted(
(task for task in values if task["claimed_by"] == tag and task["status"] == "in-progress"),
(
task for task in values
if task["claimed_by"] == tag and task["status"] == "in-progress"
),
key=queue_sort_key,
)
own_todo = sorted(
(task for task in values if task["claimed_by"] == tag and task["status"] == "todo"),
(
task for task in values
if task["claimed_by"] == tag and task["status"] == "todo"
),
key=queue_sort_key,
)
own_blocked = sorted(
(
task for task in values
if task["claimed_by"] == tag and task["status"] == "blocked"
),
key=queue_sort_key,
)
unclaimed_todo = sorted(
(task for task in values if task["claimed_by"] is None and task["status"] == "todo"),
(
task for task in values
if task["claimed_by"] is None and task["status"] == "todo"
),
key=lambda task: (task["created"], task["id"]),
)
violations = []
@@ -558,17 +574,23 @@ def command_queue(args):
for task in values:
if task["status"] == "in-progress" and task["claimed_by"] is None:
violations.append(f"{task['id']} is in-progress but unclaimed")
if task["status"] == "blocked" and task["claimed_by"] is None:
violations.append(f"{task['id']} is blocked but unclaimed")
inbox_file = main / "inbox" / "inbox.md"
result = {
**config,
"inbox_nonempty": inbox_file.is_file() and bool(inbox_file.read_text(encoding="utf-8").strip()),
"inbox_nonempty": (
inbox_file.is_file()
and bool(inbox_file.read_text(encoding="utf-8").strip())
),
"own_in_progress": own_in_progress,
"own_todo": own_todo,
"own_blocked": own_blocked,
"unclaimed_todo": unclaimed_todo,
"other_claimed_unfinished": sum(
1 for task in values
if task["claimed_by"] not in (None, tag)
and task["status"] in ("todo", "in-progress")
and task["status"] in ("todo", "in-progress", "blocked")
),
"ai_main_dirty": main_dirty,
"slot_dirty": slot_dirty,
@@ -699,6 +721,53 @@ def command_start(args):
}, indent=2, sort_keys=True))
def command_retry(args):
config = worktree_config(args, create=True)
sync_canonical(config)
slot = Path(config["slot_worktree"])
states = load_states(slot)
task = states.get(args.task)
if task is None:
raise WorkspaceError(f"Task does not exist: {args.task}")
if (
task["status"] != "blocked"
or task["claimed_by"] != config["checkout_tag"]
):
raise WorkspaceError(
f"Task is not blocked work owned by this checkout: {args.task}"
)
if not task_ready(task, states):
raise WorkspaceError(f"Task has unfinished dependencies: {args.task}")
active = [
value["id"] for value in states.values()
if value["claimed_by"] == config["checkout_tag"]
and value["status"] == "in-progress"
]
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": "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),
}, indent=2, sort_keys=True))
def task_action_config(args, require_status="in-progress", allow_project=False):
config = worktree_config(args, create=True)
slot = Path(config["slot_worktree"])
@@ -742,34 +811,61 @@ def task_commit_matches(source, value, task_id):
)
def validate_current_task_commit(source, task_id):
if not task_commit_matches(source, "HEAD", task_id):
def validate_task_commit(source, value, task_id):
if not task_commit_matches(source, value, task_id):
raise WorkspaceError(
"Telegram commit message must be exactly a one-line subject, a blank line, "
f"and Task: {task_id}"
)
def task_series_base(source, task_id):
current = "HEAD"
if not task_commit_matches(source, current, task_id):
return None
while True:
parent = resolved_ref(source, f"{current}^")
if parent is None:
raise WorkspaceError("A task implementation cannot start at the repository root")
if not task_commit_matches(source, parent, task_id):
return parent
current = parent
def task_series_refs(source, task_id):
commits = run_git(
source,
"log",
"--first-parent",
"--format=%H",
"--fixed-strings",
f"--grep=Task: {task_id}",
"HEAD",
).stdout.splitlines()
for green in commits:
if not task_commit_matches(source, green, task_id):
continue
current = green
while True:
parent = resolved_ref(source, f"{current}^")
if parent is None:
raise WorkspaceError(
"A task implementation cannot start at the repository root"
)
if not task_commit_matches(source, parent, task_id):
return parent, green
current = parent
return None
def is_ancestor(source, older, newer="HEAD"):
return not run_git(
source,
"merge-base",
"--is-ancestor",
older,
newer,
check=False,
).returncode
def validate_source_state(config, task_id, required):
source = Path(config["source_root"])
base = resolved_ref(source, source_task_ref(task_id, "base"))
green = resolved_ref(source, source_task_ref(task_id, "green"))
run = resolved_ref(source, source_task_ref(task_id, "run"))
head = resolved_ref(source, "HEAD")
if base is None:
raise WorkspaceError("The local task baseline ref is missing")
if run is None or head != run:
raise WorkspaceError("Telegram HEAD no longer matches the task run ref")
if green is None:
if required:
raise WorkspaceError("An approved task must retain a Telegram implementation commit")
@@ -778,9 +874,11 @@ def validate_source_state(config, task_id, required):
"A blocked task without an implementation must be restored to its local baseline"
)
return
if head != green:
raise WorkspaceError("Telegram HEAD is not the locally retained task implementation")
validate_current_task_commit(source, task_id)
if not is_ancestor(source, green, head):
raise WorkspaceError(
"The retained task implementation is not in Telegram HEAD history"
)
validate_task_commit(source, green, task_id)
def ensure_no_persisted_commit_hashes(root):
@@ -799,9 +897,10 @@ def ensure_no_persisted_commit_hashes(root):
raise WorkspaceError(f"Persisted commit hash found in {path}")
def delete_source_refs(config, task_id):
def delete_source_refs(config, task_id, retain_implementation=False):
source = Path(config["source_root"])
for name in ("green", "base"):
names = ("run",) if retain_implementation else ("run", "green", "base")
for name in names:
value = source_task_ref(task_id, name)
if resolved_ref(source, value) is not None:
run_git(source, "update-ref", "-d", value)
@@ -812,28 +911,33 @@ def command_source_begin(args):
source = Path(config["source_root"])
base = source_task_ref(args.task, "base")
green = source_task_ref(args.task, "green")
run = source_task_ref(args.task, "run")
base_value = resolved_ref(source, base)
green_value = resolved_ref(source, green)
head = resolved_ref(source, "HEAD")
series_base = task_series_base(source, args.task)
if series_base is not None:
run_git(source, "update-ref", base, series_base)
run_git(source, "update-ref", green, "HEAD")
state = "reconciled" if base_value is not None else "recovered"
elif base_value is None:
ensure_clean(source, "Telegram source checkout")
run_git(source, "update-ref", base, "HEAD")
state = "initialized"
elif green_value is None and head != base_value:
ensure_clean(source, "Telegram source checkout")
run_git(source, "update-ref", base, "HEAD")
state = "reconciled"
elif green_value is not None and head != green_value:
raise WorkspaceError(
"Telegram HEAD no longer matches this task's retained implementation"
)
else:
retained = (
base_value is not None
and green_value is not None
and is_ancestor(source, base_value, green_value)
and is_ancestor(source, green_value, head)
and task_commit_matches(source, green_value, args.task)
)
if retained or (base_value is not None and green_value is None and head == base_value):
state = "resumed"
else:
series = task_series_refs(source, args.task)
if series is not None:
series_base, series_green = series
run_git(source, "update-ref", base, series_base)
run_git(source, "update-ref", green, series_green)
state = "reconciled" if base_value is not None else "recovered"
else:
ensure_clean(source, "Telegram source checkout")
run_git(source, "update-ref", base, "HEAD")
if green_value is not None:
run_git(source, "update-ref", "-d", green)
state = "reconciled" if base_value is not None else "initialized"
run_git(source, "update-ref", run, "HEAD")
print(json.dumps({
"task": args.task,
"source_state": state,
@@ -852,8 +956,9 @@ def command_source_mark_green(args):
raise WorkspaceError("The local task baseline ref is missing")
if run_git(source, "merge-base", "--is-ancestor", base, "HEAD", check=False).returncode:
raise WorkspaceError("The retained implementation does not descend from the task baseline")
validate_current_task_commit(source, args.task)
validate_task_commit(source, "HEAD", args.task)
run_git(source, "update-ref", source_task_ref(args.task, "green"), "HEAD")
run_git(source, "update-ref", source_task_ref(args.task, "run"), "HEAD")
print(json.dumps({
"task": args.task,
"source_state": "retained",
@@ -901,7 +1006,11 @@ def command_finish(args):
paths,
f"{verb} {args.task}",
)
delete_source_refs(config, args.task)
delete_source_refs(
config,
args.task,
retain_implementation=(args.status == "blocked"),
)
print(json.dumps({
"task": args.task,
"status": args.status,
@@ -1148,6 +1257,11 @@ def parse_args():
start.add_argument("--task", required=True)
start.set_defaults(handler=command_start)
retry = subparsers.add_parser("retry")
add_common_arguments(retry)
retry.add_argument("--task", required=True)
retry.set_defaults(handler=command_retry)
checkpoint = subparsers.add_parser("checkpoint")
add_common_arguments(checkpoint)
checkpoint.add_argument("--task", required=True)
@@ -0,0 +1,218 @@
#!/usr/bin/env python3
import contextlib
import io
from pathlib import Path
import subprocess
import tempfile
from types import SimpleNamespace
import unittest
from unittest import mock
import workspace
TASK_ID = "2026/07/19/correct-recent-search-peer-actions"
def task_state(status, claimed_by="macbook-twork"):
return {
"id": TASK_ID,
"title": "Correct recent-search peer actions",
"status": status,
"created": "2026-07-19",
"project": None,
"depends_on": [],
"claimed_by": claimed_by,
"claimed_at": "2026-07-19T14:28:01+04:00",
"claim_order": 1,
"lease_until": None,
"phase": "blocked" if status == "blocked" else None,
}
def write_task(slot, status="blocked"):
directory = slot / "tasks" / TASK_ID
(directory / "work").mkdir(parents=True)
(directory / "task.md").write_text(
"# Correct recent-search peer actions\n",
encoding="utf-8",
)
(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
lease_until: null
phase: {status}
inbox_receipt: receipts/2026/07/19/test.md
""",
encoding="utf-8",
)
return directory
def git(repo, *args):
return subprocess.run(
["git", "-C", str(repo), *args],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
).stdout.strip()
class WorkspaceTest(unittest.TestCase):
def test_resolve_prefers_blocked_over_approved_history(self):
blocked = task_state("blocked")
approved = {
**task_state("approved"),
"id": "2026/07/18/correct-recent-search-peer-actions",
}
states = {task["id"]: task for task in (approved, blocked)}
resolved = workspace.resolve_task(states, "correct-recent-search-peer-actions")
self.assertEqual(resolved["id"], TASK_ID)
def test_retry_reopens_owned_blocked_task_and_resets_routing_marker(self):
with tempfile.TemporaryDirectory() as temporary:
slot = Path(temporary)
directory = write_task(slot)
routed = directory / "work" / "discovered-routed.md"
routed.write_text("routed\n", encoding="utf-8")
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_retry(SimpleNamespace(task=TASK_ID))
state = workspace.load_state(slot, directory / "state.yaml")
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)
def test_retry_refuses_to_compete_with_active_task(self):
with tempfile.TemporaryDirectory() as temporary:
slot = Path(temporary)
write_task(slot)
active = slot / "tasks" / "2026/07/19/active-task"
active.mkdir(parents=True)
(active / "task.md").write_text("# Active task\n", encoding="utf-8")
(active / "state.yaml").write_text(
"""status: in-progress
created: 2026-07-19
project: null
depends_on: []
claimed_by: macbook-twork
claimed_at: 2026-07-19T15:00:00+04:00
claim_order: 1
lease_until: null
phase: setup
inbox_receipt: receipts/2026/07/19/test.md
""",
encoding="utf-8",
)
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"),
):
with self.assertRaisesRegex(workspace.WorkspaceError, "already in progress"):
workspace.command_retry(SimpleNamespace(task=TASK_ID))
def test_retained_task_commit_can_precede_later_work(self):
with tempfile.TemporaryDirectory() as temporary:
repo = Path(temporary)
git(repo, "init")
git(repo, "config", "user.name", "Workflow Test")
git(repo, "config", "user.email", "workflow@example.invalid")
tracked = repo / "tracked.txt"
tracked.write_text("base\n", encoding="utf-8")
git(repo, "add", "tracked.txt")
git(repo, "commit", "-m", "Create baseline")
base = git(repo, "rev-parse", "HEAD")
tracked.write_text("task\n", encoding="utf-8")
git(repo, "commit", "-am", "Correct peer actions", "-m", f"Task: {TASK_ID}")
green = git(repo, "rev-parse", "HEAD")
tracked.write_text("later\n", encoding="utf-8")
git(repo, "commit", "-am", "Add later independent work")
self.assertEqual(workspace.task_series_refs(repo, TASK_ID), (base, green))
config = {"source_root": str(repo)}
with (
mock.patch.object(workspace, "task_action_config", return_value=(config, None)),
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_source_begin(SimpleNamespace(task=TASK_ID))
self.assertEqual(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "base")),
base,
)
self.assertEqual(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "green")),
green,
)
self.assertEqual(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "run")),
git(repo, "rev-parse", "HEAD"),
)
workspace.validate_source_state(config, TASK_ID, True)
workspace.delete_source_refs(config, TASK_ID, retain_implementation=True)
self.assertIsNotNone(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "base"))
)
self.assertIsNotNone(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "green"))
)
self.assertIsNone(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "run"))
)
def test_source_begin_initializes_new_task_without_a_green_ref(self):
with tempfile.TemporaryDirectory() as temporary:
repo = Path(temporary)
git(repo, "init")
git(repo, "config", "user.name", "Workflow Test")
git(repo, "config", "user.email", "workflow@example.invalid")
(repo / "tracked.txt").write_text("base\n", encoding="utf-8")
git(repo, "add", "tracked.txt")
git(repo, "commit", "-m", "Create baseline")
config = {"source_root": str(repo)}
with (
mock.patch.object(workspace, "task_action_config", return_value=(config, None)),
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_source_begin(SimpleNamespace(task=TASK_ID))
head = git(repo, "rev-parse", "HEAD")
self.assertEqual(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "base")),
head,
)
self.assertEqual(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "run")),
head,
)
self.assertIsNone(
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "green"))
)
if __name__ == "__main__":
unittest.main()
+55
View File
@@ -0,0 +1,55 @@
# Claude Code AI Workflow Adapter
Apply this adapter only when a command explicitly loads it. The shared
`.agents/skills/` workflow remains authoritative for task selection, artifacts,
source changes, builds, testing, commits, resumability, and AI publication.
This file adapts harness mechanics and removes unnecessary text normalization.
## Delegation
- Use Claude Code's `Agent` tool wherever the shared workflow says to spawn a
subagent, worker, performer, planner, or leaf.
- Treat `fork_turns: "none"` as a fresh Agent invocation with a self-contained
prompt containing exact repository, task, artifact, and input paths. Do not
rely on the parent conversation being inherited.
- Tell every leaf Agent not to delegate and never to commit. Preserve the
shared workflow's single-writer and one-stateful-performer constraints.
- A foreground Agent call may replace Codex-specific polling. Treat its short
reply as notification and validate the required files and repository state.
When Claude exposes a resumable agent id and more work is needed from that
same stateful worker, resume that id; never create a duplicate performer or
duplicate an agent whose writes may still be in flight.
- Translate Codex-specific wait, message, follow-up, list, and interrupt calls
to the closest available Agent operation. Preserve the artifact heartbeat,
stall windows, one-retry limit, and terminal-state rules. Never launch a
nested `claude` process from Bash.
- If the first real leaf Agent is rejected before work begins because nested
delegation is unavailable, use the shared same-session fallback. Do not
treat mere presence of the Agent tool as a successful delegation probe.
- Whenever an Agent is asked to run `process-inbox`, `perform-task`, a phase
prompt, or discovered-task routing, explicitly tell it to read this adapter
completely before the applicable shared skill or reference.
## Text handling
Do not run a dedicated line-ending or BOM check, normalization, repair, phase,
artifact, rebuild, or summary in Claude Code. In particular:
- ignore the tracked-text line-ending validation item in `process-inbox`;
- skip perform-task Phase 7 entirely on every host;
- ignore the pipeline's Normalize step and every Phase 7 prompt, result, retry,
completion check, and reporting requirement;
- do not rewrite a file solely to change LF, CRLF, mixed endings, or BOM state.
Let normal Claude editing preserve the checkout's existing text convention.
This exception removes only explicit line-ending work; it does not relax any
content validation, owned-path rule, build, review, test, or publication gate.
## UI-driver capability
The shared Computer Use reference describes Codex's driver. In Claude Code,
treat that particular driver as unavailable unless an equivalent UI-driver
tool is actually exposed in the current session. Preserve the same policy:
`auto` uses the already planned overlay fallback, while `required` reports the
exact unverified interaction. Driver availability never permits skipping the
ordinary overlay test loop, account setup, evidence, or safety rules.