mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-20 08:03:45 +08:00
[ai] Gate tasks on source branch lineage
This commit is contained in:
@@ -119,6 +119,45 @@ Do not write a batch file, claim the whole batch, or publish reservations.
|
||||
Queue refreshes update task state but never add ordinary task ids to the
|
||||
frozen batch.
|
||||
|
||||
### Source-lineage gate
|
||||
|
||||
Before freezing the batch, inspect every prospective initial task's `task.md`
|
||||
and dependencies. For every approved non-verification task whose shipped code
|
||||
is a prerequisite, run:
|
||||
|
||||
```bash
|
||||
python3 .agents/skills/process-inbox/scripts/workspace.py source-lineage \
|
||||
--task <task-id> [--require <explicit-source-task-id> ...]
|
||||
```
|
||||
|
||||
`depends_on` requirements are included automatically. Pass `--require` for an
|
||||
explicit source prerequisite named in `task.md` that old routing failed to put
|
||||
in `depends_on`. Unfinished dependencies remain a readiness concern and appear
|
||||
separately; this gate checks the history of approved source work.
|
||||
|
||||
If any prospective task reports `current_satisfies: false` at this startup
|
||||
gate, pause before freezing, starting, retrying, resuming, or switching
|
||||
branches. Report the current branch, missing source task ids, unavailable
|
||||
commits, and compatible local branches, then ask the human whether to rebase,
|
||||
bring the commit, switch the checkout, or change scope. Never create or route
|
||||
an integration task, and never cherry-pick, rebase, merge, or switch branches
|
||||
at this startup boundary. The exception is an already-active task whose saved
|
||||
artifacts prove Phase 1 completed: it has crossed the task boundary, so resume
|
||||
the performer and let the after-Phase-1 rule publish the task-local Block.
|
||||
|
||||
After the batch is frozen, rerun the same gate immediately before each Start,
|
||||
Retry, or pre-Phase-1 resume. A mismatch first found here is recoverable queue
|
||||
routing, not a task blocker: while the selected task has not completed Phase 1,
|
||||
switch this checkout to a compatible existing local branch and continue the
|
||||
same frozen batch. Require a clean source checkout and submodules, no owned or
|
||||
disposable task overlay, no exact checkout executable, no source recovery refs
|
||||
for work already begun, and verify with `git worktree list --porcelain` that the
|
||||
branch is not checked out elsewhere. Prefer a compatible branch appearing for
|
||||
the most remaining batch tasks; preserve recorded batch order. Do not create a
|
||||
branch or cherry-pick, rebase, or merge. After `git switch`, refresh `queue`,
|
||||
rerun `source-lineage` and `source-preflight`, then Start/Retry or resume. If no
|
||||
safe compatible local branch exists, stop and ask the human.
|
||||
|
||||
### Mode 1: resume active work, then drain the selected snapshot
|
||||
|
||||
If `own_in_progress` contains this checkout's active task, choose `active`
|
||||
@@ -211,7 +250,7 @@ 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>
|
||||
--task <YYYY/MM/DD/slug> [--require <explicit-source-task-id> ...]
|
||||
```
|
||||
|
||||
This preserves its ownership, source recovery refs, plans, reviews, tests,
|
||||
@@ -230,7 +269,7 @@ checkout whose id is in `batch_task_ids`, and start it:
|
||||
|
||||
```bash
|
||||
python3 .agents/skills/process-inbox/scripts/workspace.py start \
|
||||
--task <YYYY/MM/DD/slug>
|
||||
--task <YYYY/MM/DD/slug> [--require <explicit-source-task-id> ...]
|
||||
```
|
||||
|
||||
The resulting canonical `Start` commit changes it to `in-progress`. Leave
|
||||
@@ -309,7 +348,11 @@ After it returns, require one of:
|
||||
|
||||
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, a file-lock build failure that
|
||||
with independent work. A source-lineage mismatch first proven after Phase 1 is
|
||||
such a genuine task-local Block: continue with batch tasks that do not depend
|
||||
on it and whose own lineage gates pass. A pre-Phase-1 lineage stop is not a
|
||||
Block or global hard stop; apply the safe mid-queue branch-switch rule above and
|
||||
resume the same performer. A dirty source checkout, a file-lock build failure that
|
||||
remains after `perform-task` exhausts the shared exact-checkout recovery,
|
||||
missing test account, unsafe publication conflict, or comparable global safety
|
||||
failure stops the loop. The first lock signature never stops the batch.
|
||||
@@ -352,6 +395,13 @@ those paths, commits
|
||||
helper. Retry ordinary concurrent-master races; preserve a semantic conflict
|
||||
or unavailable-remote slot commit and stop.
|
||||
|
||||
Never route discovered work whose sole purpose is moving an existing commit to
|
||||
another branch: no backport, forward-port, cherry-pick, rebase, merge, or
|
||||
branch-sync task. Record that request or observation in the discovery receipt
|
||||
only, naming the source task and desired branch when known. A real product
|
||||
follow-up may depend on the source task, but `depends_on` carries that lineage;
|
||||
do not create an integration companion task.
|
||||
|
||||
Use this stable marker shape so a context-free worker can recover it:
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -64,6 +64,11 @@ Inspect the resolved task, readiness, `other_active_task`, status, and owner.
|
||||
- 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.
|
||||
- Inspect `task.md` for approved source-task prerequisites in addition to
|
||||
`depends_on`, then run `workspace.py source-lineage --task <full-task-id>`
|
||||
with one `--require <source-task-id>` for each explicit prerequisite. Require
|
||||
`current_satisfies: true` before Phase 1. For `start` or `retry`, pass the same
|
||||
`--require` arguments so claiming is machine-gated too.
|
||||
- If it is `todo` and either unclaimed or owned by this checkout, atomically
|
||||
assign and activate it:
|
||||
|
||||
@@ -89,6 +94,17 @@ 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.
|
||||
|
||||
A source-lineage mismatch found before Phase 1 is a pre-phase routing stop, not
|
||||
a task `Block`: create no phase artifacts, source edits, retained commit, or
|
||||
integration task. Return the lineage report to the `continue` scheduler, which
|
||||
may safely switch an existing local branch and resume. In a direct interactive
|
||||
invocation, report it and ask the human. If the mismatch is first discovered
|
||||
only after Phase 1 has completed, restore every owned/disposable source change
|
||||
to a clean boundary and publish a genuine `blocked` result naming the exact
|
||||
missing source task and appropriate branch evidence. Do not cherry-pick,
|
||||
rebase, merge, or manufacture the prerequisite. This blocker is task-local;
|
||||
the scheduler may continue work that does not depend on it.
|
||||
|
||||
## Run and publish
|
||||
|
||||
Execute `references/pipeline.md` exactly. A normal task produces:
|
||||
|
||||
@@ -79,7 +79,19 @@ Before planning or editing:
|
||||
1. Read `SOURCE_ROOT/AGENTS.md`, `REVIEW.md`, `AI_SLOT/AGENTS.md`, `TASK_SPEC`,
|
||||
every referenced input, and relevant project context.
|
||||
2. Verify `state.yaml` is `in-progress` and owned by this checkout tag.
|
||||
3. Run the scripted preflight report and act on its JSON instead of composing
|
||||
3. Inspect `TASK_SPEC` for approved source-task prerequisites beyond
|
||||
`depends_on`, then run the source-lineage gate before Phase 1:
|
||||
|
||||
```bash
|
||||
python3 SOURCE_ROOT/.agents/skills/process-inbox/scripts/workspace.py \
|
||||
source-lineage --source-root SOURCE_ROOT --task TASK_ID \
|
||||
[--require EXPLICIT_SOURCE_TASK_ID ...]
|
||||
```
|
||||
|
||||
Require `current_satisfies: true`. A mismatch before Phase 1 returns the
|
||||
clean pre-phase routing stop defined below; a mismatch first established
|
||||
after Phase 1 follows the task-local Block rule.
|
||||
4. Run the scripted preflight report and act on its JSON instead of composing
|
||||
the equivalent shell checks by hand:
|
||||
|
||||
```bash
|
||||
@@ -89,15 +101,15 @@ Before planning or editing:
|
||||
|
||||
It reports source/submodule cleanliness, dirty paths outside the owned
|
||||
write set, and the golden test account and live marker state for `EXE`.
|
||||
4. Require the prepared portable test account (`golden_account_present`). A
|
||||
5. Require the prepared portable test account (`golden_account_present`). A
|
||||
`type: minimal` task runs no test loop and starts without it; for every
|
||||
other type its absence is a global hard stop before implementation.
|
||||
5. Verify a usable Debug executable/build tree, safe path-scoped process
|
||||
6. Verify a usable Debug executable/build tree, safe path-scoped process
|
||||
control, safe portable-folder operations, and the ability to launch and
|
||||
render the in-binary test flow. A locked macOS session disables Computer Use
|
||||
only; it does not fail this preflight or block testing, even when policy was
|
||||
`required`.
|
||||
6. For a new run require a clean tracked Telegram worktree, clean submodules,
|
||||
7. For a new run require a clean tracked Telegram worktree, clean submodules,
|
||||
and no unrelated untracked files, then initialize local recovery state:
|
||||
|
||||
```bash
|
||||
@@ -788,6 +800,14 @@ delays finishing the work actually in hand.
|
||||
|
||||
## Failure handling
|
||||
|
||||
- Source lineage has a strict timing boundary. Before Phase 1, a missing
|
||||
approved prerequisite is a clean pre-phase routing stop: do not publish
|
||||
`blocked`, edit source, or create a backport/cherry-pick/rebase/merge task.
|
||||
The scheduler may switch to a compatible existing branch and resume. If the
|
||||
missing prerequisite is first established after Phase 1 completed, restore
|
||||
owned and disposable changes, publish a clean task-local `blocked` boundary
|
||||
naming the missing source task and branch evidence, and let `continue` run
|
||||
non-dependent batch work. Never perform branch integration inside the task.
|
||||
- A disposable phase may be retried once through the wait ladder. Never fresh
|
||||
retry the performer within the same attempt. An interruption leaves local
|
||||
task state `in-progress`; a later `continue` invocation resumes it. A later
|
||||
|
||||
@@ -76,6 +76,17 @@ inbox into requests, then decide for each request whether to:
|
||||
- add one or more tasks to an existing project;
|
||||
- create a new project when durable shared context is useful.
|
||||
|
||||
Never create a task whose work is to move an existing source commit between
|
||||
branches: no backport, forward-port, cherry-pick, rebase, merge, branch sync,
|
||||
or equivalent integration task. Branch placement is human release/history
|
||||
coordination, not product work for the autonomous queue. When a request only
|
||||
asks for that operation, record a receipt-only disposition naming the existing
|
||||
source task or commit description and the requested target branch, then leave
|
||||
the operation to the human. When new product work requires code shipped by an
|
||||
earlier task, route only the product work and express the source task as a
|
||||
dependency; do not create a companion task to bring that dependency onto a
|
||||
branch.
|
||||
|
||||
Bias project assignment toward continuity. When a request follows from an
|
||||
existing task, begin with that task's project and keep it unless independence
|
||||
is affirmatively established. A task belongs to the existing project when it
|
||||
@@ -193,7 +204,9 @@ Use a project slug instead of `null` when routed to a project. Use a YAML list
|
||||
of task identifiers for dependencies. Dependencies record code lineage as well
|
||||
as readiness: keep an approved source task in `depends_on` when the new task's
|
||||
implementation assumes its shipped changes. State that prerequisite in
|
||||
`task.md`. Inbox processing never reserves work: new tasks always remain
|
||||
`task.md`. This dependency is sufficient; never add a separate backport,
|
||||
cherry-pick, rebase, merge, or branch-sync task to make it reachable. Inbox
|
||||
processing never reserves work: new tasks always remain
|
||||
`status: todo` with `claimed_by`, `claimed_at`, and `claim_order` set to `null`.
|
||||
The checkout tag belongs in the receipt only.
|
||||
|
||||
|
||||
@@ -909,6 +909,24 @@ def command_start(args):
|
||||
)
|
||||
if not task_ready(task, states):
|
||||
raise WorkspaceError(f"Task has unfinished dependencies: {args.task}")
|
||||
lineage = source_lineage_report(
|
||||
config,
|
||||
slot,
|
||||
args.task,
|
||||
getattr(args, "require", []),
|
||||
)
|
||||
if lineage.get("unfinished_dependencies"):
|
||||
raise WorkspaceError(
|
||||
"Task has unfinished explicit source dependencies: "
|
||||
+ ", ".join(lineage["unfinished_dependencies"])
|
||||
)
|
||||
if not lineage["current_satisfies"]:
|
||||
branches = ", ".join(lineage["compatible_local_branches"]) or "none"
|
||||
raise WorkspaceError(
|
||||
"Task source dependencies are absent from the current branch: "
|
||||
+ ", ".join(lineage["missing_source_tasks"])
|
||||
+ f"; compatible local branches: {branches}"
|
||||
)
|
||||
active = [
|
||||
value["id"] for value in states.values()
|
||||
if value["claimed_by"] == config["checkout_tag"]
|
||||
@@ -962,6 +980,20 @@ def command_start(args):
|
||||
}, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def command_source_lineage(args):
|
||||
config = worktree_config(args, create=True)
|
||||
slot = Path(config["slot_worktree"])
|
||||
states = load_states(slot)
|
||||
task = resolve_task(slot, states, args.task)
|
||||
report = source_lineage_report(
|
||||
config,
|
||||
slot,
|
||||
task["id"],
|
||||
args.require,
|
||||
)
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
|
||||
|
||||
def command_retry(args):
|
||||
config = worktree_config(args, create=True)
|
||||
sync_canonical(config)
|
||||
@@ -979,6 +1011,24 @@ def command_retry(args):
|
||||
)
|
||||
if not task_ready(task, states):
|
||||
raise WorkspaceError(f"Task has unfinished dependencies: {args.task}")
|
||||
lineage = source_lineage_report(
|
||||
config,
|
||||
slot,
|
||||
args.task,
|
||||
getattr(args, "require", []),
|
||||
)
|
||||
if lineage.get("unfinished_dependencies"):
|
||||
raise WorkspaceError(
|
||||
"Task has unfinished explicit source dependencies: "
|
||||
+ ", ".join(lineage["unfinished_dependencies"])
|
||||
)
|
||||
if not lineage["current_satisfies"]:
|
||||
branches = ", ".join(lineage["compatible_local_branches"]) or "none"
|
||||
raise WorkspaceError(
|
||||
"Task source dependencies are absent from the current branch: "
|
||||
+ ", ".join(lineage["missing_source_tasks"])
|
||||
+ f"; compatible local branches: {branches}"
|
||||
)
|
||||
active = [
|
||||
value["id"] for value in states.values()
|
||||
if value["claimed_by"] == config["checkout_tag"]
|
||||
@@ -1081,6 +1131,92 @@ def task_series_refs(source, task_id):
|
||||
return None
|
||||
|
||||
|
||||
def task_tip_commits(source, task_id):
|
||||
commits = [
|
||||
commit for commit in run_git(
|
||||
source,
|
||||
"log",
|
||||
"--all",
|
||||
"--format=%H",
|
||||
"--fixed-strings",
|
||||
f"--grep=Task: {task_id}",
|
||||
).stdout.splitlines()
|
||||
if task_commit_matches(source, commit, task_id)
|
||||
]
|
||||
return [
|
||||
commit for commit in commits
|
||||
if not any(
|
||||
commit != other and is_ancestor(source, commit, other)
|
||||
for other in commits
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def source_lineage_report(config, slot, task_id, extra_requirements=()):
|
||||
states = load_states(slot)
|
||||
task = states.get(task_id)
|
||||
if task is None:
|
||||
raise WorkspaceError(f"Task does not exist: {task_id}")
|
||||
superseded = load_superseded(slot)
|
||||
required = []
|
||||
unfinished = []
|
||||
for dependency in (*task["depends_on"], *extra_requirements):
|
||||
resolved = resolve_task_id(states, superseded, dependency)["id"]
|
||||
state = states[resolved]
|
||||
if state["status"] != "approved":
|
||||
if resolved not in unfinished:
|
||||
unfinished.append(resolved)
|
||||
continue
|
||||
if state["type"] == "verify" or resolved in required:
|
||||
continue
|
||||
required.append(resolved)
|
||||
|
||||
source = Path(config["source_root"])
|
||||
tips = {
|
||||
dependency: task_tip_commits(source, dependency)
|
||||
for dependency in required
|
||||
}
|
||||
missing_history = [
|
||||
dependency for dependency, commits in tips.items()
|
||||
if not commits
|
||||
]
|
||||
missing = [
|
||||
dependency for dependency, commits in tips.items()
|
||||
if not commits or not any(is_ancestor(source, commit) for commit in commits)
|
||||
]
|
||||
branches = run_git(
|
||||
source,
|
||||
"for-each-ref",
|
||||
"--format=%(refname:short)",
|
||||
"refs/heads",
|
||||
).stdout.splitlines()
|
||||
compatible = [
|
||||
branch for branch in branches
|
||||
if all(
|
||||
commits and any(
|
||||
is_ancestor(source, commit, branch)
|
||||
for commit in commits
|
||||
)
|
||||
for commits in tips.values()
|
||||
)
|
||||
]
|
||||
current_branch = run_git(
|
||||
source,
|
||||
"branch",
|
||||
"--show-current",
|
||||
).stdout.strip() or None
|
||||
return {
|
||||
"task": task_id,
|
||||
"current_branch": current_branch,
|
||||
"current_satisfies": not missing,
|
||||
"unfinished_dependencies": unfinished,
|
||||
"required_source_tasks": required,
|
||||
"missing_source_tasks": missing,
|
||||
"unavailable_source_tasks": missing_history,
|
||||
"compatible_local_branches": compatible,
|
||||
}
|
||||
|
||||
|
||||
def is_ancestor(source, older, newer="HEAD"):
|
||||
return not run_git(
|
||||
source,
|
||||
@@ -3463,11 +3599,19 @@ def parse_args():
|
||||
start = subparsers.add_parser("start")
|
||||
add_common_arguments(start)
|
||||
start.add_argument("--task", required=True)
|
||||
start.add_argument("--require", action="append", default=[])
|
||||
start.set_defaults(handler=command_start)
|
||||
|
||||
source_lineage = subparsers.add_parser("source-lineage")
|
||||
add_common_arguments(source_lineage)
|
||||
source_lineage.add_argument("--task", required=True)
|
||||
source_lineage.add_argument("--require", action="append", default=[])
|
||||
source_lineage.set_defaults(handler=command_source_lineage)
|
||||
|
||||
retry = subparsers.add_parser("retry")
|
||||
add_common_arguments(retry)
|
||||
retry.add_argument("--task", required=True)
|
||||
retry.add_argument("--require", action="append", default=[])
|
||||
retry.set_defaults(handler=command_retry)
|
||||
|
||||
checkpoint = subparsers.add_parser("checkpoint")
|
||||
|
||||
@@ -782,6 +782,11 @@ inbox_receipt: receipts/2026/07/20/race.md
|
||||
with (
|
||||
mock.patch.object(workspace, "worktree_config", return_value=config),
|
||||
mock.patch.object(workspace, "sync_canonical"),
|
||||
mock.patch.object(
|
||||
workspace,
|
||||
"source_lineage_report",
|
||||
return_value={"current_satisfies": True},
|
||||
),
|
||||
mock.patch.object(workspace, "commit_paths") as commit,
|
||||
contextlib.redirect_stdout(io.StringIO()),
|
||||
):
|
||||
@@ -806,6 +811,11 @@ inbox_receipt: receipts/2026/07/20/race.md
|
||||
with (
|
||||
mock.patch.object(workspace, "worktree_config", return_value=config),
|
||||
mock.patch.object(workspace, "sync_canonical"),
|
||||
mock.patch.object(
|
||||
workspace,
|
||||
"source_lineage_report",
|
||||
return_value={"current_satisfies": True},
|
||||
),
|
||||
mock.patch.object(workspace, "commit_paths", return_value=True) as commit,
|
||||
contextlib.redirect_stdout(io.StringIO()),
|
||||
):
|
||||
@@ -822,6 +832,124 @@ inbox_receipt: receipts/2026/07/20/race.md
|
||||
f"Start {TASK_ID} on macbook-twork",
|
||||
)
|
||||
|
||||
def test_start_refuses_source_dependency_absent_from_branch(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),
|
||||
}
|
||||
report = {
|
||||
"current_satisfies": False,
|
||||
"missing_source_tasks": ["2026/07/18/source-task"],
|
||||
"compatible_local_branches": ["layer229"],
|
||||
}
|
||||
with (
|
||||
mock.patch.object(workspace, "worktree_config", return_value=config),
|
||||
mock.patch.object(workspace, "sync_canonical"),
|
||||
mock.patch.object(
|
||||
workspace,
|
||||
"source_lineage_report",
|
||||
return_value=report,
|
||||
) as lineage,
|
||||
mock.patch.object(workspace, "commit_paths") as commit,
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
workspace.WorkspaceError,
|
||||
"compatible local branches: layer229",
|
||||
):
|
||||
workspace.command_start(SimpleNamespace(
|
||||
task=TASK_ID,
|
||||
require=["2026/07/18/source-task"],
|
||||
))
|
||||
|
||||
state = workspace.load_state(slot, directory / "state.yaml")
|
||||
self.assertEqual(state["status"], "todo")
|
||||
self.assertIsNone(state["claimed_by"])
|
||||
lineage.assert_called_once_with(
|
||||
config,
|
||||
slot,
|
||||
TASK_ID,
|
||||
["2026/07/18/source-task"],
|
||||
)
|
||||
commit.assert_not_called()
|
||||
|
||||
def test_source_lineage_finds_compatible_local_branch(self):
|
||||
dependency_id = "2026/07/18/source-task"
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
source = root / "source"
|
||||
slot = root / "slot"
|
||||
git_repo(source)
|
||||
tracked = source / "tracked.txt"
|
||||
tracked.write_text("base\n", encoding="utf-8")
|
||||
git(source, "add", "tracked.txt")
|
||||
git(source, "commit", "-m", "Create baseline")
|
||||
git(source, "branch", "without-dependency")
|
||||
tracked.write_text("source task\n", encoding="utf-8")
|
||||
git(
|
||||
source,
|
||||
"commit",
|
||||
"-am",
|
||||
"Add source behavior",
|
||||
"-m",
|
||||
f"Task: {dependency_id}",
|
||||
)
|
||||
git(source, "branch", "with-dependency")
|
||||
git(source, "switch", "without-dependency")
|
||||
|
||||
dependency = slot / "tasks" / dependency_id
|
||||
dependency.mkdir(parents=True)
|
||||
(dependency / "task.md").write_text(
|
||||
"# Add source behavior\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(dependency / "state.yaml").write_text(
|
||||
"""status: approved
|
||||
type: implement
|
||||
created: 2026-07-18
|
||||
project: null
|
||||
depends_on: []
|
||||
claimed_by: macbook-twork
|
||||
claimed_at: 2026-07-18T10:00:00+04:00
|
||||
claim_order: 1
|
||||
lease_until: null
|
||||
phase: complete
|
||||
inbox_receipt: receipts/2026/07/18/test.md
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
target = write_task(slot, status="todo", claimed_by=None)
|
||||
state_path = target / "state.yaml"
|
||||
state_path.write_text(
|
||||
state_path.read_text(encoding="utf-8").replace(
|
||||
"depends_on: []",
|
||||
f"depends_on: [{dependency_id}]",
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
config = {"source_root": str(source)}
|
||||
report = workspace.source_lineage_report(
|
||||
config,
|
||||
slot,
|
||||
TASK_ID,
|
||||
)
|
||||
self.assertFalse(report["current_satisfies"])
|
||||
self.assertEqual(report["missing_source_tasks"], [dependency_id])
|
||||
self.assertEqual(report["unavailable_source_tasks"], [])
|
||||
self.assertIn("with-dependency", report["compatible_local_branches"])
|
||||
|
||||
git(source, "switch", "with-dependency")
|
||||
report = workspace.source_lineage_report(
|
||||
config,
|
||||
slot,
|
||||
TASK_ID,
|
||||
)
|
||||
self.assertTrue(report["current_satisfies"])
|
||||
self.assertEqual(report["missing_source_tasks"], [])
|
||||
|
||||
def test_checkpoint_updates_only_local_task_state(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
slot = Path(temporary)
|
||||
@@ -866,6 +994,11 @@ inbox_receipt: receipts/2026/07/20/race.md
|
||||
with (
|
||||
mock.patch.object(workspace, "worktree_config", return_value=config),
|
||||
mock.patch.object(workspace, "sync_canonical"),
|
||||
mock.patch.object(
|
||||
workspace,
|
||||
"source_lineage_report",
|
||||
return_value={"current_satisfies": True},
|
||||
),
|
||||
mock.patch.object(workspace, "commit_paths", side_effect=record_commit),
|
||||
contextlib.redirect_stdout(io.StringIO()),
|
||||
):
|
||||
@@ -943,6 +1076,11 @@ inbox_receipt: receipts/2026/07/19/test.md
|
||||
with (
|
||||
mock.patch.object(workspace, "worktree_config", return_value=config),
|
||||
mock.patch.object(workspace, "sync_canonical"),
|
||||
mock.patch.object(
|
||||
workspace,
|
||||
"source_lineage_report",
|
||||
return_value={"current_satisfies": True},
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(workspace.WorkspaceError, "already in progress"):
|
||||
workspace.command_retry(SimpleNamespace(task=TASK_ID))
|
||||
|
||||
Reference in New Issue
Block a user