[ai] Test through locked macOS sessions.

This commit is contained in:
John Preston
2026-07-20 11:05:20 +04:00
parent 19f9732418
commit bd67b2feb9
10 changed files with 393 additions and 25 deletions
+13 -1
View File
@@ -250,7 +250,12 @@ How TEST verifies it (numbers over eyes):
The overlay is ad-hoc, authored fresh against the CURRENT implementation, injected at the
highest level that still exercises the change (often a direct data-layer call like
`item->applyEdition(...)` rather than a faked MTP response). It must:
`item->applyEdition(...)` rather than a faked MTP response). It is also a complete runtime driver
when external desktop control is unavailable. Drive the whole task-specific flow inside the Debug
binary by invoking application actions or posting Qt input events on the event loop, waiting for
observable state, logging assertions, capturing the rendered target in-process, and quitting. A
locked macOS session does not reduce required coverage and is never a testing blocker. The overlay
must:
- Live entirely inside `#ifdef _DEBUG` blocks.
- Pick a **test strategy** and record it in the spec:
@@ -267,6 +272,10 @@ highest level that still exercises the change (often a direct data-layer call li
it) so the target is unambiguously in frame at usable resolution. A full-window grab that leaves
the target clipped, off-screen, or thumbnail-sized is NOT acceptable evidence — if the target
isn't clearly captured, that is a TEST_FLAW (re-frame), never a pass.
- When the desktop is locked or an OS screenshot is unavailable, capture from inside the process
with `QWidget::grab()` or a renderer-owned image after layout and paint have completed. A widget or
test-window grab plus logged geometry is primary visual evidence; never wait for unlock merely to
obtain a desktop screenshot.
- **Lay down the oracle's references.** Save every applicable independent reference beside the
crop. Exact asset work saves OLD and intended-NEW art as `<name>_{old,new}.png`. Without target
artwork, save the baseline/reference-component crop when available and log the contract anchors,
@@ -332,6 +341,9 @@ actually land; likewise for screenshots.
## Build & run discipline
- On macOS, a locked graphical session disables external UI driving only. Launch `EXE` normally,
run the in-binary overlay flow, collect its logs and widget/window grabs, assess them, and clean up.
Do not try to unlock the session and do not return BLOCKED because the lock screen is present.
- Build with `BUILD`. A single changed TU compiles fast; only the overlay-touched files + link
rebuild between rounds. Proactive path-scoped cleanup may run before the build. If the build reports
`LNK1104`, `C1041`, access denied, or file in use, follow `AGENTS.md`: stop immediately, do not
+53 -16
View File
@@ -26,12 +26,17 @@ python3 .agents/skills/process-inbox/scripts/workspace.py queue
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 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.
Stop before mutating anything when `violations` is nonempty or AI master is
dirty. Dirty task-scoped AI files are the active checkout's local resumable
phase state and are expected; resume from them, never discard them. Other
uncommitted slot changes under `tasks/`, `projects/`, or `receipts/` are
disposable leftovers of an interrupted worker; the ignored inbox snapshot and
published task results retain every durable input needed to redo them.
Restore those exact tracked paths to the slot branch head, delete their
untracked files, and continue. Stop instead of cleaning when any other slot
path changed, and never clean the main worktree or stash anywhere.
Unpublished clean AI slot commits are incomplete publication; run the
helper's `publish` command before selecting work.
The canonical lifecycle is deliberately small:
@@ -56,8 +61,11 @@ artifacts; it is never automatic scheduler behavior.
## Interpret scope hints
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,
guidance for new shared work. Its own wording decides its strength. A
preference such as "payments tasks first" only reorders selection; the run
still drains every eligible task. A restriction such as "only the payments
tasks" limits new shared work to matching tasks and ends the run when none
remain. Neither form reserves a batch. Start only one shared task at a time,
finish or exceptionally block it, refresh canonical state, then choose again.
Always resume this checkout's `in-progress` task and previously blocked work
@@ -69,6 +77,14 @@ asks to stop or reassign it.
Create an empty invocation-local `attempted_blocked` set, then repeat. Refresh
queue JSON after every delegated operation and state transition.
Before publishing any new canonical `Start` commit, require a startable
environment: a clean Telegram source checkout with clean submodules and no
unrelated untracked files, plus an existing
`out/Debug/test_TelegramForcePortable` golden account. A failed check is a
global hard stop before claiming; never reserve shared work this checkout
cannot immediately run. Resuming and retrying already-owned work keeps the
performer's own preflight rules instead.
### 1. Resume active work
If this checkout has an `in-progress` task, select it and spawn one performer.
@@ -126,10 +142,12 @@ ready work.
### 5. Start shared work
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.
Otherwise select the first ready unclaimed `todo` task the hint prefers. Under
a restrictive hint consider only matching tasks. With no hint, or when a mere
preference has no matching ready task left, select 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.
A concurrent start may mean another checkout won the task. Never overwrite
shared state; refresh and choose again.
@@ -141,12 +159,26 @@ Stop when the inbox is empty and none of these exist:
- this checkout's active task;
- a ready blocked task not attempted in this invocation;
- a ready legacy reserved task;
- a ready unclaimed task matching the invocation's priority scope.
- a ready unclaimed task this invocation may still start: any under no hint or
a preference, only matching ones under a restrictive hint.
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.
Immediately before this normal stop, run the housekeeping command once:
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py archive-stale
```
It publishes one canonical `Archive <slug>` commit for every project whose
tasks are all approved and whose newest task is older than its threshold,
after rewriting the project's relative links for the deeper path. Skip this
housekeeping on any global hard stop. Only routing new work to an archived
project restores it, through the helper's `unarchive` command; nothing
un-archives on a timer.
## Spawn one performer
Spawn exactly one performer for the selected task with `fork_turns: "none"`
@@ -185,6 +217,9 @@ failure stops the loop.
The missing `test_TelegramForcePortable` golden account is the only
portable-folder global stop. All live/real folder combinations must be
reconciled by `perform-task` according to the shared test-loop protocol.
Computer Use being unavailable because macOS is locked is never a scheduler
stop; the performer must continue with the in-binary overlay driver and
artifact-based assessment.
## Route discovered follow-ups
@@ -209,6 +244,8 @@ conflict or unavailable-remote slot commit and stop.
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.
tasks started or left queued, routed discoveries, archived projects, any
discarded interrupted-worker leftovers, 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.
+5
View File
@@ -98,6 +98,11 @@ 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.
A locked macOS session is not an environment stop or verification blocker.
Skip interactive Computer Use and complete the same coverage through the
in-binary overlay: drive the flow, log/assert, capture widgets or windows,
quit, and assess the saved artifacts.
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
@@ -9,6 +9,7 @@ authoritative.
- [Driver policy](#driver-policy)
- [Capability gate](#capability-gate)
- [Locked macOS override](#locked-macos-override)
- [Exact-app gate](#exact-app-gate)
- [Hybrid handshake](#hybrid-handshake)
- [Safety envelope](#safety-envelope)
@@ -23,7 +24,8 @@ Resolve one policy from the user's request and pass it to the task-runner:
focus, scrolling, dragging, menus, windowing, or native UI materially improves coverage.
- `overlay-only` — never use Computer Use.
- `required` — use hybrid driving for the named flow; if it cannot run safely, return
`BLOCKED(test)` with the exact missing interaction rather than weakening the oracle.
`BLOCKED(test)` with the exact missing interaction rather than weakening the oracle, except for
the locked-macOS override below.
For each check, select `Driver: overlay` or `Driver: hybrid`. Keep overlay-only for internal state,
data, exact text, and geometry that the in-app harness can exercise deterministically. Select hybrid
@@ -63,14 +65,37 @@ not arrive through `fork_turns: "none"`; the explicit path is the handoff.
Do not infer capability from an installed directory or config entry. An installed plugin may be
disabled by policy, lack its runtime tool, support a different platform, or still need app/OS
approval. In `auto`, fall back to the prewritten overlay action. In `required`, or when no equivalent
overlay can exercise the physical interaction, return `BLOCKED(test)` with the canonical mapping below.
Missing capability or permission is never an implementation bug.
overlay can exercise the physical interaction, return `BLOCKED(test)` with the canonical mapping
below unless the locked-macOS override applies. Missing capability or permission is never an
implementation bug.
Computer Use runs in the foreground on Windows. Use it only on an unlocked, reserved active desktop
or isolated VM. Treat local pointer or keyboard interference, a focus steal, or a window switch as a
contaminated run and restart within `MAX_TEST_RUNS`. Treat WSL/Linux/headless as unavailable unless
the current host explicitly exposes a supported desktop adapter for the exact test app.
## Locked macOS override
When macOS reports the graphical session locked, including
`CGSSessionScreenIsLocked=Yes`, do not wait for unlock, repeat capability checks, ask the human to
unlock, count repeated lock checks toward a blocked goal, or return `BLOCKED(test)`. Do not call
Computer Use against the lock screen. Force every planned check to `Driver: overlay`, regardless of
whether the original policy was `auto` or `required`, and continue the ordinary build/run/assess
loop. Skip the exact-app gate and hybrid handshake entirely for that run.
Treat the in-binary overlay as a complete driver, not merely supplemental instrumentation. Extend it
until it performs the whole task-specific flow on the Qt event loop: create or inject the fixture,
invoke the same application actions or post the needed Qt input events, wait on observable
conditions, log actual values and assertions, grab the target widget or top-level test window from
inside the process, save artifacts, emit `TEST_COMPLETE`, and quit. Prefer `QWidget::grab()` or a
renderer-owned image over an OS desktop capture, which may show only the lock screen. Assess those
logs, measurements, and saved images exactly as in any other overlay run.
Record the lock in `computer-use-capability.md` as the reason for selecting overlay-only and report
`UI-Driver: overlay`. Lock state is neither missing verification nor a global environment stop. If
the first overlay design depended on an external gesture, redesign that action inside the binary
rather than treating the locked session as evidence that the task cannot be tested.
## Exact-app gate
Keep process and account ownership in the ordinary runner:
@@ -146,6 +171,8 @@ Whenever a planned hybrid check is unavailable before a run, write
`<TASK_DIR>/computer-use-capability.md` with the policy, host, active skill path or `none`, runtime
tool status, OS/app-approval status, exact-app identity result, fallback decision, and reason.
- Locked macOS always continues overlay-only under the override above, including for `required`.
Never map screen lock to `BLOCKED(test)` or `UI-Driver: hybrid-unavailable`.
- `auto` with an equivalent fallback continues overlay-only. Record `UI-Driver: overlay` and cite the
capability report plus overlay evidence; capability failure is not a blocker.
- `required`, or `auto` without an equivalent fallback, records `STATUS: BLOCKED`,
@@ -80,8 +80,10 @@ Before planning or editing:
3. Require the prepared portable test account. Its absence is a global hard
stop before implementation.
4. Verify a usable Debug executable/build tree, safe path-scoped process
control, safe portable-folder operations, and an app-run display. Computer
Use remains optional unless policy is `required`.
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`.
5. For a new run require a clean tracked Telegram worktree, clean submodules,
and no unrelated untracked files, then initialize local recovery state:
@@ -279,6 +281,11 @@ rules, with these external-task safety adaptations:
clean tip before reapplying the overlay.
- Reapply with `git apply --3way`; re-author a conflicting hunk from `test.md`
rather than leaving conflict markers.
- On locked macOS, force overlay-only testing without waiting or blocking.
Encode the complete interaction inside the Debug binary using application
actions or Qt events, log assertions and geometry, capture widgets/windows
in-process, save the artifacts, and quit. Do not require an OS-level desktop
screenshot or interactive Computer Use evidence.
- Missing `test_TelegramForcePortable` is the only portable-account setup
blocker. If live exists without real, move live to real. If live and real
both exist, recursively delete live completely. Then deep-copy golden to
@@ -387,6 +394,9 @@ planner into new unclaimed dated tasks before selecting more shared work.
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.
- A locked macOS session and the resulting unavailable Computer Use driver
never stop or block the task; continue with the complete in-binary overlay
flow.
- 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.
+15 -1
View File
@@ -46,7 +46,8 @@ Read these before planning:
- source checkout `AGENTS.md`;
- `ai_main/AGENTS.md`;
- existing `projects/*/project.md` and task states relevant to the request;
- existing `projects/*/project.md`, including `projects/archive/`, and task
states relevant to the request;
- the transaction's `inbox.md` and every file it references.
Use one disposable leaf planner when the harness supports delegation; instruct
@@ -67,6 +68,18 @@ domain projects. Group requests into one task only when they form one cohesive,
independently testable behavior. Split work until every task is implementable
in one pass and has an exact observable acceptance result.
Project slugs are unique across `projects/` and `projects/archive/`. When a
request belongs to an archived project, restore it before routing to it:
```bash
python3 .agents/skills/process-inbox/scripts/workspace.py unarchive \
--project <slug>
```
The helper moves the project back to `projects/<slug>`, rewrites its relative
links, and leaves the restored files staged for this transaction's commit.
Never point a task at a path under `projects/archive/`.
Briefly inspect Telegram source when needed to understand scope and testable
seams. Do not plan implementation internals and do not modify the source tree.
@@ -157,6 +170,7 @@ Before committing, verify:
- every new task has `task.md`, valid `state.yaml`, and a falsifiable
acceptance result;
- every task link, dependency, and copied input exists;
- no task or project reference points into `projects/archive/`;
- no raw inbox path, `.local/`, browser profile, portable account, credential,
complete run directory, or complete build log is tracked;
- no Telegram or AI commit hash is copied into a task, project, or receipt;
@@ -21,6 +21,9 @@ COMMIT_HASH_PATTERN = re.compile(
r"(?i)\b(?:commit|revision|sha(?:-1)?)\b[^\r\n]{0,32}(?<!#)\b[0-9a-f]{7,64}\b"
)
LEGACY_COMMIT_FIELDS = ("Task-Base-SHA:", "Implementation-SHA:")
PROJECT_ARCHIVE_DIR = "archive"
PROJECT_LINK_PATTERN = re.compile(r"\]\(\.\./\.\./(?!\.\./)")
ARCHIVED_PROJECT_LINK_PATTERN = re.compile(r"\]\(\.\./\.\./\.\./")
STATE_FIELD_ORDER = [
"status",
"created",
@@ -993,6 +996,109 @@ def command_publish(args):
print(json.dumps({"published": bool(published)}, indent=2, sort_keys=True))
def rewrite_project_links(directory, pattern, replacement):
for path in sorted(directory.rglob("*.md")):
data = path.read_bytes()
if data.startswith(b"\xef\xbb\xbf"):
raise WorkspaceError(f"Refusing to preserve a UTF-8 BOM in {path}")
text = data.decode("utf-8")
replaced = pattern.sub(replacement, text)
if replaced != text:
path.write_bytes(replaced.encode("utf-8"))
def project_last_activity(states):
activity = {}
unfinished = set()
for task in states.values():
project = task["project"]
if project is None:
continue
try:
created = datetime.date.fromisoformat(task["created"])
except ValueError as error:
raise WorkspaceError(
f"Invalid created date in {task['id']}: {task['created']!r}"
) from error
if project not in activity or created > activity[project]:
activity[project] = created
if task["status"] != "approved":
unfinished.add(project)
return activity, unfinished
def command_archive_stale(args):
if args.days < 1:
raise WorkspaceError("The archive threshold must be at least one day")
config = worktree_config(args, create=True)
sync_canonical(config)
slot = Path(config["slot_worktree"])
activity, unfinished = project_last_activity(load_states(slot))
cutoff = datetime.date.today() - datetime.timedelta(days=args.days)
projects = slot / "projects"
candidates = sorted(
entry.name for entry in projects.iterdir()
if entry.is_dir() and entry.name != PROJECT_ARCHIVE_DIR
) if projects.is_dir() else []
archived = []
published = False
for slug in candidates:
if slug in unfinished or slug not in activity or activity[slug] > cutoff:
continue
target = projects / PROJECT_ARCHIVE_DIR / slug
if target.exists():
raise WorkspaceError(f"The archive already contains a project: {slug}")
target.parent.mkdir(exist_ok=True)
run_git(
slot,
"mv",
f"projects/{slug}",
f"projects/{PROJECT_ARCHIVE_DIR}/{slug}",
)
rewrite_project_links(target, PROJECT_LINK_PATTERN, "](../../../")
published = commit_paths(
config,
[f"projects/{PROJECT_ARCHIVE_DIR}/{slug}"],
f"Archive {slug}",
)
archived.append(slug)
print(json.dumps({
"archived": archived,
"days": args.days,
"published": bool(published),
}, indent=2, sort_keys=True))
def command_unarchive(args):
slug = args.project
if slug == PROJECT_ARCHIVE_DIR or not TAG_PATTERN.fullmatch(slug):
raise WorkspaceError(f"Invalid project slug: {slug!r}")
config = worktree_config(args, create=True)
slot = Path(config["slot_worktree"])
archived = slot / "projects" / PROJECT_ARCHIVE_DIR / slug
target = slot / "projects" / slug
if not archived.is_dir():
raise WorkspaceError(f"No archived project: {slug}")
if target.exists():
raise WorkspaceError(f"The project is already live: {slug}")
run_git(
slot,
"mv",
f"projects/{PROJECT_ARCHIVE_DIR}/{slug}",
f"projects/{slug}",
)
rewrite_project_links(target, ARCHIVED_PROJECT_LINK_PATTERN, "](../../")
run_git(slot, "add", "--", f"projects/{slug}")
archive_root = slot / "projects" / PROJECT_ARCHIVE_DIR
if archive_root.is_dir() and not any(archive_root.iterdir()):
archive_root.rmdir()
print(json.dumps({
"project": slug,
"published": False,
"unarchived": True,
}, indent=2, sort_keys=True))
def payload_entries(inbox):
return sorted(
(path for path in inbox.iterdir() if path.name != "backup"),
@@ -1252,6 +1358,16 @@ def parse_args():
add_common_arguments(publish)
publish.set_defaults(handler=command_publish)
archive_stale = subparsers.add_parser("archive-stale")
add_common_arguments(archive_stale)
archive_stale.add_argument("--days", type=int, default=90)
archive_stale.set_defaults(handler=command_archive_stale)
unarchive = subparsers.add_parser("unarchive")
add_common_arguments(unarchive)
unarchive.add_argument("--project", required=True)
unarchive.set_defaults(handler=command_unarchive)
return parser.parse_args()
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import contextlib
import datetime
import io
from pathlib import Path
import subprocess
@@ -15,6 +16,12 @@ import workspace
TASK_ID = "2026/07/19/correct-recent-search-peer-actions"
class FrozenDate(datetime.date):
@classmethod
def today(cls):
return cls(2026, 7, 20)
def task_state(status, claimed_by="macbook-twork"):
return {
"id": TASK_ID,
@@ -61,6 +68,42 @@ inbox_receipt: receipts/2026/07/19/test.md
return directory
def write_project(slot, slug):
directory = slot / "projects" / slug
directory.mkdir(parents=True)
(directory / "project.md").write_text(f"# {slug}\n", encoding="utf-8")
(directory / "tasks.md").write_text(
"# Tasks\n\n- [Task](../../tasks/2026/01/10/some-task/task.md)\n",
encoding="utf-8",
)
return directory
def write_project_task(slot, task_id, project, status, created):
directory = slot / "tasks" / task_id
directory.mkdir(parents=True)
(directory / "task.md").write_text(
f"# {task_id.rsplit('/', 1)[-1]}\n",
encoding="utf-8",
)
claimed = status != "todo"
(directory / "state.yaml").write_text(
f"""status: {status}
created: {created}
project: {project}
depends_on: []
claimed_by: {"macbook-twork" if claimed else "null"}
claimed_at: {f"{created}T10:00:00+04:00" if claimed else "null"}
claim_order: {"1" if claimed else "null"}
lease_until: null
phase: {"complete" if status == "approved" else "null"}
inbox_receipt: receipts/2026/01/10/test.md
""",
encoding="utf-8",
)
return directory
def git(repo, *args):
return subprocess.run(
["git", "-C", str(repo), *args],
@@ -71,6 +114,13 @@ def git(repo, *args):
).stdout.strip()
def git_repo(path):
path.mkdir(parents=True, exist_ok=True)
git(path, "init")
git(path, "config", "user.name", "Workflow Test")
git(path, "config", "user.email", "workflow@example.invalid")
class WorkspaceTest(unittest.TestCase):
def test_resolve_prefers_blocked_over_approved_history(self):
blocked = task_state("blocked")
@@ -337,6 +387,101 @@ inbox_receipt: receipts/2026/07/19/test.md
workspace.resolved_ref(repo, workspace.source_task_ref(TASK_ID, "green"))
)
def test_archive_stale_moves_only_old_fully_approved_projects(self):
with tempfile.TemporaryDirectory() as temporary:
slot = Path(temporary)
git_repo(slot)
write_project(slot, "old-project")
write_project(slot, "fresh-project")
write_project(slot, "pending-project")
write_project_task(
slot, "2026/01/10/old-task", "old-project", "approved", "2026-01-10"
)
write_project_task(
slot, "2026/07/19/fresh-task", "fresh-project", "approved", "2026-07-19"
)
write_project_task(
slot, "2026/01/11/pending-task", "pending-project", "blocked", "2026-01-11"
)
git(slot, "add", ".")
git(slot, "commit", "-m", "Seed projects")
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,
mock.patch.object(workspace.datetime, "date", FrozenDate),
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_archive_stale(SimpleNamespace(days=90))
archived = slot / "projects" / "archive" / "old-project"
self.assertTrue(archived.is_dir())
self.assertFalse((slot / "projects" / "old-project").exists())
self.assertIn(
"](../../../tasks/",
(archived / "tasks.md").read_text(encoding="utf-8"),
)
self.assertTrue((slot / "projects" / "fresh-project").is_dir())
self.assertTrue((slot / "projects" / "pending-project").is_dir())
commit.assert_called_once()
self.assertEqual(
commit.call_args.args[1],
["projects/archive/old-project"],
)
self.assertEqual(commit.call_args.args[2], "Archive old-project")
def test_archive_stale_without_candidates_reports_nothing(self):
with tempfile.TemporaryDirectory() as temporary:
slot = Path(temporary)
config = {
"checkout_tag": "macbook-twork",
"slot_worktree": str(slot),
}
out = io.StringIO()
with (
mock.patch.object(workspace, "worktree_config", return_value=config),
mock.patch.object(workspace, "sync_canonical"),
mock.patch.object(workspace, "commit_paths") as commit,
contextlib.redirect_stdout(out),
):
workspace.command_archive_stale(SimpleNamespace(days=90))
commit.assert_not_called()
self.assertIn('"archived": []', out.getvalue())
def test_unarchive_restores_links_and_stages_the_project(self):
with tempfile.TemporaryDirectory() as temporary:
slot = Path(temporary)
git_repo(slot)
directory = slot / "projects" / "archive" / "old-project"
directory.mkdir(parents=True)
(directory / "project.md").write_text("# Old project\n", encoding="utf-8")
(directory / "tasks.md").write_text(
"# Tasks\n\n- [Task](../../../tasks/2026/01/10/some-task/task.md)\n",
encoding="utf-8",
)
git(slot, "add", ".")
git(slot, "commit", "-m", "Seed archive")
config = {"slot_worktree": str(slot)}
with (
mock.patch.object(workspace, "worktree_config", return_value=config),
contextlib.redirect_stdout(io.StringIO()),
):
workspace.command_unarchive(SimpleNamespace(project="old-project"))
restored = slot / "projects" / "old-project"
self.assertIn(
"](../../tasks/",
(restored / "tasks.md").read_text(encoding="utf-8"),
)
self.assertFalse((slot / "projects" / "archive").exists())
staged = git(slot, "diff", "--cached", "--name-only")
self.assertIn("projects/old-project/tasks.md", staged)
if __name__ == "__main__":
unittest.main()
+2 -1
View File
@@ -16,7 +16,8 @@ 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 priority hints:
Arguments, when present, are natural-language hints for new shared work; their
wording decides whether they merely prioritize or strictly restrict selection:
```text
$ARGUMENTS
+2 -1
View File
@@ -44,7 +44,8 @@ Resolve the task and read its context:
1. Run `python3 .agents/skills/process-inbox/scripts/workspace.py resolve --name "$ARGUMENTS"` (use the host's Python 3 command).
2. Read the resolved task's `task.md` and `work/context.md` from the returned AI slot worktree.
3. When `project` is non-null, also read `projects/<project>/project.md`.
3. When `project` is non-null, also read `projects/<project>/project.md`, or
`projects/archive/<project>/project.md` when the project has been archived.
This helps you distinguish between:
- **Task-specific mistakes** — the agent misunderstood this particular feature's requirements or made a wrong choice within the specific problem. These are NOT documentation-worthy.