From cc6e9f4b979bacb117b4966cde7dd10e2f457b3d Mon Sep 17 00:00:00 2001 From: Cameron Date: Wed, 29 Jul 2026 13:33:55 -0700 Subject: [PATCH] Retire the advisory claim ledger. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tools/claim.sh and .agents/claims/ are deleted; agent-scale.md slice A is retired rather than amended. Every claim in the repo at decision time was stale — two records held by dead pids, one of which a session had to --force past. The ledger also never decided anything: overlap always warned and proceeded because worktrees isolate edits, while the collisions that actually cost time (save.rs, SAVE_VERSION, the state fingerprint, generated indexes) were in files no declared key covered. Concurrency now comes from records that cannot outlive their work. A worktree exists only while the work does, and project-status reports each one's real touched paths from its diff against origin/main — the actual edit surface rather than a guess made before starting. Heartbeat runs already sat beside every claim status change task.sh made, which is the clearest sign the ledger was a duplicate. Unchanged: overlap is not ownership, worktrees isolate, landings reconcile semantically, and only the Rust gate and final landing phase serialize. worktree-new.sh still accepts --class/--key so existing dispatch lines keep working; it records neither and says so. Defense: agent-scale.md slice A owns this and is retired here with a replacement criterion its landing meets; AGENT.md, workflows.md, env.md, ROADMAP P1, and the two prompts that told agents to declare themselves are updated in the same commit. project-status consistency drops the rules that required an activity record and keeps the run/worktree linkage; fixtures were rewritten to assert the derived truth rather than deleted. No crates/ change. --- .gitignore | 1 - AGENT.md | 6 +- prompts/README.md | 6 +- prompts/implement-gap.md | 2 +- tools/check.sh | 4 +- tools/claim.sh | 379 --------------------- tools/project-status.py | 54 +-- tools/task.sh | 2 - tools/test_project_ops.py | 152 +-------- tools/tick-brief.sh | 8 +- tools/worktree-done.sh | 5 - tools/worktree-new.sh | 16 +- wiki/engineering/env.md | 4 +- wiki/log/2026-07-29-retire-claim-ledger.md | 59 ++++ wiki/log/DEVLOG.md | 5 + wiki/log/decisions/2026-07-29.md | 35 ++ wiki/process/ROADMAP.md | 16 +- wiki/process/agent-scale.md | 105 +++--- wiki/process/workflows.md | 12 +- 19 files changed, 212 insertions(+), 659 deletions(-) delete mode 100755 tools/claim.sh create mode 100644 wiki/log/2026-07-29-retire-claim-ledger.md diff --git a/.gitignore b/.gitignore index 2ac50073..a8c6a1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ .Codex/ # Machine-local multi-agent coordination (not product source) -.agents/claims/ .agents/runs/ # Local editor/vault metadata diff --git a/AGENT.md b/AGENT.md index d3d5af12..ca5619e3 100644 --- a/AGENT.md +++ b/AGENT.md @@ -68,8 +68,10 @@ no unique law and does not satisfy this rule. helpers without auto-committing or destructive Git recovery. Run `tools/project-status.py` for the live dispatch/worktree/activity view and `tools/doctor.sh --offline` for local setup diagnosis. -- **Advertise before heavy work.** Inspect `tools/claim.sh list` and record - likely edit paths. Overlap is a warning, not ownership: proceed in an +- **Check who else is live before heavy work.** `git worktree list` (or + `tools/project-status.py`, which also prints what each worktree touches) + is the concurrency truth; there is no claim ledger to declare yourself in + (retired 2026-07-29). Overlap is a warning, not ownership: proceed in an isolated worktree and reconcile against current main at landing. See [wiki/process/agent-scale.md](wiki/process/agent-scale.md). - A shell working directory does not guarantee that every patch tool resolves diff --git a/prompts/README.md b/prompts/README.md index f0e4f909..3cad2e1c 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -21,9 +21,9 @@ These rules bind every prompted agent, and each prompt file assumes them: `Type: law`, owning `Type: spec`, dependencies, and nearby `Type: knowledge` pages it orders. Do not skip this on later runs — the corpus moves daily. -2. **Worktree + activity, always.** First action: create an isolated worktree - (`tools/worktree-new.sh --class - --key ...`) or equivalent worktree + `tools/claim.sh activity`. +2. **Worktree, always.** First action: create an isolated worktree with + `tools/worktree-new.sh `; check `git worktree list` for who else is + live (the claim ledger was retired 2026-07-29). Never edit the primary checkout. An overlap warning means another task is nearby, not that it owns the file; continue and reconcile at landing. 3. **The one rule.** Functional changes ship with their owning binding wiki diff --git a/prompts/implement-gap.md b/prompts/implement-gap.md index c5c26648..84f05f1c 100644 --- a/prompts/implement-gap.md +++ b/prompts/implement-gap.md @@ -12,7 +12,7 @@ Read, in order: `AGENT.md`, the corpus map and relevant law/spec slice, `wiki/process/specs.md` (the status table), and `wiki/process/ROADMAP.md` (the dispatch board — it may already name the highest-value gap and its worktree name). Obey the shared contract in `prompts/README.md`: -worktree+activity first (`tools/worktree-new.sh` or `claim.sh activity`), one rule, +worktree first (`tools/worktree-new.sh`), one rule, check.sh gate (`--land` before merge), heartbeat on long runs, direct merge to main, union merges on ledgers. diff --git a/tools/check.sh b/tools/check.sh index 833a6c14..7918538b 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -11,7 +11,7 @@ # ./tools/check.sh --land # land policy: auto-classify (full when unclear) # # Concurrent agents: at most one Rust gate runs at a time on this machine -# (mkdir lock). Docs gates never take the lock. `tools/claim.sh` records +# (mkdir lock). Docs gates never take the lock. Live worktrees record # advisory task surfaces; it is not a semantic mutex. # # An unclassifiable clean primary checkout stays conservative (full). Tangled @@ -164,7 +164,7 @@ guard_cargo_target_dir() { step "script syntax" for script in tools/check.sh tools/corpus_gate.sh tools/wiki_gate.sh tools/design_amendment_gate.sh tools/seed-cargo-target.sh \ tools/ci-pkg-config.sh \ - tools/claim.sh tools/worktree-new.sh tools/worktree-done.sh tools/heartbeat.sh \ + tools/worktree-new.sh tools/worktree-done.sh tools/heartbeat.sh \ tools/ledger_index.sh tools/test_corpus_engine.sh tools/test_design_amendment_gate.sh \ tools/test_bevy_headless.sh \ tools/test_site_deploy.sh tools/test_site_smoke.sh tools/site-smoke.sh \ diff --git a/tools/claim.sh b/tools/claim.sh deleted file mode 100755 index 993c3414..00000000 --- a/tools/claim.sh +++ /dev/null @@ -1,379 +0,0 @@ -#!/usr/bin/env bash -# Local work-order activity — advisory overlap visibility for concurrent agents. -# Binding behavior: wiki/process/agent-scale.md slice A (amended 2026-07-10). -# -# Records retain the historical `.agents/claims/` path and `claim` command so -# older worktrees keep interoperating. Path overlap warns but never blocks; -# worktrees isolate edits and the landing phase reconciles against current main. -# -# Usage: -# tools/claim.sh activity --class \ -# [--key path]... [--force] -# tools/claim.sh status [id] -# tools/claim.sh list -# tools/claim.sh set-status -# tools/claim.sh release [--status done|abandoned] -# tools/claim.sh check --key path [--key path]... -# -# Exit codes: 0 ok, 1 conflict/error, 2 bad usage. -set -euo pipefail - -script_root=$(cd "$(dirname "$0")/.." && pwd) -root=$script_root -if [ -f "$script_root/.git" ]; then - common=$(git -C "$script_root" rev-parse --path-format=absolute --git-common-dir) - case "$common" in - */.git) root=${common%/.git} ;; - esac -fi -cd "$root" - -CLAIMS_DIR=${MISALIGNED_CLAIMS_DIR:-"$root/.agents/claims"} -WORKTREE_ROOT=${MISALIGNED_WORKTREE_ROOT:-} -if [ -n "$WORKTREE_ROOT" ]; then - case "$WORKTREE_ROOT" in /*) ;; *) WORKTREE_ROOT="$root/$WORKTREE_ROOT" ;; esac -fi -ACTIVE_STATUSES="claimed blocked checking landing" - -usage() { - sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//' - exit 2 -} - -ensure_dir() { - mkdir -p "$CLAIMS_DIR" -} - -claim_path() { - local id="$1" - # Sanitize id to a single path segment. - case "$id" in - ""|*/*|*\\*|*..*) - echo "FAIL: activity id must be a single path segment (got '$id')" >&2 - exit 2 - ;; - esac - printf '%s/%s.claim\n' "$CLAIMS_DIR" "$id" -} - -is_active() { - case "$1" in - claimed|blocked|checking|landing) return 0 ;; - *) return 1 ;; - esac -} - -# Read claim file into globals: c_id c_class c_status c_pid c_started c_host c_keys (array) -read_claim() { - local path="$1" line - c_id=""; c_class=""; c_status=""; c_pid=""; c_started=""; c_host="" - c_keys=() - [ -f "$path" ] || return 1 - while IFS= read -r line || [ -n "$line" ]; do - case "$line" in - id=*) c_id=${line#id=} ;; - class=*) c_class=${line#class=} ;; - status=*) c_status=${line#status=} ;; - pid=*) c_pid=${line#pid=} ;; - started=*) c_started=${line#started=} ;; - host=*) c_host=${line#host=} ;; - key=*) c_keys+=("${line#key=}") ;; - esac - done < "$path" - return 0 -} - -write_claim() { - local path="$1" key - { - printf 'id=%s\n' "$c_id" - printf 'class=%s\n' "$c_class" - printf 'status=%s\n' "$c_status" - printf 'pid=%s\n' "$c_pid" - printf 'started=%s\n' "$c_started" - printf 'host=%s\n' "$c_host" - for key in "${c_keys[@]+"${c_keys[@]}"}"; do - printf 'key=%s\n' "$key" - done - } > "$path" -} - -keys_intersect() { - # Args: proposed keys..., then --, then held keys... - local -a proposed=() held=() - local mode=proposed k p h - for k in "$@"; do - if [ "$k" = "--" ]; then - mode=held - continue - fi - if [ "$mode" = proposed ]; then - proposed+=("$k") - else - held+=("$k") - fi - done - # Empty proposed keys never conflict (docs-wide parallel ok). - [ "${#proposed[@]}" -eq 0 ] && return 1 - for p in "${proposed[@]}"; do - for h in "${held[@]+"${held[@]}"}"; do - if [ "$p" = "$h" ]; then - return 0 - fi - case "$p:$h" in - @*|*:@*) ;; - *) - p=${p%/}; h=${h%/} - case "$p/" in "$h/"*) return 0 ;; esac - case "$h/" in "$p/"*) return 0 ;; esac - ;; - esac - done - done - return 1 -} - -pid_alive() { - local pid="$1" - [ -n "$pid" ] && [ "$pid" -eq "$pid" ] 2>/dev/null && kill -0 "$pid" 2>/dev/null -} - -linked_worktree_exists() { - local id="$1" - if [ -n "$WORKTREE_ROOT" ] && [ -d "$WORKTREE_ROOT/$id" ]; then - return 0 - fi - # Letta, Claude, Codex, and the project wrapper use different parent - # directories. Git's registry is the common truth; task worktree basenames - # are the durable activity ids. - git worktree list --porcelain 2>/dev/null | awk -v id="$id" ' - $1 == "worktree" { - path = $2 - sub(/^.*\//, "", path) - if (path == id) found = 1 - } - END { exit(found ? 0 : 1) } - ' -} - -# A helper command's pid is short-lived in agent environments. A linked task -# worktree is the durable ownership signal; reap only when both pid and -# worktree are gone. -reap_stale() { - local path id - ensure_dir - for path in "$CLAIMS_DIR"/*.claim; do - [ -f "$path" ] || continue - read_claim "$path" || continue - is_active "$c_status" || continue - if ! pid_alive "$c_pid"; then - if linked_worktree_exists "$c_id"; then - continue - fi - echo "activity: reaping stale record '$c_id' (dead pid $c_pid, was $c_status)" >&2 - rm -f "$path" - fi - done -} - -cmd_list() { - ensure_dir - reap_stale - local path any=0 - for path in "$CLAIMS_DIR"/*.claim; do - [ -f "$path" ] || continue - read_claim "$path" || continue - any=1 - printf '%s class=%s status=%s pid=%s paths=%s\n' \ - "$c_id" "$c_class" "$c_status" "$c_pid" \ - "$(IFS=,; echo "${c_keys[*]-}")" - done - if [ "$any" -eq 0 ]; then - echo "(no active task activity in $CLAIMS_DIR)" - fi -} - -cmd_status() { - local id="${1:-}" - ensure_dir - if [ -z "$id" ]; then - cmd_list - return - fi - local path - path=$(claim_path "$id") - if ! read_claim "$path"; then - echo "FAIL: no activity record '$id' ($path)" >&2 - exit 1 - fi - printf 'id=%s\nclass=%s\nstatus=%s\npid=%s\nstarted=%s\nhost=%s\n' \ - "$c_id" "$c_class" "$c_status" "$c_pid" "$c_started" "$c_host" - local k - for k in "${c_keys[@]+"${c_keys[@]}"}"; do - printf 'key=%s\n' "$k" - done -} - -find_conflict() { - # Sets conflict_id conflict_keys on match. Args: proposed keys... - local -a proposed=("$@") - local path - conflict_id="" - conflict_keys="" - for path in "$CLAIMS_DIR"/*.claim; do - [ -f "$path" ] || continue - read_claim "$path" || continue - is_active "$c_status" || continue - if keys_intersect "${proposed[@]+"${proposed[@]}"}" -- "${c_keys[@]+"${c_keys[@]}"}"; then - conflict_id=$c_id - conflict_keys=$(IFS=,; echo "${c_keys[*]-}") - return 0 - fi - done - return 1 -} - -cmd_check() { - ensure_dir - reap_stale - local -a keys=() - while [ $# -gt 0 ]; do - case "$1" in - --key) shift; [ $# -gt 0 ] || usage; keys+=("$1"); shift ;; - *) usage ;; - esac - done - if find_conflict "${keys[@]+"${keys[@]}"}"; then - echo "WARN: paths overlap active task '$conflict_id' (paths: $conflict_keys)" >&2 - echo " overlap is advisory; finish in a worktree and reconcile at landing" >&2 - exit 0 - fi - echo "check: ok (no active task advertises those paths)" -} - -cmd_claim() { - ensure_dir - reap_stale - local id="" class="" force=0 - local -a keys=() - id="${1:-}"; shift || true - [ -n "$id" ] || usage - while [ $# -gt 0 ]; do - case "$1" in - --class) shift; class="${1:-}"; shift || true ;; - --key) shift; keys+=("${1:-}"); shift || true ;; - --force) force=1; shift ;; - *) usage ;; - esac - done - case "$class" in - docs|frontend|sim|save|process) ;; - *) - echo "FAIL: --class must be docs|frontend|sim|save|process" >&2 - exit 2 - ;; - esac - - local path - path=$(claim_path "$id") - - holder=${MISALIGNED_CLAIM_PID:-$PPID} - if [ -f "$path" ]; then - read_claim "$path" - if is_active "$c_status"; then - if [ "$force" -eq 1 ]; then - echo "activity: --force replacing '$id' (was $c_status pid $c_pid)" - elif pid_alive "$c_pid" && [ "$c_pid" = "$holder" ]; then - : # same agent re-claim updates keys/status - elif pid_alive "$c_pid"; then - echo "WARN: replacing activity '$id' owned by pid $c_pid; task identity is shared" >&2 - else - echo "activity: replacing '$id' from dead pid $c_pid" - fi - fi - fi - - if find_conflict "${keys[@]+"${keys[@]}"}" && [ "$conflict_id" != "$id" ]; then - echo "WARN: paths overlap active task '$conflict_id'" >&2 - echo " their paths: $conflict_keys" >&2 - echo " your paths: $(IFS=,; echo "${keys[*]-}")" >&2 - echo " both tasks may proceed in separate worktrees; reconcile at landing" >&2 - fi - - c_id=$id - c_class=$class - c_status=claimed - # Holder is the calling agent/shell (PPID), not this short-lived script. - # Override with MISALIGNED_CLAIM_PID when a dispatcher wraps the tool. - c_pid=$holder - c_started=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - c_host=$(hostname 2>/dev/null || echo unknown) - c_keys=("${keys[@]+"${keys[@]}"}") - write_claim "$path" - echo "activity: recorded '$id' class=$class status=claimed pid=$c_pid paths=$(IFS=,; echo "${c_keys[*]-}")" - echo " file $path" -} - -cmd_set_status() { - local id="${1:-}" status="${2:-}" - [ -n "$id" ] && [ -n "$status" ] || usage - case "$status" in - claimed|blocked|checking|landing|done|abandoned) ;; - *) - echo "FAIL: status must be claimed|blocked|checking|landing|done|abandoned" >&2 - exit 2 - ;; - esac - ensure_dir - local path - path=$(claim_path "$id") - if ! read_claim "$path"; then - echo "FAIL: no activity record '$id'" >&2 - exit 1 - fi - if [ "$status" = "done" ] || [ "$status" = "abandoned" ]; then - rm -f "$path" - echo "activity: '$id' -> $status (cleared)" - return 0 - fi - c_status=$status - # Keep original holder pid; only refresh if explicitly set. - if [ -n "${MISALIGNED_CLAIM_PID:-}" ]; then - c_pid=$MISALIGNED_CLAIM_PID - fi - write_claim "$path" - echo "activity: '$id' -> $status" -} - -cmd_release() { - local id="${1:-}" status=done - shift || true - [ -n "$id" ] || usage - while [ $# -gt 0 ]; do - case "$1" in - --status) shift; status="${1:-done}"; shift || true ;; - *) usage ;; - esac - done - cmd_set_status "$id" "$status" -} - -main() { - local cmd="${1:-}" - shift || true - case "$cmd" in - activity|claim) cmd_claim "$@" ;; - list) cmd_list "$@" ;; - status) cmd_status "$@" ;; - set-status) cmd_set_status "$@" ;; - release) cmd_release "$@" ;; - check) cmd_check "$@" ;; - -h|--help|help|"") usage ;; - *) - echo "unknown command: $cmd" >&2 - usage - ;; - esac -} - -main "$@" diff --git a/tools/project-status.py b/tools/project-status.py index 089f2719..8270c8fb 100755 --- a/tools/project-status.py +++ b/tools/project-status.py @@ -60,6 +60,13 @@ def worktree_rows(root: Path) -> list[dict]: parts = counts.split() if len(parts) == 2 and all(part.isdigit() for part in parts): behind, ahead = map(int, parts) + # What this worktree is really touching, replacing the + # advisory keys the retired claim system asked agents to + # declare by hand. A diff cannot go stale or lie. + _, changed = run( + ["git", "diff", "--name-only", "origin/main...HEAD"], path + ) + touching = [line for line in changed.splitlines() if line] _, timestamp = run(["git", "show", "-s", "--format=%ct", "HEAD"], path) age_hours = None if timestamp.isdigit(): @@ -71,6 +78,7 @@ def worktree_rows(root: Path) -> list[dict]: "head": current.get("HEAD", "")[:12], "dirty": bool(status), "dirty_entries": status.splitlines(), + "touching": touching, "ahead": ahead, "behind": behind, "last_commit_age_hours": age_hours, @@ -175,7 +183,6 @@ def consistency(payload: dict, now: float | None = None, stale_hours: float = 2. errors: list[str] = [] warnings: list[str] = [] worktrees = [row for row in payload["worktrees"] if "error" not in row] - activities = payload["activities"] runs = payload["runs"] for row in payload["worktrees"]: @@ -200,34 +207,9 @@ def consistency(payload: dict, now: float | None = None, stale_hours: float = 2. indexed[item_id] = row return indexed - activity_by_id = validate_ids(activities, "activity") run_by_id = validate_ids(runs, "run") - active_statuses = {"claimed", "blocked", "checking", "landing"} run_statuses = {"running", "ok", "fail"} - for item_id, activity in activity_by_id.items(): - status = str(activity.get("status", "")) - if status not in active_statuses | {"done", "abandoned"}: - errors.append(f"activity {item_id} has invalid status {status or '?'}") - continue - if status not in active_statuses: - continue - started = parse_time(activity.get("started")) - stale = started is not None and now - started > stale_after - worktree = linked_worktree(item_id, worktrees) - run = run_by_id.get(item_id) - problems = [] - if worktree is None: - problems.append("has no linked registered worktree") - # `worktree-new.sh` is also a supported low-level entry point and does - # not open a heartbeat. Absence is therefore not contradictory. Once - # an id has both records, however, active activity cannot point at a - # completed/failed run. - if run is not None and run.get("status") != "running": - problems.append("has no running heartbeat") - for problem in problems: - (warnings if stale else errors).append(f"activity {item_id} {problem}") - for item_id, run_row in run_by_id.items(): status = str(run_row.get("status", "")) if status not in run_statuses: @@ -241,14 +223,11 @@ def consistency(payload: dict, now: float | None = None, stale_hours: float = 2. continue stale = now - updated > stale_after worktree = linked_worktree(item_id, worktrees, run_row.get("worktree")) - activity = activity_by_id.get(item_id) problems = [] if worktree is None: explicit = run_row.get("worktree") detail = f" ({explicit})" if explicit else "" problems.append(f"has no linked registered worktree{detail}") - if activity is None or activity.get("status") not in active_statuses: - problems.append("has no active activity record") if stale: warnings.append( f"run {item_id} heartbeat is stale by {(now - updated) / 3600:.1f}h" @@ -317,9 +296,6 @@ def collect(root: Path, offline: bool) -> dict: "primary_root": str(primary), "recommended_next": next_lane, "work_orders": lanes, - # Storage keeps the historical path so older worktrees remain visible; - # these records are advisory activity, not exclusive ownership. - "activities": key_value_files(primary / ".agents/claims", ".claim"), "runs": run_files(primary / ".agents/runs"), "worktrees": worktree_rows(primary), "issues": issues(primary, offline), @@ -347,15 +323,6 @@ def human(payload: dict) -> str: lines.append( f" {row['priority']:>3} {row['task']} [{row['status']}/{row['class']}]{blocker}" ) - lines.extend(["", f"Activities ({len(payload['activities'])}):"]) - if payload["activities"]: - for activity in payload["activities"]: - lines.append( - f" {activity.get('id', '?')} {activity.get('status', '?')} " - f"paths={','.join(activity.get('keys', []))}" - ) - else: - lines.append(" none") lines.extend(["", f"Runs ({len(payload['runs'])}):"]) if payload["runs"]: for row in payload["runs"]: @@ -377,6 +344,11 @@ def human(payload: dict) -> str: if row["last_commit_age_hours"] is not None: flags.append(f"age {row['last_commit_age_hours']}h") lines.append(f" {row['branch']} ({', '.join(flags) or 'clean'}) {row['path']}") + touching = row.get("touching") or [] + if touching: + shown = ", ".join(touching[:4]) + more = f" +{len(touching) - 4} more" if len(touching) > 4 else "" + lines.append(f" touching: {shown}{more}") issue = payload["issues"] lines.extend(["", f"Decision issues: {'available' if issue['available'] else 'unavailable'}"]) for item in issue["items"]: diff --git a/tools/task.sh b/tools/task.sh index c2c5b992..5086cf9f 100755 --- a/tools/task.sh +++ b/tools/task.sh @@ -132,7 +132,6 @@ cmd_check() { local task=${1:-} wt [ -n "$task" ] || { usage >&2; exit 2; } wt=$(require_worktree "$task") - bash "$wt/tools/claim.sh" set-status "$task" checking bash "$wt/tools/heartbeat.sh" phase "$task" check-land (cd "$wt" && ./tools/check.sh --land) } @@ -166,7 +165,6 @@ cmd_finish() { printf '%s\n' "$primary_dirty" >&2 exit 1 } - bash "$wt/tools/claim.sh" set-status "$task" landing bash "$wt/tools/heartbeat.sh" phase "$task" rebase git -C "$wt" fetch origin git -C "$wt" rebase origin/main diff --git a/tools/test_project_ops.py b/tools/test_project_ops.py index fdf9fe7e..f10faf28 100755 --- a/tools/test_project_ops.py +++ b/tools/test_project_ops.py @@ -141,114 +141,12 @@ class ProjectOpsFixtures(unittest.TestCase): parsed = [scenario.parse(path) for path in sorted((repo / "scenarios").glob("*.agent"))] self.assertGreaterEqual(len(parsed), 3) - def test_activity_overlap_warns_without_blocking_and_task_missing_refuses(self) -> None: - repo = Path(__file__).resolve().parent.parent - claims = self.root / "claims" - env = os.environ.copy() - env["MISALIGNED_CLAIMS_DIR"] = str(claims) - env["MISALIGNED_CLAIM_PID"] = str(os.getpid()) - first = subprocess.run( - ["bash", "tools/claim.sh", "activity", "first", "--class", "sim", "--key", "crates/misaligned-core"], - cwd=repo, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - self.assertEqual(0, first.returncode, first.stdout) - second = subprocess.run( - ["bash", "tools/claim.sh", "claim", "second", "--class", "sim", "--key", "crates/misaligned-core/src/sim.rs"], - cwd=repo, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - self.assertEqual(0, second.returncode, second.stdout) - self.assertIn("overlap", second.stdout.lower()) - blocked = subprocess.run( - ["bash", "tools/claim.sh", "set-status", "second", "blocked"], - cwd=repo, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - self.assertEqual(0, blocked.returncode, blocked.stdout) - check = subprocess.run( - ["bash", "tools/claim.sh", "check", "--key", "crates/misaligned-core/src/sim.rs"], - cwd=repo, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - self.assertEqual(0, check.returncode, check.stdout) - self.assertIn("advisory", check.stdout.lower()) - missing = subprocess.run( - ["bash", "tools/task.sh", "finish", "definitely-missing"], - cwd=repo, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - self.assertNotEqual(0, missing.returncode) - self.assertIn("does not exist", missing.stdout) - planned = subprocess.run( - ["bash", "tools/task.sh", "start", "wiki/process/agent-scale.md", "--dry-run"], - cwd=repo, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - self.assertEqual(0, planned.returncode, planned.stdout) - self.assertIn("worktree-new.sh", planned.stdout) - self.assertIn("project-operations", planned.stdout) - - def test_dead_pid_activity_survives_while_task_worktree_exists(self) -> None: - repo = Path(__file__).resolve().parent.parent - claims = self.root / "claims" - claims.mkdir() - worktrees = self.root / "worktrees" - (worktrees / "project-operations").mkdir(parents=True) - (claims / "project-operations.claim").write_text( - "id=project-operations\nclass=process\nstatus=claimed\n" - "pid=999999\nstarted=test\nhost=test\nkey=tools/\n", - encoding="utf-8", - ) - env = os.environ.copy() - env["MISALIGNED_CLAIMS_DIR"] = str(claims) - env["MISALIGNED_WORKTREE_ROOT"] = str(worktrees) - result = subprocess.run( - ["bash", "tools/claim.sh", "list"], - cwd=repo, - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - check=False, - ) - self.assertEqual(0, result.returncode, result.stdout) - self.assertIn("project-operations", result.stdout) - self.assertTrue((claims / "project-operations.claim").is_file()) - - def test_project_status_consistency_links_live_activity_run_and_worktree(self) -> None: + def test_project_status_consistency_links_live_run_and_worktree(self) -> None: + # The advisory claim ledger was retired 2026-07-29: a running + # heartbeat must still point at a real worktree, but nothing has to + # declare itself first. now = 1_720_000_000.0 payload = { - "activities": [ - { - "record_name": "task-a", - "id": "task-a", - "status": "claimed", - "started": "2024-07-03T09:46:00Z", - } - ], "runs": [ { "record_name": "task-a", @@ -269,32 +167,17 @@ class ProjectOpsFixtures(unittest.TestCase): self.assertTrue(checked["ok"], checked) self.assertEqual([], checked["warnings"]) - def test_project_status_allows_low_level_activity_without_heartbeat(self) -> None: - payload = { - "activities": [ - { - "record_name": "task-a", - "id": "task-a", - "status": "claimed", - "started": "2024-07-03T09:46:00Z", - } - ], - "runs": [], - "worktrees": [ - { - "path": str(self.root / "task-a"), - "branch": "worktree-task-a", - } - ], - } - checked = project_status.consistency(payload, now=1_720_000_000.0) - self.assertTrue(checked["ok"], checked) - self.assertEqual([], checked["warnings"]) + def test_project_status_worktree_reports_what_it_touches(self) -> None: + rows = project_status.worktree_rows(Path(__file__).resolve().parents[1]) + primary = [row for row in rows if "error" not in row] + self.assertTrue(primary, rows) + for row in primary: + self.assertIn("touching", row) + self.assertIsInstance(row["touching"], list) def test_project_status_recent_broken_links_fail_but_stale_links_warn(self) -> None: now = 1_720_000_000.0 recent = { - "activities": [], "runs": [ { "record_name": "lost", @@ -308,7 +191,6 @@ class ProjectOpsFixtures(unittest.TestCase): checked = project_status.consistency(recent, now=now) self.assertFalse(checked["ok"]) self.assertTrue(any("no linked registered worktree" in error for error in checked["errors"])) - self.assertTrue(any("no active activity" in error for error in checked["errors"])) recent["runs"][0]["updated_at"] = "2024-07-03T01:00:00Z" checked = project_status.consistency(recent, now=now) @@ -318,20 +200,18 @@ class ProjectOpsFixtures(unittest.TestCase): def test_project_status_rejects_invalid_and_mismatched_records(self) -> None: payload = { - "activities": [ + "runs": [ { "record_name": "expected", "id": "other", - "status": "claimed", - "started": "2024-07-03T09:46:00Z", - } - ], - "runs": [ + "status": "running", + "updated_at": "2024-07-03T09:46:00Z", + }, { "record_name": "broken", "status": "invalid", "parse_error": "bad json", - } + }, ], "worktrees": [], } diff --git a/tools/tick-brief.sh b/tools/tick-brief.sh index 7a40bd1f..176a37dc 100755 --- a/tools/tick-brief.sh +++ b/tools/tick-brief.sh @@ -19,12 +19,8 @@ section() { section "recent commits" git log --oneline -15 || echo "(git log unavailable)" -section "activity" -if [ -x tools/claim.sh ]; then - tools/claim.sh list || echo "(claim list failed)" -else - echo "(tools/claim.sh missing)" -fi +section "live worktrees" +git worktree list || echo "(worktree list unavailable)" section "project status" if [ -f tools/project-status.py ]; then diff --git a/tools/worktree-done.sh b/tools/worktree-done.sh index 90f89e6d..bf3e4200 100755 --- a/tools/worktree-done.sh +++ b/tools/worktree-done.sh @@ -86,9 +86,4 @@ if git show-ref --verify --quiet "refs/heads/$branch"; then fi fi -if [ -x "$primary/tools/claim.sh" ] || [ -f "$primary/tools/claim.sh" ]; then - bash "$primary/tools/claim.sh" release "$task" --status "$status" 2>/dev/null || \ - echo "worktree-done: no activity named '$task' (ok)" -fi - echo "worktree-done: finished $task ($status)" diff --git a/tools/worktree-new.sh b/tools/worktree-new.sh index ed5ba728..366bcdfa 100755 --- a/tools/worktree-new.sh +++ b/tools/worktree-new.sh @@ -98,15 +98,13 @@ if [ "$seed" -eq 1 ]; then echo "worktree-new: seed skipped or failed (ok if no primary target cache)" fi -if [ "$do_claim" -eq 1 ]; then - if [ -z "$class" ]; then - class=process - fi - claim_args=(activity "$task" --class "$class") - for k in "${keys[@]+"${keys[@]}"}"; do - claim_args+=(--key "$k") - done - bash "$primary/tools/claim.sh" "${claim_args[@]}" +# 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 + 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 cat </.agents/claims`, gitignored). | -| `MISALIGNED_CLAIM_PID` | `tools/claim.sh` | pid | Holder pid written into an activity record (default: parent of the compatibility tool). | | `MISALIGNED_LANDING_WAIT` | `tools/task.sh finish` | `0` or `1` (default `1`) | When another task owns the final landing lock, `1` queues; `0` fails immediately. | | `MISALIGNED_LANDING_LOCK` | `tools/task.sh finish` | path | Override the final rebase/check/merge/push lock directory (default `/tmp/misaligned-landing.lock`). | | `MISALIGNED_RUNS_DIR` | `tools/heartbeat.sh` | path | Override run heartbeat dir (default `/.agents/runs`, gitignored). | -| `MISALIGNED_WORKTREE_ROOT` | `tools/worktree-new.sh`, `tools/worktree-done.sh`, `tools/claim.sh` | absolute path or repository-relative path | Override the canonical task-worktree root (default `/.Codex/worktrees` for create/remove helpers). `claim.sh` also recognizes task basenames in Git's registered worktree list, so dead helper PIDs do not reap active Letta/Claude worktrees outside that root. Creation and cleanup helpers must receive the same override. | +| `MISALIGNED_WORKTREE_ROOT` | `tools/worktree-new.sh`, `tools/worktree-done.sh` | absolute path or repository-relative path | Override the canonical task-worktree root (default `/.Codex/worktrees`). Creation and cleanup helpers must receive the same override. | | `MISALIGNED_LEDGER_MODE` | `tools/ledger_index.sh` | `write` or `check` | Internal: write regenerated indexes or fail if stale (set by the script, not hand-used). | | `MISALIGNED_LEDGER_ONLY` | `tools/ledger_index.sh` | `all`, `devlog`, or `specs` | Internal: which indexes to touch (set by the script flags). | | `MISALIGNED_SEED_TARGET_FROM` | `tools/seed-cargo-target.sh` | path | Source target directory to seed a worktree's private `target/` from (default: the primary checkout's `target/`). | diff --git a/wiki/log/2026-07-29-retire-claim-ledger.md b/wiki/log/2026-07-29-retire-claim-ledger.md new file mode 100644 index 00000000..aa54dc92 --- /dev/null +++ b/wiki/log/2026-07-29-retire-claim-ledger.md @@ -0,0 +1,59 @@ +# 2026-07-29 — The claim ledger is gone + +``` +Type: log +``` + +Cameron: "Can we get rid of the claim system" — then, after the case for it: +"I want it gone, update everything." `tools/claim.sh` and `.agents/claims/` +are deleted, and [agent-scale.md](../process/agent-scale.md) slice A is +retired rather than amended. + +**Why it went.** The ledger asked an agent to declare intent before starting, +and that declaration outlived the process that made it. Both claims live in +this repo when the decision was made were lies: `wire-law` was held by a dead +pid from an interrupted turn (a session had to `--force` past it), and +`reopened-untouched` had been held by a dead pid since 27 July. A list that +drifts toward noise trains agents to ignore it. + +It also never decided anything. Overlap always warned and proceeded, because +worktrees isolate edits — so the correct response to a conflicting claim was +always "continue." Meanwhile the collisions that actually cost time in +practice were in files every session touches: `save.rs`, `SAVE_VERSION`, the +canonical state fingerprint, the generated indexes. No declared key predicted +any of them. What protected the work was always worktrees, rebase, and the +gates. + +**What replaced it, and why it is strictly more honest.** Concurrency now +comes from facts that cannot go stale: + +- `git worktree list` — a worktree exists only while the work does, and + `worktree-done.sh` removes it. +- `tools/project-status.py` — the same list plus each worktree's **real + touched paths**, computed as `git diff --name-only origin/main...HEAD`. + That is the actual edit surface, not a guess made before the work started, + and it updates itself as the work moves. +- `.agents/runs/` heartbeats — already written beside every claim status + change `task.sh` used to make, which is the clearest sign the ledger was a + duplicate. + +**What did not change.** Overlap is still not ownership. Worktrees still +isolate edits and the landing agent still reconciles semantically against +current main. Only scarce phases stay serialized: the machine-wide Rust gate +and the short `task.sh finish` landing phase. The ROADMAP rule of at most one +sim+save-heavy work order stands — it is now read off live worktrees. + +**Surfaces updated.** `worktree-new.sh` (no longer records; still accepts +`--class`/`--key` so existing dispatch lines work, and says they are +ignored), `worktree-done.sh`, `task.sh`, `tick-brief.sh` (its "activity" +section is now "live worktrees"), `project-status.py` (payload, human output, +and the consistency rules that required an activity record), `check.sh`'s +tool list, `test_project_ops.py`, plus AGENT.md, workflows.md, env.md +(two retired variables), ROADMAP P1, and the two prompts that told agents to +declare themselves. + +The one real argument for keeping it, recorded so it is not rediscovered as +new: a claim announced intent *before* a worktree existed, which at high +parallelism is genuinely different information. It was judged not worth 379 +lines and a spec slice, because intent goes stale within hours and the +worktree appears seconds later anyway. diff --git a/wiki/log/DEVLOG.md b/wiki/log/DEVLOG.md index e71e6fd6..6a4ad495 100644 --- a/wiki/log/DEVLOG.md +++ b/wiki/log/DEVLOG.md @@ -36,6 +36,11 @@ add or amend a session log, then re-run the generator. - Intent: (see session log) - Log: [wiki/log/2026-07-29-roadmap-material-path.md](2026-07-29-roadmap-material-path.md) +## 2026-07-29 - The claim ledger is gone + +- Intent: (see session log) +- Log: [wiki/log/2026-07-29-retire-claim-ledger.md](2026-07-29-retire-claim-ledger.md) + ## 2026-07-29 - Resident procedures put automation on machines - Intent: (see session log) diff --git a/wiki/log/decisions/2026-07-29.md b/wiki/log/decisions/2026-07-29.md index 4eb735ce..d47779f6 100644 --- a/wiki/log/decisions/2026-07-29.md +++ b/wiki/log/decisions/2026-07-29.md @@ -29,3 +29,38 @@ Type: log folds. Owner: [context-menu.md](../../interface/context-menu.md). + +## Retire the advisory claim ledger + +### DECIDED + +- `tools/claim.sh` and `.agents/claims/` are deleted; agent-scale.md slice A + is retired rather than amended. Nothing declares intent before starting. +- Concurrency is derived from records that cannot outlive their work: live + worktrees (`git worktree list`) and heartbeat runs, with + `tools/project-status.py` reporting each worktree's real touched paths from + its diff against `origin/main`. +- Unchanged: overlap is not ownership, worktrees isolate edits, the landing + agent reconciles semantically, and only the Rust gate and the final landing + phase are serialized. At most one sim+save-heavy work order at a time + stands, now read off live worktrees. +- `worktree-new.sh` keeps accepting `--class`/`--key` so existing dispatch + lines keep working, records neither, and says so. + +### Why + +Every claim in the repo at decision time was stale — two records held by dead +pids, one of which a session had to `--force` past. The ledger also never +decided anything: overlap always warned and proceeded, while the collisions +that actually cost time (`save.rs`, `SAVE_VERSION`, the state fingerprint, +generated indexes) were in files no declared key covered. + +### Rejected + +- **Keeping it for intent-before-worktree.** A claim announces work seconds + before the worktree does, and goes stale within hours; not worth 379 lines + and a spec slice. +- **Fixing staleness with better reaping.** Treats the symptom; the record + would still decide nothing. + +Owner: [agent-scale.md](../../process/agent-scale.md) slice A. diff --git a/wiki/process/ROADMAP.md b/wiki/process/ROADMAP.md index e00db481..96875d73 100644 --- a/wiki/process/ROADMAP.md +++ b/wiki/process/ROADMAP.md @@ -942,12 +942,16 @@ Standing work orders for multi-agent throughput. Prefer **one slice per agent**. Do not start the crate workspace until compile cost or package edges clearly block parallel work — coordination slices land first. -### P1. Dispatch activity (advisory overlap) 🟩 process — DONE 2026-07-10 -- **Spec:** [agent-scale.md](agent-scale.md) slice A (HELD) -- **Landed:** `tools/claim.sh` (compatibility name; activity - record/list/status/release/check), gitignored `.agents/claims/`, AGENT + - prompts contract. Overlap warns and proceeds; only Rust-gate and final - landing phases are serialized. +### P1. Dispatch activity (advisory overlap) 🟩 process — RETIRED 2026-07-29 +- **Spec:** [agent-scale.md](agent-scale.md) slice A (RETIRED) +- **Landed 2026-07-10, removed 2026-07-29:** `tools/claim.sh` and + `.agents/claims/` are deleted. A declared record outlived the process that + made it and never decided anything — overlap always warned and proceeded. + Concurrency now comes from live worktrees and heartbeat runs, and + `project-status` prints each worktree's real touched paths. Overlap is + still not ownership; only the Rust gate and the final landing phase are + serialized. Log: + [claim retirement](../log/2026-07-29-retire-claim-ledger.md). ### P2. Append-only ledgers / generated indexes 🟩 process — DONE 2026-07-10 - **Spec:** [agent-scale.md](agent-scale.md) slice C (HELD) diff --git a/wiki/process/agent-scale.md b/wiki/process/agent-scale.md index 10d5726c..a4dc623f 100644 --- a/wiki/process/agent-scale.md +++ b/wiki/process/agent-scale.md @@ -67,73 +67,63 @@ independent landings unless a criterion names another as a hard prereq. | Bevy evidence | Headless/offscreen land proof, not only interactive windows | | Observability | Heartbeat files so "is it stuck?" is answerable | | Dispatch truth | Specs own structured work metadata; ROADMAP receives a generated live index | -| Project status | One human/JSON command combines work orders, activity, runs, worktrees, issues, and freshness | +| Project status | One human/JSON command combines work orders, runs, worktrees, issues, and freshness | | Scenario evidence | Deterministic agent scripts emit transcripts and machine-readable evidence | | Task lifecycle | One safe wrapper starts, checks, lands, or abandons a metadata-backed work order | | Environment | One doctor command reports missing required and optional local capabilities | -## 1. Dispatch activity (advisory overlap) +## 1. Dispatch activity — RETIRED 2026-07-29 ### Behavior -Before an agent starts repository work that can edit code or binding wiki -pages, it records work-order **activity**: - -- **id** — worktree/task name (e.g. `dark-frame`) -- **class** — `docs` | `frontend` | `sim` | `save` | `process` -- **exclusive_keys** — legacy metadata name for likely edit surfaces (for - example `crates/misaligned-bevy/src/main.rs`, - `crates/misaligned-core/src/save.rs`, or a binding wiki page). These paths - are collision information, not ownership. -- **status** — `claimed` → `checking` → `landing` → `done`, with `blocked` - and `abandoned` available when the task is not progressing. - -A second activity record whose paths intersect an active task **warns and -proceeds**. Worktrees isolate edits; Git exposes textual collisions; the -corpus, tests, and landing agent resolve semantic collisions against current -main. Hard path exclusion was retired after it created a wait cycle on -2026-07-10: action-contract held `main.rs` while waiting for camera-exposure -work that needed the same file. A blocked task must never stop its own -prerequisite. - -Only scarce phases are serialized: the machine-wide Rust gate and the short -`task.sh finish` landing phase. The ROADMAP rule of at most one sim+save-heavy -work order remains an explicit dispatch constraint rather than a path lock. - -Storage keeps the historical machine-local gitignored path -`.agents/claims/.claim` (override with `MISALIGNED_CLAIMS_DIR`). -The `claim.sh` filename and `claim` verb are compatibility interfaces for -older worktrees; `activity` is the current verb. Discover with -`tools/claim.sh list`. -Holder pid is the calling agent/shell -(`PPID`, or `MISALIGNED_CLAIM_PID`). Dead holders are reaped on the next -claim/list. No Tangled auth required. +**There is no activity ledger.** `tools/claim.sh` and `.agents/claims/` are +deleted. Concurrency is read from facts that cannot go stale: the live +worktrees (`git worktree list`, and `tools/project-status.py`, which also +prints what each worktree actually touches) and the heartbeat runs under +`.agents/runs/`. + +Why it went. The ledger asked an agent to declare intent before starting, and +that declaration outlived the process that made it: a killed or finished +session left a record claiming work nobody was doing, so the list drifted +toward noise and agents learned to `--force` past it. It also never decided +anything — overlap always warned and proceeded, because worktrees isolate +edits — while the collisions that actually cost time were in files every +session touches (`save.rs`, the save version, generated indexes) and no +declared key predicted them. What protected the work was always worktrees, +rebase, and the gates. + +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. + +What did **not** change: overlap is still not ownership, worktrees still +isolate edits, and the landing agent still reconciles semantically against +current main. Only scarce phases are serialized — the machine-wide Rust gate +and the short `task.sh finish` landing phase. The ROADMAP rule of at most one +sim+save-heavy work order remains an explicit dispatch constraint, now read +off live worktrees rather than declared records. ```bash -tools/claim.sh activity dark-frame --class frontend --key crates/misaligned-bevy/src/main.rs \ - --key wiki/interface/material-dark-frame.md -tools/claim.sh list -tools/claim.sh set-status dark-frame blocked # visible, never a path lock -tools/claim.sh set-status dark-frame checking # or landing -tools/claim.sh release dark-frame # done (or --status abandoned) -# bundled with worktree create/remove: -tools/worktree-new.sh dark-frame --class frontend --key crates/misaligned-bevy/src/main.rs -tools/worktree-done.sh dark-frame +git worktree list # who is working, and where +python3 tools/project-status.py # plus what each worktree touches, and runs ``` ROADMAP conflict flags (🟥 sim+save / 🟧 sim / 🟩 isolated) remain the dispatch -index. Path activity predicts reconciliation cost; it does not prohibit work. +index. -### Acceptance criteria (slice A) — HELD 2026-07-09 +### Acceptance criteria (slice A) — RETIRED 2026-07-29 -1. A documented activity procedure exists (tool or script + AGENT.md / - prompts pointer) that records class + likely paths + status. -2. Attempting a second overlapping activity succeeds with a warning naming - the other task and paths; it never creates a wait dependency. -3. `blocked` is representable, and completing or abandoning work clears its - activity record. -4. ROADMAP or workflows.md states that agents inspect overlaps, work in - isolated worktrees, and reconcile at landing. +The original four criteria described the activity record and are withdrawn +with it. Their replacement is one criterion, met by the same landing: + +1. Concurrency is derivable without any hand-maintained record: live + worktrees and heartbeat runs are the only sources, `project-status` + reports each worktree's real touched paths, and its consistency check + requires a running heartbeat to name a registered worktree without + requiring anything to have declared itself first. ## 2. Verification phases (fast vs land) @@ -417,11 +407,10 @@ corpus and wiki checks. It enforces facts that are mechanical enough to prove: that the same current page declares closed; - advertised path keys exist, so retired workspace paths cannot silently remain dispatch locks. -- fresh activity, heartbeat/run, and registered-worktree records agree: an - active run cannot lose its activity record or point at no worktree, and a - claimed/checking/landing activity must name a registered worktree. Stale - historical runs degrade to warnings so one abandoned heartbeat cannot brick - the repository forever. +- heartbeat/run and registered-worktree records agree: an active run cannot + point at no worktree. Stale historical runs degrade to warnings so one + abandoned heartbeat cannot brick the repository forever. (The activity + half of this rule retired with the claim ledger, slice A.) Semantic design disagreements still belong to ticks; the gate must not guess at meaning from ordinary prose. Network-only issue truth is shown by project diff --git a/wiki/process/workflows.md b/wiki/process/workflows.md index 95c7f19b..0c44c021 100644 --- a/wiki/process/workflows.md +++ b/wiki/process/workflows.md @@ -59,15 +59,17 @@ tools/ledger_index.sh --check # used by ./tools/check.sh docs path Do not hand-edit those two generated files. -**Activity and worktrees** (advisory overlap — see -[agent-scale.md](agent-scale.md)): +**Worktrees** (advisory overlap — see [agent-scale.md](agent-scale.md); the +claim ledger was retired 2026-07-29, so live worktrees and heartbeat runs are +the concurrency truth): ```bash -tools/worktree-new.sh dark-frame --class frontend --key crates/misaligned-bevy/ -tools/claim.sh list +tools/worktree-new.sh dark-frame +git worktree list # who else is live +python3 tools/project-status.py # plus what each worktree actually touches tools/heartbeat.sh start dark-frame --phase implement ./tools/check.sh --land -tools/worktree-done.sh dark-frame # clear activity, prune target/, remove worktree +tools/worktree-done.sh dark-frame # prune target/, remove worktree ``` Specs with structured work-order metadata have a shorter safe doorway: -- 2.51.2