diff --git a/AGENT.md b/AGENT.md index bf16ebb3..e0ccbe33 100644 --- a/AGENT.md +++ b/AGENT.md @@ -59,10 +59,12 @@ no unique law and does not satisfy this rule. - **Always use a worktree for file changes.** Never edit the shared primary checkout. A read-only design conversation creates no worktree. -- Prefer `tools/worktree-new.sh --class - --key ...` (seeds cargo target and records advisory activity). Finish with - `tools/worktree-done.sh ` (clears activity, prunes `target/`, removes - worktree). The canonical root is `.Codex/worktrees/`; operators may override +- Prefer `tools/worktree-new.sh ` (seeds the Cargo target; pass + `--no-seed` for docs/process-only work). The live worktree and its actual diff + are coordination truth; register a separate `tools/heartbeat.sh` run when a + dispatch will be long-running. Finish with `tools/worktree-done.sh ` + after ending any matching heartbeat; it prunes `target/` and removes the + worktree. The canonical root is `.Codex/worktrees/`; operators may override it with `MISALIGNED_WORKTREE_ROOT` without changing the repository contract. - For a spec carrying work-order metadata, prefer `tools/task.sh start wiki/path.md`; `tools/task.sh status|check|finish|abandon` composes the same diff --git a/tools/test_project_ops.py b/tools/test_project_ops.py index 593b78de..d57b5020 100755 --- a/tools/test_project_ops.py +++ b/tools/test_project_ops.py @@ -343,6 +343,48 @@ Depends on: none self.assertIn("touching", row) self.assertIsInstance(row["touching"], list) + def test_worktree_doorways_do_not_advertise_retired_activity(self) -> None: + repo = Path(__file__).resolve().parents[1] + created = subprocess.run( + ["bash", "tools/worktree-new.sh", "--help"], + cwd=repo, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + self.assertEqual(0, created.returncode, created.stdout) + self.assertIn("actual diff are coordination truth", created.stdout) + for retired_flag in ("--class", "--key", "--no-activity", "--no-claim"): + self.assertNotIn(retired_flag, created.stdout) + + malformed = subprocess.run( + ["bash", "tools/worktree-new.sh", "--not-a-real-flag"], + cwd=repo, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + self.assertEqual(2, malformed.returncode, malformed.stdout) + self.assertIn("unknown flag", malformed.stdout) + + removed = subprocess.run( + ["bash", "tools/worktree-done.sh", "--help"], + cwd=repo, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + check=False, + ) + self.assertEqual(0, removed.returncode, removed.stdout) + self.assertIn("does not end heartbeat state", removed.stdout) + self.assertNotIn("clear activity", removed.stdout.lower()) + + doorway = (repo / "AGENT.md").read_text(encoding="utf-8") + self.assertIn("tools/worktree-new.sh ", doorway) + self.assertNotIn("records advisory activity", doorway) + def test_project_status_recent_broken_links_fail_but_stale_links_warn(self) -> None: now = 1_720_000_000.0 recent = { diff --git a/tools/worktree-done.sh b/tools/worktree-done.sh index bf3e4200..c4bb5238 100755 --- a/tools/worktree-done.sh +++ b/tools/worktree-done.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Finish a task worktree: clear activity, remove worktree + branch, prune target. +# Finish a task worktree: remove its worktree + branch and prune its target. # Binding: wiki/process/agent-scale.md slices A + D. # # Usage: @@ -10,6 +10,20 @@ # unless --force. set -euo pipefail +usage() { + cat <<'EOF' +Finish a clean task worktree by pruning its target and removing its worktree +plus merged task branch. + +Usage: + tools/worktree-done.sh [--status done|abandoned] [--keep-target] + +Run from the primary checkout or any worktree of the same repository. This +helper does not end heartbeat state; end a matching long-dispatch heartbeat +first, or use tools/task.sh finish|abandon for the structured lifecycle. +EOF +} + root=$(cd "$(dirname "$0")/.." && pwd) # Resolve primary checkout (shared .git). primary=$root @@ -38,7 +52,7 @@ while [ $# -gt 0 ]; do --keep-target) keep_target=1; shift ;; --force) force=1; shift ;; -h|--help) - sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' + usage exit 0 ;; -*) diff --git a/tools/worktree-new.sh b/tools/worktree-new.sh index b391638e..c50c96c6 100755 --- a/tools/worktree-new.sh +++ b/tools/worktree-new.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -# Create a seeded task worktree and optionally record local task activity. +# Create an isolated task worktree and optionally seed its Cargo target. # Binding: wiki/process/agent-scale.md slices A + D. -# -# Usage: -# tools/worktree-new.sh [--class ] \ -# [--key path]... [--base-revision revision] [--no-seed] [--no-activity] -# -# Creates (default; override root with MISALIGNED_WORKTREE_ROOT): -# .Codex/worktrees/ on branch worktree- from origin/main -# Runs tools/seed-cargo-target.sh unless --no-seed. -# Records with the given class/paths unless --no-activity. `--no-claim` -# remains a compatibility alias. Overlap is advisory; the worktree is isolation. set -euo pipefail script_root=$(cd "$(dirname "$0")/.." && pwd) @@ -30,14 +20,25 @@ case "$worktree_root" in esac usage() { - sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//' - exit 2 + cat <<'EOF' +Create an isolated task worktree from current origin/main. + +Usage: + tools/worktree-new.sh [--base-revision revision] [--no-seed] + +Creates (default; override root with MISALIGNED_WORKTREE_ROOT): + .Codex/worktrees/ on branch worktree- + +Runs tools/seed-cargo-target.sh unless --no-seed. The live worktree and its +actual diff are coordination truth; start tools/heartbeat.sh separately for a +long-running dispatch. +EOF } task="" class="" seed=1 -do_claim=1 +warn_legacy_activity=1 keys=() base_revision="" base_revision_set=0 @@ -51,24 +52,26 @@ while [ $# -gt 0 ]; do base_revision="${1:-}"; shift || true ;; --no-seed) seed=0; shift ;; - --no-activity|--no-claim) do_claim=0; shift ;; - -h|--help) usage ;; + --no-activity|--no-claim) warn_legacy_activity=0; shift ;; + -h|--help) usage; exit 0 ;; -*) echo "unknown flag: $1" >&2 - usage + usage >&2 + exit 2 ;; *) if [ -z "$task" ]; then task=$1 shift else - usage + usage >&2 + exit 2 fi ;; esac done -[ -n "$task" ] || usage +[ -n "$task" ] || { usage >&2; exit 2; } [ "$base_revision_set" -le 1 ] && { [ "$base_revision_set" -eq 0 ] || [ -n "$base_revision" ]; } || { @@ -127,8 +130,9 @@ fi # The advisory claim ledger was retired 2026-07-29 (agent-scale.md slice A): # a live worktree and its real diff already say what is being worked on, and # they cannot outlive the process the way a hand-declared record did. -# `--class` / `--key` remain accepted so existing dispatch lines keep working. -if [ "$do_claim" -eq 1 ] && [ "${#keys[@]}" -gt 0 -o -n "$class" ]; then +# `--class` / `--key` remain accepted so existing dispatch lines keep working, +# but they are compatibility-only and therefore absent from current usage. +if [ "$warn_legacy_activity" -eq 1 ] && [ "${#keys[@]}" -gt 0 -o -n "$class" ]; then echo "worktree-new: note: --class/--key are no longer recorded; run" >&2 echo " tools/project-status.py to see live worktrees and what they touch" >&2 fi diff --git a/wiki/log/2026-08-04-worktree-activity-contract.md b/wiki/log/2026-08-04-worktree-activity-contract.md new file mode 100644 index 00000000..99e6ac2d --- /dev/null +++ b/wiki/log/2026-08-04-worktree-activity-contract.md @@ -0,0 +1,53 @@ +# 2026-08-04 — Worktree activity contract repair + +``` +Type: log +``` + +## Finding + +The 2026-07-29 claim-ledger retirement was complete in project status and the +owning process law, but not at the two worktree doorways agents actually read. +`AGENT.md` still told every direct task to pass `--class` and `--key` because +`worktree-new.sh` would “record advisory activity”; the helper's own usage made +the same promise, then its implementation discarded those values and printed a +contradictory note. Its paired cleanup header also claimed to clear activity +even though it only removes the worktree, merged branch, and target directory. +Worse, `worktree-done.sh --help` read its own relative path only after changing +into the primary checkout, so a task branch could display the primary script's +older help instead of the code actually being run. +The immediately prior hourly task reproduced the contradiction while creating +an ordinary isolated worktree. + +## Change + +- Made the current `worktree-new.sh` usage expose only behavior that exists: + worktree creation, optional Cargo target seeding, and explicit separate + heartbeat registration for long dispatches. +- Kept retired activity flags parser-compatible for old dispatch lines without + advertising them to new work; legacy class/key calls still receive the + existing no-record warning. +- Corrected `AGENT.md`, the worktree cleanup header, and the held process slice + so live worktrees/diffs and separately owned heartbeats have one consistent + lifecycle. +- Replaced both line-number/self-file help projections with literal usage + contracts, so help invoked from a worktree cannot drift to primary-checkout + bytes after repository-root resolution; explicit `worktree-new.sh --help` + also exits successfully while malformed invocations still fail. +- Added a focused fixture that fails if either helper or the main doorway + reintroduces the retired activity promise. + +## Verification + +- `bash -n tools/worktree-new.sh tools/worktree-done.sh` +- `python3 tools/test_project_ops.py` +- `./tools/check.sh --docs` + +## Defense + +`ProjectOpsFixtures::test_worktree_doorways_do_not_advertise_retired_activity` +executes both helper help paths and reads the primary agent doorway. It requires +the live-diff coordination contract, rejects every retired activity flag from +current usage, rejects the false cleanup claim, and rejects the stale +“records advisory activity” instruction while compatibility remains pinned in +the owning process page. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index b944ac20..537f582f 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -11,6 +11,11 @@ add or amend a session log, then re-run the generator. +## 2026-08-04 - Worktree activity contract repair + +- Intent: (see session log) +- Log: [wiki/log/2026-08-04-worktree-activity-contract.md](2026-08-04-worktree-activity-contract.md) + ## 2026-08-04 - Social contract re-audit - Intent: (see session log) diff --git a/wiki/process/agent-scale.md b/wiki/process/agent-scale.md index e2fe534a..e3535f43 100644 --- a/wiki/process/agent-scale.md +++ b/wiki/process/agent-scale.md @@ -3,8 +3,8 @@ ``` Type: spec Status: IMPLEMENTED -Status note: slices A–M are held: advisory activity and shared - worktree/run state; package-aware fast/land verification; generated ledgers; +Status note: slices A–M are held: live worktree and shared run state; + package-aware fast/land verification; generated ledgers; worktree bootstrap/prune; corpus engine; hidden-window Bevy evidence; heartbeats; spec-owned work-order metadata plus generated ROADMAP status; human/JSON project status; dispatch-consistency fixtures; deterministic @@ -100,8 +100,10 @@ What replaced it is strictly more honest: a worktree exists only while the work does, `worktree-done.sh` removes it, and its diff against `origin/main` is the real edit surface rather than a guess made before the work started. -`worktree-new.sh` still accepts `--class` and `--key` so older dispatch lines -keep working; it records neither and says so. +`worktree-new.sh` still accepts `--class`, `--key`, `--no-activity`, and +`--no-claim` so older dispatch lines keep working. They are compatibility-only, +absent from current usage and doorways, and record nothing; legacy lines that +pass class or key values receive an explicit note instead of a false receipt. What did **not** change: overlap is still not ownership, worktrees still isolate edits, and the landing agent still reconciles semantically against @@ -249,17 +251,20 @@ making their replaceable status machine-readable instead of relying on prose. ### Behavior (landed helpers) ```bash -tools/worktree-new.sh --class frontend --key crates/misaligned-bevy/src/main.rs -# -> .Codex/worktrees/ on worktree-, seed-cargo-target, activity -tools/worktree-done.sh # clear activity, rm target/, remove worktree +tools/worktree-new.sh +# -> .Codex/worktrees/ on worktree-, seed-cargo-target +tools/heartbeat.sh start --worktree "$PWD/.Codex/worktrees/" --phase implement +# long dispatches only; end the heartbeat before lower-level cleanup +tools/worktree-done.sh # rm target/, remove worktree + merged branch ``` `.Codex/worktrees` is the one repository default for every agent surface. Operators that need another location set `MISALIGNED_WORKTREE_ROOT` for both -helpers; tool brands do not define separate roots. Activity reaping also checks -Git's registered worktree basenames, so a task created by a harness-native -Letta or Claude worktree remains durable even when its short-lived creator PID -has exited and it lives outside `.Codex/worktrees`. +helpers; tool brands do not define separate roots. `project-status.py` reads +Git's registered worktrees wherever they live, and a long dispatch adds a +separate heartbeat whose id matches the worktree basename. The lower-level +cleanup helper does not invent or end a heartbeat; `task.sh finish|abandon` +owns that lifecycle when the work began through the structured doorway. Shared **dependency** compilation cache (sccache or cargo cache) remains recommended; **local package** artifacts stay per-worktree. Never share one