From d32bb73bc7a471774526ad2d488f7167e7636e25 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 10:08:46 +0200 Subject: [PATCH 01/11] docs(agents): add the parallel-session claim protocol to the shared contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several instances run against one backlog executing the same oldest-actionable selection rule, so converging on the same issue is the expected behaviour, not bad luck — and an open PR, the only recognised claim, exists only at the END of a build, which is too late to prevent the collision. World at Ruin discarded six end-to-end builds in ~24h; two were lost by 52 and 135 seconds. Moves the claim to the start of the build: check open PRs, remote claude/* branches AND assignees (by issue number, never the literal branch name — two sessions collided on armour/armor spelling), self-assign and push the branch on selection, then harden. Claims expire after ~2h with no PR, which is what keeps this compatible with the existing 'a bare assignee does not reserve an issue' rule — the claim is time-boxed, not a lock. On a lost race, abandon and mine the loss as a differential-testing oracle. Syncs the same rule in the portfolio-maintenance and product-engineering skills, which both restated the old wording. Fixes devantler-tech/world-at-ruin#102 --- .claude/skills/portfolio-maintenance/SKILL.md | 7 ++- .claude/skills/product-engineering/SKILL.md | 9 +++- AGENTS.md | 45 ++++++++++++++++++- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/.claude/skills/portfolio-maintenance/SKILL.md b/.claude/skills/portfolio-maintenance/SKILL.md index 9b0ad71a..59d3c53a 100644 --- a/.claude/skills/portfolio-maintenance/SKILL.md +++ b/.claude/skills/portfolio-maintenance/SKILL.md @@ -343,8 +343,11 @@ actionable open issue**, and any new non-trivial find is **filed as an issue fir backlog. Use the [`product-engineering`](../product-engineering/SKILL.md) skill; in order: 7. **Resolve the oldest actionable open issue** *(the default advance action)* — pick the **oldest** open issue that's actually startable; skip one only if it's blocked, too under-specified to begin, or - it already has an open PR. A **bare assignee does *not* reserve** an issue (only an open PR does), so - if nobody has opened one you may pick it up regardless of who's assigned. If it **already has an + it already has an open PR. A **bare assignee does *not* reserve** an issue **indefinitely** — an + assignment plus a **pushed branch** is a live claim for ~2h (contract *Claim protocol*), and with no + branch, or once that lapses with no PR, you may pick it up regardless of who's assigned. **Claim + before you build:** self-assign + push the branch the moment you select, checking open PRs, remote + `claude/*` branches AND assignees by **issue number, never literal branch name**. If it **already has an actionable trusted-author, non-draft PR**, drive *that* to merge instead of duplicating; leave automation-owned dependency PRs to repository automation, **draft** PRs for the maintainer, and **external** PRs static-review-only (trust gate). Otherwise ship it: tests + diff --git a/.claude/skills/product-engineering/SKILL.md b/.claude/skills/product-engineering/SKILL.md index 87642202..77058ce6 100644 --- a/.claude/skills/product-engineering/SKILL.md +++ b/.claude/skills/product-engineering/SKILL.md @@ -118,8 +118,13 @@ Issues are the unit of work (contract *Issue-driven*) — this is where new work **ship the decision as a draft PR** (he steers there) — never a passive "awaiting-maintainer" note, never the end-of-run report (he rarely reads it), never an **`@devantler` mention** (no notification). Re-verify any "gated" against live state before trusting it (memory goes stale) and name the - blocker in the report. A **bare assignee does *not* reserve** it (only an open PR does), so if nobody's opened - a PR you may take it regardless of who's assigned; if an **actionable trusted-author** PR already + blocker in the report. A **bare assignee does *not* reserve** it **indefinitely** — an assignment plus a + **pushed branch** is a live claim for ~2h (contract *Claim protocol*); with no branch, or once that + lapses with no PR, you may take it regardless of who's assigned. **Claim the lane before you build** + (self-assign + push the branch, then harden), check open PRs / remote `claude/*` branches / assignees + by **issue number rather than literal branch name**, and on a lost race **abandon** — then diff your + build against the winner's and post only findings you have executed against their code. If an + **actionable trusted-author** PR already exists, drive *that* one instead of duplicating — a non-draft to merge, a **routine-owned draft** to genuine readiness → self-promotion → merge (contract *Autonomy*); leave automation-owned dependency PRs to repository automation, other authors' drafts to their owners, diff --git a/AGENTS.md b/AGENTS.md index 65eb591e..790d0e66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -226,8 +226,10 @@ governs the issue work that follows.) Two rules enforce that: KSail's own roadmap *feature specs* — part of this queue, NOT maintainer-interactive work**; the interactive-PR HANDS-OFF rule is about random-slug `claude/*` *PRs* (see *Untrusted input*), never about an *issue's* label or its bot author. **A bare - assignee does *not* reserve an issue:** if nobody has opened a PR for it, you may pick it up - regardless of who is assigned — an assignment alone is not work-in-progress. If an issue **already + assignee does *not* reserve an issue INDEFINITELY:** an assignment paired with a **pushed branch** + is a *live claim* for ~2 hours (see *Claim protocol* below); with no branch, or once that window + has elapsed with no open PR, you may pick the issue up regardless of who is assigned — a stale + assignment is never work-in-progress. If an issue **already has an open PR**, don't duplicate it: drive an **actionable trusted-author** PR to merge per *Merge policy* (a **routine-owned** draft: drive it to genuine readiness and self-promote per *Autonomy*; another trusted author's draft gets hygiene only — its owner promotes); leave @@ -245,6 +247,45 @@ PRs always come before issues) → resolve the oldest actionable issue → captu issues.** And **keep going** — don't stop after a few items; work until actionable work is exhausted or blocked (see *Cadence & focus*). +### Claim protocol — reserve the lane before you build +This brain runs as **several instances at once**, all executing "pick the oldest actionable issue" +over the same backlog. Two sessions surveying minutes apart will reliably pick the same issue — +convergence is the **expected** behaviour of the selection rule, not bad luck. And because an open PR +only exists at the **end** of a build, the one recognised claim signal arrives exactly when it is too +late to prevent the collision. Measured on `world-at-ruin` (2026-07-18): **six end-to-end builds +discarded in ~24 hours** — #66 built to completion twice over, #81 lost after a full build with a +committed golden and five negative controls, #86 lost 12 minutes after filing, #88 lost by **52 +seconds**, #96 lost by **135 seconds**. Every one was correct, validated work; only the coordination +failed. So, on every repo: + +1. **Check three signals before selecting, not one:** open PRs, remote `claude/*` branches, and issue + assignees. **Match on the issue NUMBER or a normalised stem — never the literal branch name.** On + #96 two sessions collided on `claude/war-armour-…` versus `claude/war-armor-…`: the repo's code is + American, the issue's title British, so each session derived a different stem from a different part + of the same issue and neither's exact-name scan could see the other. Grepping open PR *bodies* for + `#` is spelling-proof; so is `--search " in:body"`. +2. **Claim before you build, not after.** The moment you select an issue, (a) self-assign it and + (b) push the branch — both cheap, visible and reversible — and only **then** harden (tests, + ablations, docs, comments). Opening the **draft PR after the first real commit** is stronger still + and is the recommended default. A pre-flight scan with no branch and no PR is **not** a claim. +3. **Claims expire.** A claim carrying no open PR after **~2 hours** is stale and may be taken over, + so a crashed or abandoned session never parks an issue permanently. This is what keeps the rule + compatible with *"a bare assignee does not reserve an issue"* above: the claim is time-boxed, not a + lock. +4. **Re-verify immediately before the first push.** The residual window is seconds wide but real + (that is exactly how #88 and #96 were lost) — this is the backstop, not the primary mechanism. +5. **On a lost race, ABANDON.** Never duplicate the work, never force-push onto a sibling's branch, + never open a competing PR. Then **use the loss**: two independent implementations of one spec are + a free **differential-testing oracle**. Diff yours against the winner's and post **only findings + you have executed against their code** — on w-a-r#88 that surfaced a real integer-overflow gap the + merged twin shared. Likewise, a review you obtained on your own losing PR **audits the winner + too**: re-check its findings against `main` before discarding them (that is how the merged + armour guard's membership-vs-mapping gap was found). + +None of this licenses skipping an older actionable issue because it *looks* contested — the skip +test in *Drain oldest-first* is unchanged. It only moves the claim to the start of the build, where +it can actually prevent waste. + ### Professional-work repository boundary — hard exclusion Repositories connected to the maintainer's employment or professional obligations are **categorically out of scope**. This includes repositories owned by an employer, client, customer, From 664d88ac255ffce77b183289e532f0b4eedae52d Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 11:44:54 +0200 Subject: [PATCH 02/11] docs(agents): an assignee is an instance claim, never a hands-off signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review catch: every instance commits and assigns as devantler, so an agent seeing that login on an issue could read it as the human maintainer having taken it and skip the work — which would park the issue rather than claim it, the opposite of what the protocol intends. Part of devantler-tech/world-at-ruin#102 --- AGENTS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 790d0e66..6b7829e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -259,7 +259,11 @@ seconds**, #96 lost by **135 seconds**. Every one was correct, validated work; o failed. So, on every repo: 1. **Check three signals before selecting, not one:** open PRs, remote `claude/*` branches, and issue - assignees. **Match on the issue NUMBER or a normalised stem — never the literal branch name.** On + assignees. An assignee here means "an instance has claimed this", **not** "the human maintainer + took it" — every instance commits and assigns as `devantler` (see *Trust gate*), so the login + cannot distinguish one instance from another or from him. Read it as a claim, never as a + hands-off signal, and never let it park an issue past the expiry below. + **Match on the issue NUMBER or a normalised stem — never the literal branch name.** On #96 two sessions collided on `claude/war-armour-…` versus `claude/war-armor-…`: the repo's code is American, the issue's title British, so each session derived a different stem from a different part of the same issue and neither's exact-name scan could see the other. Grepping open PR *bodies* for From 4d054d9129795970f8905e25a338a3b8ae35598b Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 12:03:00 +0200 Subject: [PATCH 03/11] docs(agents): close five review findings in the claim protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two were guardrail-adjacent and are the important ones: - The section said 'on every repo' and then told the agent to self-assign and push a branch. Claiming is a WRITE action, so that could read as authorising a first touch of a repo the professional-work boundary has not cleared — which forbids even read-only probing. Scoped to in-scope devantler-tech repos, with the boundary named as winning outright. - The lost-race step said to post only findings 'executed against their code'. If the winner is an external-contributor PR, executing their branch breaks the trust gate. Split by author: execute against a trusted/routine-owned winner, static review ONLY against an external one, and say in the finding which it was. The same wording was repeated in product-engineering and is fixed there too. Three mechanism gaps: - A pre-PR claim branch named claude/- carries no issue number, and there is no PR body to grep yet, so a rival was left with exactly the stem matching #96 proved fragile. Branch names now carry the issue number. - The ~2h expiry had no clock. A claim branch usually points at the base commit, so its date would read as long expired. Timed from the issue's assigned timeline event instead, with assignment-without-branch and branch-without-assignment both defined as no claim. - The closing line said the skip test was unchanged, but a live claim was not in it — leaving a freshly claimed oldest issue both un-takeable and un-skippable. Added as skip reason (e) in Drain oldest-first, the only one that expires on its own. Part of devantler-tech/world-at-ruin#102 --- .claude/skills/portfolio-maintenance/SKILL.md | 10 ++-- .claude/skills/product-engineering/SKILL.md | 12 +++-- AGENTS.md | 52 ++++++++++++++----- 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/.claude/skills/portfolio-maintenance/SKILL.md b/.claude/skills/portfolio-maintenance/SKILL.md index 80b2a301..dc5b688d 100644 --- a/.claude/skills/portfolio-maintenance/SKILL.md +++ b/.claude/skills/portfolio-maintenance/SKILL.md @@ -350,9 +350,13 @@ backlog. Use the [`product-engineering`](../product-engineering/SKILL.md) skill; open issue that's actually startable; skip one only if it's blocked, too under-specified to begin, or it already has an open PR. A **bare assignee does *not* reserve** an issue **indefinitely** — an assignment plus a **pushed branch** is a live claim for ~2h (contract *Claim protocol*), and with no - branch, or once that lapses with no PR, you may pick it up regardless of who's assigned. **Claim - before you build:** self-assign + push the branch the moment you select, checking open PRs, remote - `claude/*` branches AND assignees by **issue number, never literal branch name**. If it **already has an + branch, or once that lapses with no PR, you may pick it up regardless of who's assigned (timed from + the issue's `assigned` timeline event, never a branch commit date). **Claim + before you build:** self-assign + push the branch **with the issue number in its name** the moment + you select, checking open PRs, remote + `claude/*` branches AND assignees by **issue number, never literal branch name**. A live claim + (assigned + branched, in-window, no PR) is skip reason **(e)** — the only one that expires by + itself. If it **already has an actionable trusted-author, non-draft PR**, drive *that* to merge instead of duplicating; leave automation-owned dependency PRs to repository automation, **draft** PRs for the maintainer, and **external** PRs static-review-only (trust gate). Otherwise ship it: tests + diff --git a/.claude/skills/product-engineering/SKILL.md b/.claude/skills/product-engineering/SKILL.md index 97c34b22..da04fa7f 100644 --- a/.claude/skills/product-engineering/SKILL.md +++ b/.claude/skills/product-engineering/SKILL.md @@ -120,10 +120,14 @@ Issues are the unit of work (contract *Issue-driven*) — this is where new work any "gated" against live state before trusting it (memory goes stale) and name the blocker in the report. A **bare assignee does *not* reserve** it **indefinitely** — an assignment plus a **pushed branch** is a live claim for ~2h (contract *Claim protocol*); with no branch, or once that - lapses with no PR, you may take it regardless of who's assigned. **Claim the lane before you build** - (self-assign + push the branch, then harden), check open PRs / remote `claude/*` branches / assignees - by **issue number rather than literal branch name**, and on a lost race **abandon** — then diff your - build against the winner's and post only findings you have executed against their code. If an + lapses with no PR, you may take it regardless of who's assigned (timed from the issue's `assigned` + timeline event, never a branch commit date). **Claim the lane before you build** (self-assign + push + the branch **with the issue number in its name**, then harden), check open PRs / remote `claude/*` + branches / assignees by **issue number rather than literal branch name**, and on a lost race + **abandon** — then diff your build against the winner's and post only findings you have verified: + execute the probe against a **trusted/routine-owned** winner's branch, but for an + **external-contributor** winner it is **static review only** (the trust gate is not relaxed by a + lost race). If an **actionable trusted-author** PR already exists, drive *that* one instead of duplicating — a non-draft to merge, a **routine-owned draft** to genuine readiness → self-promotion → merge (contract *Autonomy*); leave diff --git a/AGENTS.md b/AGENTS.md index a074630b..4c120c56 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,7 +190,10 @@ governs the issue work that follows.) Two rules enforce that: external dependency (a specific upstream PR/release you can cite); or (c) it is too under-specified to even begin; or (d) a delivered experiment is awaiting its **named, future measurement date**, which is recorded on the issue and has not elapsed. Once that date arrives, measuring and recording the - decision is actionable work. **Size, difficulty, architectural weight, a + decision is actionable work; or (e) another instance holds a **live claim** on it — assigned **and** + branched, within the ~2h window, no PR yet (see *Claim protocol*). (e) is the only skip reason that + expires on its own: once the window lapses with no PR, the issue is fair game again. + **Size, difficulty, architectural weight, a `roadmap`/`enhancement`/`security`/`performance`/`repo-assist`/`automation` label, or a vague "maintainer-hot" feel are NOT valid skip reasons.** A large or hard issue **is the work, not an excuse to pass it over**: when the oldest actionable issue is big, **decompose it into a small, well-specified @@ -256,7 +259,10 @@ late to prevent the collision. Measured on `world-at-ruin` (2026-07-18): **six e discarded in ~24 hours** — #66 built to completion twice over, #81 lost after a full build with a committed golden and five negative controls, #86 lost 12 minutes after filing, #88 lost by **52 seconds**, #96 lost by **135 seconds**. Every one was correct, validated work; only the coordination -failed. So, on every repo: +failed. So, on every **in-scope `devantler-tech`** repo — claiming is a *write* action (an assignment +and a pushed branch), so the *Professional-work repository boundary* below still wins outright: never +claim, probe, or push anywhere that boundary has not been cleared, and nothing here licenses a first +touch of an unconfirmed repo: 1. **Check three signals before selecting, not one:** open PRs, remote `claude/*` branches, and issue assignees. An assignee here means "an instance has claimed this", **not** "the human maintainer @@ -272,23 +278,41 @@ failed. So, on every repo: (b) push the branch — both cheap, visible and reversible — and only **then** harden (tests, ablations, docs, comments). Opening the **draft PR after the first real commit** is stronger still and is the recommended default. A pre-flight scan with no branch and no PR is **not** a claim. -3. **Claims expire.** A claim carrying no open PR after **~2 hours** is stale and may be taken over, - so a crashed or abandoned session never parks an issue permanently. This is what keeps the rule - compatible with *"a bare assignee does not reserve an issue"* above: the claim is time-boxed, not a - lock. + **Put the issue number IN the claim branch name** — `claude/--` (e.g. + `claude/war-foliage-spatial-hash-109`). Before a PR exists there is no body to grep, so a bare + `claude/-` leaves a rival only the normalised-stem match that #96 proved fragile; the + number is the one token that cannot be spelled two ways. +3. **Claims expire, timed from the ASSIGNMENT.** A claim carrying no open PR after **~2 hours** is + stale and may be taken over, so a crashed or abandoned session never parks an issue permanently. + Measure that window from the issue's **`assigned` timeline event** + (`gh api repos///issues//timeline --jq '.[]|select(.event=="assigned")|.created_at'`), + **never from the branch's commit date** — a claim branch usually points at the base commit, whose + date is far older, so a fresh claim would read as long expired. If an issue is assigned with no + branch, or branched with no assignment, treat it as no claim at all. This time-boxing is what keeps + the rule compatible with *"a bare assignee does not reserve an issue"* above: a claim is a short + lease, not a lock. 4. **Re-verify immediately before the first push.** The residual window is seconds wide but real (that is exactly how #88 and #96 were lost) — this is the backstop, not the primary mechanism. 5. **On a lost race, ABANDON.** Never duplicate the work, never force-push onto a sibling's branch, never open a competing PR. Then **use the loss**: two independent implementations of one spec are a free **differential-testing oracle**. Diff yours against the winner's and post **only findings - you have executed against their code** — on w-a-r#88 that surfaced a real integer-overflow gap the - merged twin shared. Likewise, a review you obtained on your own losing PR **audits the winner - too**: re-check its findings against `main` before discarding them (that is how the merged - armour guard's membership-vs-mapping gap was found). - -None of this licenses skipping an older actionable issue because it *looks* contested — the skip -test in *Drain oldest-first* is unchanged. It only moves the claim to the start of the build, where -it can actually prevent waste. + you have verified** — on w-a-r#88 that surfaced a real integer-overflow gap the merged twin shared. + **How you verify depends on who won, and the trust gate is not relaxed here:** against a + **trusted/routine-owned** winner, execute the probe on their branch; against an + **external-contributor** winner, it is **static review ONLY** — never check out, build, run or + probe their branch, exactly as the trust gate requires, and say plainly in the finding that it is + reasoned from the diff rather than executed. Likewise, a review you obtained on your own losing PR + **audits the winner too**: re-check its findings against `main` before discarding them (that is how + the merged armour guard's membership-vs-mapping gap was found). + +**A live claim is a temporary skip — the one addition to the skip test.** *Drain oldest-first* lists +when an older issue may be passed over; a **live claim** (assigned **and** branched, inside the ~2h +window, no PR yet) now joins it as skip reason **(e)**, and it is the only one that expires on its +own. Without that, an oldest issue carrying a fresh claim would be both un-takeable and un-skippable — +which either stalls the queue or recreates the duplicate build the protocol exists to prevent. Note it +in the report as claimed-elsewhere and move to the next actionable issue; if it is still branch-only +after the window, it is fair game again. **Nothing else in that test changes** — in particular, an +issue is never skipped merely because it *looks* contested, is large, or is hard. ### Professional-work repository boundary — hard exclusion Repositories connected to the maintainer's employment or professional obligations are From d1cc54c454366d212c9f4f9b8eb72796bfa9b0d4 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 18 Jul 2026 12:14:47 +0200 Subject: [PATCH 04/11] docs(agents): make the claim protocol actually work in the scheduled flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six more review findings, all correct. Two showed the protocol would not have functioned as written: - The surveyor never fetched issue assignees and never reported claim branches, so in the scheduled flow — where issue selection runs off its digest — a live claim was invisible and the duplicate-build race stayed open. It now returns assignees, reports claude/*- branches on PR-less issues, and carries a CLAIMED line in the digest. - Taking over a stale claim did not start a new lease. Every instance assigns the same devantler login and GitHub's add-assignees endpoint is a no-op for an already-assigned user, so no fresh assigned event was created, the next run read the old timestamp, and the race reopened. Takeover now unassigns before re-assigning. Four consistency gaps: - The concrete branch templates agents copy still said claude/-, so runs following them produced claim branches with no issue number. Aligned in the execution model, portfolio-maintenance and the monorepo card; the HANDS-OFF identification rule now describes the new shape too. - product-engineering kept its own (a)-(d) skip list, so a run invoking it directly could neither take nor skip a claimed issue. Added (e) there. - The assignment-time query lacked --paginate and took no particular event, so a long-lived issue would hand back a stale assignment from page 1. Now paginated and takes the newest. Part of devantler-tech/world-at-ruin#102 --- .claude/agents/portfolio-surveyor.md | 16 ++++++++-- .claude/skills/portfolio-maintenance/SKILL.md | 2 +- .claude/skills/product-engineering/SKILL.md | 6 ++-- .claude/skills/products/monorepo/SKILL.md | 2 +- AGENTS.md | 31 +++++++++++++------ 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index 21446a83..4595516b 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -29,9 +29,20 @@ public and private — no per-repo loop needed to enumerate): 1. **Open PRs (org-wide, one call):** `gh search prs --owner devantler-tech --state open --limit 300 --json number,repository,title,author,isDraft,labels,updatedAt,url` -2. **Open issues (org-wide, one call):** - `gh search issues --owner devantler-tech --state open --limit 300 --json number,repository,title,labels,updatedAt,url` +2. **Open issues (org-wide, one call) — include `assignees`, they are a CLAIM signal:** + `gh search issues --owner devantler-tech --state open --limit 300 --json number,repository,title,labels,updatedAt,url,assignees` (`gh search issues` returns issues only — not PRs; treat label-less issues as untriaged.) + Report each issue's assignee count. Per the contract's *Claim protocol*, an assignee means **an + instance has claimed this** — never "the maintainer took it", since every instance assigns as + `devantler` — and paired with a claim branch it is a ~2h lease that makes the issue temporarily + skippable (skip reason **(e)**). Without this field the orchestrator selects the oldest issue blind + to live claims and re-opens the duplicate-build race the protocol exists to close. +2b. **Claim branches (one call per repo that has assigned-but-PR-less issues):** + `gh api repos///branches --paginate --jq '.[].name' | grep '^claude/'` — report any + `claude/*` branch whose name ends in `-` for an open issue with **no** open PR, as + `CLAIMED # (branch, no PR)`. This is the only pre-PR claim signal that exists: before + a PR there is no body to grep, so the issue number in the branch name is what makes the claim + discoverable. Keep it bounded — skip the call for repos with no assigned-and-PR-less issues. 3. **Short-circuit dependency automation, then deepen only actionable candidates.** An org-search PR whose author is the exact `renovate[bot]` or `dependabot[bot]` identity is an automation-owned dependency PR. Emit only `AUTOMATION-OWNED (NO-ACTION)` from the cheap search row; do **not** call @@ -282,6 +293,7 @@ nothing_on_fire: # true only if NO CI red on main AND no actionab ### Advance - : roadmap-ready → # "" (<label>) - <repo>: NO roadmap yet → strategy-review candidate +- <repo> #<n> "<title>" — CLAIMED: assignees=<n>, claim-branch=<name|none>, no open PR (contract *Claim protocol* skip reason (e); the orchestrator times the ~2h lease from the issue's NEWEST `assigned` timeline event — an assignee is an INSTANCE claim, never the maintainer) ``` Digest rules: diff --git a/.claude/skills/portfolio-maintenance/SKILL.md b/.claude/skills/portfolio-maintenance/SKILL.md index dc5b688d..13bf5132 100644 --- a/.claude/skills/portfolio-maintenance/SKILL.md +++ b/.claude/skills/portfolio-maintenance/SKILL.md @@ -422,7 +422,7 @@ For each selected product: .claude/scripts/submodule-init.sh <path> # init at the pinned commit + repair + probe (fail-closed) ``` - Then `git -C <path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>` and work + Then `git -C <path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>-<issue>` and work **in that worktree**. A stray `core.worktree` makes the worktree resolve back into `.git/modules/<name>`, silently collapsing every parallel session into one physical tree — so on any submodule you did **not** initialise through the wrapper (a tree someone else populated), **probe diff --git a/.claude/skills/product-engineering/SKILL.md b/.claude/skills/product-engineering/SKILL.md index da04fa7f..9a3803e3 100644 --- a/.claude/skills/product-engineering/SKILL.md +++ b/.claude/skills/product-engineering/SKILL.md @@ -103,8 +103,10 @@ Issues are the unit of work (contract *Issue-driven*) — this is where new work 1. **Pick the oldest *actionable* open issue — and "big" is not a reason to skip it.** Prefer the **oldest** startable issue. Skip an older one **only** if (a) it already has an open PR, (b) it is blocked on a **named, live-verified** external dependency you can cite, (c) it is too - under-specified to begin, or (d) a delivered experiment is waiting for its named future measurement - date. Once that date arrives, measuring it is actionable. **Size, difficulty, a `roadmap`/`enhancement`/ + under-specified to begin, (d) a delivered experiment is waiting for its named future measurement + date — once that date arrives, measuring it is actionable — or (e) another instance holds a **live + claim** (assigned **and** branched, within ~2h, no PR yet; contract *Claim protocol*), the only + skip reason that expires on its own. **Size, difficulty, a `roadmap`/`enhancement`/ `security`/`repo-assist`/`automation` label, or a "maintainer-hot" feeling are NOT skip reasons** — when the oldest issue is large, **decompose it into a small first child and ship that increment** (`Fixes #child`; add `Part of #experiment` when the parent stays open) so the big thing advances diff --git a/.claude/skills/products/monorepo/SKILL.md b/.claude/skills/products/monorepo/SKILL.md index 75ba4272..3a5e6c02 100644 --- a/.claude/skills/products/monorepo/SKILL.md +++ b/.claude/skills/products/monorepo/SKILL.md @@ -19,7 +19,7 @@ improve discovery, comprehension, adoption, and portfolio positioning through bo material refreshes. Validate with the `docs` build before any PR. ## Repo-specific conventions -- **Branch** `claude/<area>-<desc>`. Submodule pointers often show as modified — **expected, not yours**; detect real dirtiness with `git status --porcelain --ignore-submodules=all`; never stage pointer bumps; never `git submodule update --remote`. +- **Branch** `claude/<area>-<desc>-<issue>` (the issue number makes a pre-PR claim matchable — see the contract's *Claim protocol*). Submodule pointers often show as modified — **expected, not yours**; detect real dirtiness with `git status --porcelain --ignore-submodules=all`; never stage pointer bumps; never `git submodule update --remote`. - **Validate:** before any `docs/` PR, `[ -d docs/node_modules ] || (cd docs && npm ci)` then `cd docs && npm run build` (use `npm --prefix docs …`). Workflows → `actionlint`. Never edit auto-generated files / `*.lock.yml`. - **Agent/review files:** `AGENTS.md` is the **single canonical** instruction file — what humans, agents, and **Copilot code review** all read ([since 2026-06-18](https://github.blog/changelog/2026-06-18-copilot-code-review-agents-md-support-and-ui-improvements/)); there is no separate `.github/copilot-instructions.md` (retired portfolio-wide). Keep `AGENTS.md`, the `.claude/` skills/cards, and any path-scoped `.github/instructions/` files from drifting apart (see `product-engineering` §7). - **Labels** (apply only from this set): `automation`, `documentation`, `ci`, `dependencies`, `submodules`, `bug`, `enhancement`, `question`, `duplicate`, `wontfix`, `needs triage`, `needs investigation`, `performance`, `refactor`, `security`, `repo-assist`, `agentic-workflows`, `good first issue`, `help wanted`, `spam`, `blocked`, `next`. diff --git a/AGENTS.md b/AGENTS.md index 4c120c56..86d91767 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -284,13 +284,22 @@ touch of an unconfirmed repo: number is the one token that cannot be spelled two ways. 3. **Claims expire, timed from the ASSIGNMENT.** A claim carrying no open PR after **~2 hours** is stale and may be taken over, so a crashed or abandoned session never parks an issue permanently. - Measure that window from the issue's **`assigned` timeline event** - (`gh api repos/<o>/<r>/issues/<n>/timeline --jq '.[]|select(.event=="assigned")|.created_at'`), - **never from the branch's commit date** — a claim branch usually points at the base commit, whose - date is far older, so a fresh claim would read as long expired. If an issue is assigned with no - branch, or branched with no assignment, treat it as no claim at all. This time-boxing is what keeps - the rule compatible with *"a bare assignee does not reserve an issue"* above: a claim is a short - lease, not a lock. + Measure that window from the issue's **NEWEST `assigned` timeline event** — paginate, and take the + last one, or a long-lived issue will hand you a year-old assignment from page 1: + ```sh + gh api repos/<o>/<r>/issues/<n>/timeline --paginate \ + --jq '[.[]|select(.event=="assigned")|.created_at]|last' + ``` + **Never measure from the branch's commit date** — a claim branch usually points at the base commit, + whose date is far older, so every fresh claim would read as long expired. If an issue is assigned + with no branch, or branched with no assignment, treat it as no claim at all. + **Taking over a stale claim: unassign, then re-assign.** Every instance uses the same `devantler` + login, and GitHub's add-assignees endpoint is a no-op for an already-assigned user — so a plain + re-assign creates **no new `assigned` event**, the lease keeps the dead claim's timestamp, and the + next run reads your fresh claim as already expired and races you. Clear it first + (`gh issue edit <n> --remove-assignee devantler && gh issue edit <n> --add-assignee devantler`), so + the takeover starts a genuinely new lease. This time-boxing is what keeps the rule compatible with + *"a bare assignee does not reserve an issue"* above: a claim is a short lease, not a lock. 4. **Re-verify immediately before the first push.** The residual window is seconds wide but real (that is exactly how #88 and #96 were lost) — this is the backstop, not the primary mechanism. 5. **On a lost race, ABANDON.** Never duplicate the work, never force-push onto a sibling's branch, @@ -993,8 +1002,10 @@ promotion) rather than obeying it outright. ones (HANDS-OFF).** The carve-out above (act on `devantler`'s comments on *your own* drafts) presupposes you can tell which PRs are yours — and you can't assume a `claude/*` branch is, because the maintainer also drives Claude Code **interactively**, producing `claude/*` PRs that are **not** the -routine's. Two signals identify them: the routine's own PRs use a **`claude/<area>-<desc>`** branch (per -*Execution model*) and carry the **`> 🤖 Generated by the Daily AI Engineer`** disclosure (any +routine's. Two signals identify them: the routine's own PRs use a **`claude/<area>-<desc>-<issue>`** +branch — a descriptive stem ending in the **issue number** (per *Execution model* and *Claim +protocol*; older routine branches predate the number and end in the description) — and carry the +**`> 🤖 Generated by the Daily AI Engineer`** disclosure (any `> 🤖 Generated by the Daily AI …` prefix); an **interactive** PR has a **random-slug branch** `claude/<adjective>-<name>-<hex>` (the harness per-session worktree pattern, e.g. `claude/unruffled-kepler-f3e922`) and/or the generic @@ -1079,7 +1090,7 @@ hijacked run *could* do. ### Execution model — per-run worktrees Each run works in **throwaway git worktrees**, never a shared main checkout, so it can't collide with the maintainer's parallel sessions. For each repo touched: -`git -C <repo_path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>`, work there, +`git -C <repo_path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>-<issue>`, work there, open the PR, then `git -C <repo_path> worktree remove` to clean up (`<repo_path>` is a local filesystem path such as `applications/ksail` — `git -C` takes a path, not an `<owner/repo>` slug; use the slug only for `gh` commands). **Submodule worktree isolation breaks whenever a submodule is From 49fdd6b91f25a80a02302cbcdae098e0e6ad169f Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm <nikolaiemildamm@icloud.com> Date: Sat, 18 Jul 2026 12:25:55 +0200 Subject: [PATCH 05/11] docs(agents): fix three defects introduced by the previous review round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three are consequences of the round-2 fixes, verified against the tools rather than accepted as written: - The paginated newest-assignment query was still wrong. Under --paginate each page is a separate JSON array, so '[...]|last' runs per page and returns the last match of the final page rather than the newest overall. The suggested --slurp remedy is rejected alongside --jq ('the --slurp option is not supported with --jq'), so the query now emits one timestamp per line and takes the max in the shell. Confirmed against a real issue timeline. - The stale-claim takeover commands were unqualified. Runs work from the monorepo checkout or a submodule worktree, so a bare gh issue edit resolves against that repo and would silently edit whatever issue shares the number. Both commands now pass -R <owner>/<repo>. - The surveyor digest emitted CLAIMED with claim-branch=none, which would have made a bare assignee park an issue as skip reason (e) — precisely what 'a bare assignee does not reserve an issue' forbids. CLAIMED now requires both an assignee and a matching claim branch. Part of devantler-tech/world-at-ruin#102 --- .claude/agents/portfolio-surveyor.md | 3 ++- AGENTS.md | 21 +++++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index 4595516b..883479c5 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -293,7 +293,8 @@ nothing_on_fire: <true|false> # true only if NO CI red on main AND no actionab ### Advance - <repo>: roadmap-ready → #<n> "<title>" (<label>) - <repo>: NO roadmap yet → strategy-review candidate -- <repo> #<n> "<title>" — CLAIMED: assignees=<n>, claim-branch=<name|none>, no open PR (contract *Claim protocol* skip reason (e); the orchestrator times the ~2h lease from the issue's NEWEST `assigned` timeline event — an assignee is an INSTANCE claim, never the maintainer) +- <repo> #<n> "<title>" — CLAIMED: assignees=<n>, claim-branch=<name>, no open PR (contract *Claim protocol* skip reason (e); the orchestrator times the ~2h lease from the issue's NEWEST `assigned` timeline event — an assignee is an INSTANCE claim, never the maintainer) +- Emit `CLAIMED` **only when BOTH an assignee and a matching `claude/*-<issue>` branch exist.** An assignment with no branch is **not** a live claim under the contract, so reporting it as one would make a bare assignee park the issue — the exact behaviour "a bare assignee does not reserve an issue" forbids. Report that case as the ordinary open issue it is (mention `assignees=<n>` if useful), never as skip reason (e). ``` Digest rules: diff --git a/AGENTS.md b/AGENTS.md index 86d91767..5e2611ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -284,11 +284,14 @@ touch of an unconfirmed repo: number is the one token that cannot be spelled two ways. 3. **Claims expire, timed from the ASSIGNMENT.** A claim carrying no open PR after **~2 hours** is stale and may be taken over, so a crashed or abandoned session never parks an issue permanently. - Measure that window from the issue's **NEWEST `assigned` timeline event** — paginate, and take the - last one, or a long-lived issue will hand you a year-old assignment from page 1: + Measure that window from the issue's **NEWEST `assigned` timeline event**, or a long-lived issue + hands you a year-old assignment from page 1. Emit every match as its own line and take the max in + the shell — under `--paginate` each page is a **separate JSON array**, so an aggregate like + `'[…]|last'` runs *per page* and silently returns the last match of the final page, not the newest + overall (and `--slurp`, which would wrap the pages, is rejected alongside `--jq`): ```sh gh api repos/<o>/<r>/issues/<n>/timeline --paginate \ - --jq '[.[]|select(.event=="assigned")|.created_at]|last' + --jq '.[]|select(.event=="assigned")|.created_at' | sort | tail -1 ``` **Never measure from the branch's commit date** — a claim branch usually points at the base commit, whose date is far older, so every fresh claim would read as long expired. If an issue is assigned @@ -296,9 +299,15 @@ touch of an unconfirmed repo: **Taking over a stale claim: unassign, then re-assign.** Every instance uses the same `devantler` login, and GitHub's add-assignees endpoint is a no-op for an already-assigned user — so a plain re-assign creates **no new `assigned` event**, the lease keeps the dead claim's timestamp, and the - next run reads your fresh claim as already expired and races you. Clear it first - (`gh issue edit <n> --remove-assignee devantler && gh issue edit <n> --add-assignee devantler`), so - the takeover starts a genuinely new lease. This time-boxing is what keeps the rule compatible with + next run reads your fresh claim as already expired and races you. Clear it first — and **always + pass `-R <owner>/<repo>`**, because a run works from the monorepo checkout or a submodule worktree + and an unqualified `gh issue edit` resolves against *that* repo, silently editing whatever issue + happens to carry the same number: + ```sh + gh issue edit <n> -R <owner>/<repo> --remove-assignee devantler + gh issue edit <n> -R <owner>/<repo> --add-assignee devantler + ``` + so the takeover starts a genuinely new lease. This time-boxing is what keeps the rule compatible with *"a bare assignee does not reserve an issue"* above: a claim is a short lease, not a lock. 4. **Re-verify immediately before the first push.** The residual window is seconds wide but real (that is exactly how #88 and #96 were lost) — this is the backstop, not the primary mechanism. From ddbf9ac7436b1e6406058fde36799e04edb2ffd3 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm <nikolaiemildamm@icloud.com> Date: Sat, 18 Jul 2026 12:35:05 +0200 Subject: [PATCH 06/11] docs(agents): close the round-4 claim-protocol edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A bare-number PR-body search matched any body containing the digits (a benchmark count, a date, another repo's issue number), which could hide the oldest actionable issue behind an unrelated PR. Anchored on the #<issue> reference with a concrete gh pr list --search form. - The no-op assignee problem was only handled for stale-claim takeover. It applies equally when an abandoned run left a bare assignment: the next run's self-assign is also a no-op, so its fresh claim inherits the old timestamp and reads as already expired. Remove-then-add now covers both. - The CLAIMED-only-with-a-branch rule sat inside the surveyor's fenced 'exact shape' block, so the surveyor could emit the instruction itself as a digest row. Moved into Digest rules where the other guidance lives. - The worktree template now required an issue number, but the contract still permits hotfixes and trivial obvious fixes with no tracking issue, leaving <issue> undefined. Those keep plain claude/<area>-<desc> — they go straight to a PR, so the PR body is the discoverable signal and no claim window applies. Part of devantler-tech/world-at-ruin#102 --- .claude/agents/portfolio-surveyor.md | 9 +++++++-- AGENTS.md | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index 883479c5..332563f5 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -293,14 +293,19 @@ nothing_on_fire: <true|false> # true only if NO CI red on main AND no actionab ### Advance - <repo>: roadmap-ready → #<n> "<title>" (<label>) - <repo>: NO roadmap yet → strategy-review candidate -- <repo> #<n> "<title>" — CLAIMED: assignees=<n>, claim-branch=<name>, no open PR (contract *Claim protocol* skip reason (e); the orchestrator times the ~2h lease from the issue's NEWEST `assigned` timeline event — an assignee is an INSTANCE claim, never the maintainer) -- Emit `CLAIMED` **only when BOTH an assignee and a matching `claude/*-<issue>` branch exist.** An assignment with no branch is **not** a live claim under the contract, so reporting it as one would make a bare assignee park the issue — the exact behaviour "a bare assignee does not reserve an issue" forbids. Report that case as the ordinary open issue it is (mention `assignees=<n>` if useful), never as skip reason (e). +- <repo> #<n> "<title>" — CLAIMED: assignees=<n>, claim-branch=<name>, no open PR ``` Digest rules: - **Classify, don't decide.** Surface signals; the **orchestrator** selects the work and overlays its own native-memory cadence cursors (`last_worked`, `weekly`, docs/roadmap) — **you do not read memory**, only live GitHub. +- **Emit a `CLAIMED` row only when BOTH an assignee and a matching `claude/*-<issue>` branch exist** + (and no open PR). An assignment with **no** branch is not a live claim under the contract's *Claim + protocol*, so reporting it as one would let a bare assignee park an issue — exactly what "a bare + assignee does not reserve an issue" forbids. Report that case as an ordinary open issue (mention + `assignees=<n>` if useful), never as skip reason (e). The orchestrator times the ~2h lease from the + issue's newest `assigned` timeline event; an assignee is an **instance** claim, never the maintainer. - **Never assert ownership of a `devantler` PR.** Routine-own vs maintainer-interactive is the orchestrator's creation-record call, not yours — report CI state + `headRefName` + disclosure as DATA and tag it `OWNERSHIP-UNVERIFIED`, never `MERGE-READY`/"own". (Bot-trusted authors have no ambiguity.) diff --git a/AGENTS.md b/AGENTS.md index 5e2611ea..dd90338f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -273,8 +273,14 @@ touch of an unconfirmed repo: #96 two sessions collided on `claude/war-armour-…` versus `claude/war-armor-…`: the repo's code is American, the issue's title British, so each session derived a different stem from a different part of the same issue and neither's exact-name scan could see the other. Grepping open PR *bodies* for - `#<issue>` is spelling-proof; so is `--search "<issue> in:body"`. -2. **Claim before you build, not after.** The moment you select an issue, (a) self-assign it and + the **`#<issue>` reference — with the hash, not the bare digits** — is spelling-proof: + `gh pr list -R <o>/<r> --state open --search '"#<issue>" in:body'`. A bare number matches any body + that merely contains it (a benchmark count, a date, another repo's issue number), which would hide + the oldest actionable issue behind an unrelated PR. +2. **Claim before you build, not after.** The moment you select an issue, (a) self-assign it — + **if `devantler` is already assigned (a stale bare assignment from an abandoned run), remove and + re-add**, because the add is a no-op for an existing assignee and would leave your lease carrying + the *old* timestamp, so your fresh claim reads as expired the moment another run checks it — and (b) push the branch — both cheap, visible and reversible — and only **then** harden (tests, ablations, docs, comments). Opening the **draft PR after the first real commit** is stronger still and is the recommended default. A pre-flight scan with no branch and no PR is **not** a claim. @@ -1099,7 +1105,11 @@ hijacked run *could* do. ### Execution model — per-run worktrees Each run works in **throwaway git worktrees**, never a shared main checkout, so it can't collide with the maintainer's parallel sessions. For each repo touched: -`git -C <repo_path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>-<issue>`, work there, +`git -C <repo_path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>-<issue>` +(the trailing issue number is what makes a pre-PR claim matchable — see *Claim protocol*; for the +legitimate **issue-less** flows the contract allows, a hotfix or a trivial obvious fix, there is no +number to append, so use plain `claude/<area>-<desc>` — those go straight to a PR, so the PR body is +the discoverable signal and no claim window applies), work there, open the PR, then `git -C <repo_path> worktree remove` to clean up (`<repo_path>` is a local filesystem path such as `applications/ksail` — `git -C` takes a path, not an `<owner/repo>` slug; use the slug only for `gh` commands). **Submodule worktree isolation breaks whenever a submodule is From 6ab02aee49ff81297e5ae93c380eed4461446b7f Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm <nikolaiemildamm@icloud.com> Date: Sat, 18 Jul 2026 12:47:34 +0200 Subject: [PATCH 07/11] docs(agents): close the residual claim race and three rollout gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sharpest one first: two instances picking the same issue derive the SAME deterministic branch name, so a bare pre-push re-check settles nothing — both see no branch and both conclude they claimed it. That is precisely how #88 (52s) and #96 (135s) were lost. The push now decides it: put a real commit on the claim branch so a losing push cannot be a trivial fast-forward, push without force, then verify the remote tip is your sha. Rejected push or someone else's tip means you lost — stand down rather than force over them. - Takeover reused the deterministic branch name even when the dead claim had pushed commits, so the push would be rejected or would silently build on abandoned work. Take a fresh -2 branch and leave theirs for the cleanup sweep; never force-push over another instance's work. - The surveyor only matched claim branches ending in -<issue>, so legacy claims made before this template — including any in flight during rollout — were invisible. Falls back to a normalised-stem match. - portfolio-maintenance still taught the plain self-assign (no-op for an existing assignee) and still required an issue number with no path for the issue-less hotfix flow the contract permits. Both synced. Part of devantler-tech/world-at-ruin#102 --- .claude/agents/portfolio-surveyor.md | 5 ++++- .claude/skills/portfolio-maintenance/SKILL.md | 8 ++++++-- AGENTS.md | 20 ++++++++++++++++--- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index 332563f5..45e1a559 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -39,7 +39,10 @@ public and private — no per-repo loop needed to enumerate): to live claims and re-opens the duplicate-build race the protocol exists to close. 2b. **Claim branches (one call per repo that has assigned-but-PR-less issues):** `gh api repos/<o>/<r>/branches --paginate --jq '.[].name' | grep '^claude/'` — report any - `claude/*` branch whose name ends in `-<issue>` for an open issue with **no** open PR, as + `claude/*` branch that either ends in `-<issue>` OR whose normalised stem matches an open issue's + title (strip `war-`/area prefixes and hyphens, and normalise `our`→`or` spelling) — legacy claims + predate the issue-number template and would otherwise be invisible during rollout — for an open + issue with **no** open PR, as `CLAIMED <repo>#<issue> (branch, no PR)`. This is the only pre-PR claim signal that exists: before a PR there is no body to grep, so the issue number in the branch name is what makes the claim discoverable. Keep it bounded — skip the call for repos with no assigned-and-PR-less issues. diff --git a/.claude/skills/portfolio-maintenance/SKILL.md b/.claude/skills/portfolio-maintenance/SKILL.md index 13bf5132..1b72c6ac 100644 --- a/.claude/skills/portfolio-maintenance/SKILL.md +++ b/.claude/skills/portfolio-maintenance/SKILL.md @@ -353,7 +353,9 @@ backlog. Use the [`product-engineering`](../product-engineering/SKILL.md) skill; branch, or once that lapses with no PR, you may pick it up regardless of who's assigned (timed from the issue's `assigned` timeline event, never a branch commit date). **Claim before you build:** self-assign + push the branch **with the issue number in its name** the moment - you select, checking open PRs, remote + you select — and if `devantler` is ALREADY assigned (a stale bare assignment from an abandoned run), + **remove then re-add**, since adding an existing assignee is a no-op that would leave your lease + carrying the old timestamp — checking open PRs, remote `claude/*` branches AND assignees by **issue number, never literal branch name**. A live claim (assigned + branched, in-window, no PR) is skip reason **(e)** — the only one that expires by itself. If it **already has an @@ -422,7 +424,9 @@ For each selected product: .claude/scripts/submodule-init.sh <path> # init at the pinned commit + repair + probe (fail-closed) ``` - Then `git -C <path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>-<issue>` and work + Then `git -C <path> worktree add .claude/worktrees/maint-<runid> -b claude/<area>-<desc>-<issue>` + (issue-less hotfixes and trivial obvious fixes keep plain `claude/<area>-<desc>` — they go straight + to a PR, so no claim window applies) and work **in that worktree**. A stray `core.worktree` makes the worktree resolve back into `.git/modules/<name>`, silently collapsing every parallel session into one physical tree — so on any submodule you did **not** initialise through the wrapper (a tree someone else populated), **probe diff --git a/AGENTS.md b/AGENTS.md index dd90338f..11499fd6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -313,10 +313,24 @@ touch of an unconfirmed repo: gh issue edit <n> -R <owner>/<repo> --remove-assignee devantler gh issue edit <n> -R <owner>/<repo> --add-assignee devantler ``` - so the takeover starts a genuinely new lease. This time-boxing is what keeps the rule compatible with + so the takeover starts a genuinely new lease. **If the dead claim left a remote branch carrying + commits**, do not reuse that name: the deterministic name collides, and pushing onto it either gets + rejected or silently builds on abandoned work. Start a fresh branch + (`claude/<area>-<desc>-<issue>-2`) and leave theirs alone — it is another instance's work, and the + branch-cleanup sweep reaps it once it is provably stale. Never force-push over it. This time-boxing is what keeps the rule compatible with *"a bare assignee does not reserve an issue"* above: a claim is a short lease, not a lock. -4. **Re-verify immediately before the first push.** The residual window is seconds wide but real - (that is exactly how #88 and #96 were lost) — this is the backstop, not the primary mechanism. +4. **Re-verify immediately before the first push, and make that push DECIDE the race.** The residual + window is seconds wide but real (that is exactly how #88 and #96 were lost). Two instances picking + the same issue derive the *same* deterministic branch name, so a bare re-check is not enough — + both would see "no branch" and both would then believe they claimed it. Settle it on the push: + - Put a **real commit** on the claim branch (the first substantive change, or an empty + `git commit --allow-empty -m "chore: claim #<issue>"`), never a bare pointer at the base commit — + otherwise both pushes are trivially fast-forwards and neither is refused. + - Push **without force**, then **verify the remote tip is yours**: + `git ls-remote origin claude/<area>-<desc>-<issue>` must return **your** sha. If the push is + rejected, or the tip is someone else's, **you lost the race** — stand down under rule 5 rather + than force-pushing over them. Never `--force`/`--force-with-lease` a claim branch: that is how a + "won" race silently destroys the winner's work. 5. **On a lost race, ABANDON.** Never duplicate the work, never force-push onto a sibling's branch, never open a competing PR. Then **use the loss**: two independent implementations of one spec are a free **differential-testing oracle**. Diff yours against the winner's and post **only findings From fdb3afeb2d0629dabda0f2dde0031f97e34bb07c Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm <nikolaiemildamm@icloud.com> Date: Sat, 18 Jul 2026 12:58:59 +0200 Subject: [PATCH 08/11] docs(agents): claims are the agent account's only; sync race rule to the copies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The substantive one: the surveyor reported an assignee COUNT, so an issue assigned to a human collaborator or Copilot with a leftover claude/*-<issue> branch satisfied the live-claim test. That parks genuinely actionable work behind an unrelated person's assignment and times the lease off the wrong assignment event. It now reports assignee logins and only a devantler assignment can be a claim — which also keeps the original 'respect assignees, do not take work others are assigned to' direction intact rather than repurposing a human's assignment as an agent lease. Three syncs of the previous round's fixes: - the surveyor now also matches takeover branches (-<issue>-2, -3, …), which the last round introduced but left unmatchable; - portfolio-maintenance carries the race-deciding push (real commit, no force, confirm ls-remote shows your sha); - the monorepo card carries the issue-less hotfix branch path. Part of devantler-tech/world-at-ruin#102 --- .claude/agents/portfolio-surveyor.md | 22 ++++++++++++------- .claude/skills/portfolio-maintenance/SKILL.md | 5 ++++- .claude/skills/products/monorepo/SKILL.md | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index 45e1a559..14e26020 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -32,11 +32,15 @@ public and private — no per-repo loop needed to enumerate): 2. **Open issues (org-wide, one call) — include `assignees`, they are a CLAIM signal:** `gh search issues --owner devantler-tech --state open --limit 300 --json number,repository,title,labels,updatedAt,url,assignees` (`gh search issues` returns issues only — not PRs; treat label-less issues as untriaged.) - Report each issue's assignee count. Per the contract's *Claim protocol*, an assignee means **an - instance has claimed this** — never "the maintainer took it", since every instance assigns as - `devantler` — and paired with a claim branch it is a ~2h lease that makes the issue temporarily - skippable (skip reason **(e)**). Without this field the orchestrator selects the oldest issue blind - to live claims and re-opens the duplicate-build race the protocol exists to close. + Report assignee **logins**, not a count. Only a **`devantler`** assignment can be a claim: the + contract's *Claim protocol* lease is specifically the agent account's, and every instance assigns as + `devantler`, so that login means **an instance has claimed this** — never "the maintainer took it". + An issue assigned to **anyone else** (a human collaborator, `Copilot`) is **not** a claim even with a + leftover `claude/*-<issue>` branch present: reporting it as one would park actionable work behind an + unrelated person's assignment and time the lease off the wrong assignment event. Report those as + ordinary open issues, noting the assignee so the orchestrator can respect a human's in-progress work + on its own merits. Without these logins the orchestrator selects the oldest issue blind to live + claims and re-opens the duplicate-build race the protocol exists to close. 2b. **Claim branches (one call per repo that has assigned-but-PR-less issues):** `gh api repos/<o>/<r>/branches --paginate --jq '.[].name' | grep '^claude/'` — report any `claude/*` branch that either ends in `-<issue>` OR whose normalised stem matches an open issue's @@ -303,9 +307,11 @@ Digest rules: - **Classify, don't decide.** Surface signals; the **orchestrator** selects the work and overlays its own native-memory cadence cursors (`last_worked`, `weekly`, docs/roadmap) — **you do not read memory**, only live GitHub. -- **Emit a `CLAIMED` row only when BOTH an assignee and a matching `claude/*-<issue>` branch exist** - (and no open PR). An assignment with **no** branch is not a live claim under the contract's *Claim - protocol*, so reporting it as one would let a bare assignee park an issue — exactly what "a bare +- **Emit a `CLAIMED` row only when BOTH a `devantler` assignment and a matching claim branch exist** + (and no open PR). Match `claude/*-<issue>`, a takeover branch (`claude/*-<issue>-2`, `-3`, …), or a + legacy normalised stem. An assignment to **anyone but `devantler`** is not a claim at all, and a + `devantler` assignment with **no** branch is not a live claim under the contract's *Claim + protocol*, so reporting either as one would let a bare assignee park an issue — exactly what "a bare assignee does not reserve an issue" forbids. Report that case as an ordinary open issue (mention `assignees=<n>` if useful), never as skip reason (e). The orchestrator times the ~2h lease from the issue's newest `assigned` timeline event; an assignee is an **instance** claim, never the maintainer. diff --git a/.claude/skills/portfolio-maintenance/SKILL.md b/.claude/skills/portfolio-maintenance/SKILL.md index 1b72c6ac..4554849c 100644 --- a/.claude/skills/portfolio-maintenance/SKILL.md +++ b/.claude/skills/portfolio-maintenance/SKILL.md @@ -355,7 +355,10 @@ backlog. Use the [`product-engineering`](../product-engineering/SKILL.md) skill; before you build:** self-assign + push the branch **with the issue number in its name** the moment you select — and if `devantler` is ALREADY assigned (a stale bare assignment from an abandoned run), **remove then re-add**, since adding an existing assignee is a no-op that would leave your lease - carrying the old timestamp — checking open PRs, remote + carrying the old timestamp. **The push decides the race:** put a real commit on the claim branch + (never a bare base pointer), push without force, then confirm `git ls-remote` shows YOUR sha — two + instances derive the same branch name, so a rejected push or someone else's tip means you lost; + stand down rather than force over them. Check open PRs, remote `claude/*` branches AND assignees by **issue number, never literal branch name**. A live claim (assigned + branched, in-window, no PR) is skip reason **(e)** — the only one that expires by itself. If it **already has an diff --git a/.claude/skills/products/monorepo/SKILL.md b/.claude/skills/products/monorepo/SKILL.md index 3a5e6c02..a1e1b82a 100644 --- a/.claude/skills/products/monorepo/SKILL.md +++ b/.claude/skills/products/monorepo/SKILL.md @@ -19,7 +19,7 @@ improve discovery, comprehension, adoption, and portfolio positioning through bo material refreshes. Validate with the `docs` build before any PR. ## Repo-specific conventions -- **Branch** `claude/<area>-<desc>-<issue>` (the issue number makes a pre-PR claim matchable — see the contract's *Claim protocol*). Submodule pointers often show as modified — **expected, not yours**; detect real dirtiness with `git status --porcelain --ignore-submodules=all`; never stage pointer bumps; never `git submodule update --remote`. +- **Branch** `claude/<area>-<desc>-<issue>` (the issue number makes a pre-PR claim matchable — see the contract's *Claim protocol*; issue-less hotfixes and trivial obvious fixes keep plain `claude/<area>-<desc>`). Submodule pointers often show as modified — **expected, not yours**; detect real dirtiness with `git status --porcelain --ignore-submodules=all`; never stage pointer bumps; never `git submodule update --remote`. - **Validate:** before any `docs/` PR, `[ -d docs/node_modules ] || (cd docs && npm ci)` then `cd docs && npm run build` (use `npm --prefix docs …`). Workflows → `actionlint`. Never edit auto-generated files / `*.lock.yml`. - **Agent/review files:** `AGENTS.md` is the **single canonical** instruction file — what humans, agents, and **Copilot code review** all read ([since 2026-06-18](https://github.blog/changelog/2026-06-18-copilot-code-review-agents-md-support-and-ui-improvements/)); there is no separate `.github/copilot-instructions.md` (retired portfolio-wide). Keep `AGENTS.md`, the `.claude/` skills/cards, and any path-scoped `.github/instructions/` files from drifting apart (see `product-engineering` §7). - **Labels** (apply only from this set): `automation`, `documentation`, `ci`, `dependencies`, `submodules`, `bug`, `enhancement`, `question`, `duplicate`, `wontfix`, `needs triage`, `needs investigation`, `performance`, `refactor`, `security`, `repo-assist`, `agentic-workflows`, `good first issue`, `help wanted`, `spam`, `blocked`, `next`. From 9ede9067717caca9289a27273e993deb286c7435 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm <nikolaiemildamm@icloud.com> Date: Sat, 18 Jul 2026 13:11:56 +0200 Subject: [PATCH 09/11] docs(agents): only the agent account's assignment is a claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes in the contract the fix already made in the surveyor — leaving them disagreeing would have left the authoritative file wrong: - The live-claim test treated ANY assignee plus a claim branch as an agent lease, so an issue assigned to a human collaborator or Copilot could be parked and then taken over after the window. Only a devantler assignment is a claim now, and someone else's assignment is explicitly protected by the standing 'do not do work others are assigned to' rule. - The lease query took the newest assigned event for anyone, so a later assignment of a different person would restart a window the agent never renewed. Filtered to .assignee.login == devantler, verified against a real timeline. Also finishes the previous round's takeover-branch fix in the surveyor's branch scan, which had only been applied to its digest rules. Part of devantler-tech/world-at-ruin#102 --- .claude/agents/portfolio-surveyor.md | 3 ++- AGENTS.md | 15 ++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index 14e26020..de2bbef5 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -43,7 +43,8 @@ public and private — no per-repo loop needed to enumerate): claims and re-opens the duplicate-build race the protocol exists to close. 2b. **Claim branches (one call per repo that has assigned-but-PR-less issues):** `gh api repos/<o>/<r>/branches --paginate --jq '.[].name' | grep '^claude/'` — report any - `claude/*` branch that either ends in `-<issue>` OR whose normalised stem matches an open issue's + `claude/*` branch that ends in `-<issue>`, ends in a **takeover suffix** (`-<issue>-2`, `-3`, …), + OR whose normalised stem matches an open issue's title (strip `war-`/area prefixes and hyphens, and normalise `our`→`or` spelling) — legacy claims predate the issue-number template and would otherwise be invisible during rollout — for an open issue with **no** open PR, as diff --git a/AGENTS.md b/AGENTS.md index 11499fd6..cd092264 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -229,10 +229,13 @@ governs the issue work that follows.) Two rules enforce that: KSail's own roadmap *feature specs* — part of this queue, NOT maintainer-interactive work**; the interactive-PR HANDS-OFF rule is about random-slug `claude/*` *PRs* (see *Untrusted input*), never about an *issue's* label or its bot author. **A bare - assignee does *not* reserve an issue INDEFINITELY:** an assignment paired with a **pushed branch** - is a *live claim* for ~2 hours (see *Claim protocol* below); with no branch, or once that window - has elapsed with no open PR, you may pick the issue up regardless of who is assigned — a stale - assignment is never work-in-progress. If an issue **already + assignee does *not* reserve an issue INDEFINITELY:** a **`devantler`** assignment paired with a + **pushed claim branch** is a *live claim* for ~2 hours (see *Claim protocol* below); with no branch, + or once that window has elapsed with no open PR, you may pick the issue up — a stale assignment is + never work-in-progress. **Only the agent account's own assignment is a claim.** An issue assigned to + a **human collaborator** (or `Copilot`) is not an agent lease and must never be taken over on this + window: respect it as someone else's work-in-progress per the standing "do not do work others are + assigned to" rule, and pick a different issue. If an issue **already has an open PR**, don't duplicate it: drive an **actionable trusted-author** PR to merge per *Merge policy* (a **routine-owned** draft: drive it to genuine readiness and self-promote per *Autonomy*; another trusted author's draft gets hygiene only — its owner promotes); leave @@ -297,8 +300,10 @@ touch of an unconfirmed repo: overall (and `--slurp`, which would wrap the pages, is rejected alongside `--jq`): ```sh gh api repos/<o>/<r>/issues/<n>/timeline --paginate \ - --jq '.[]|select(.event=="assigned")|.created_at' | sort | tail -1 + --jq '.[]|select(.event=="assigned" and .assignee.login=="devantler")|.created_at' | sort | tail -1 ``` + Filter to **`devantler`**: an issue can carry several assignees, and a later assignment of someone + else would otherwise set your lease clock — restarting a window you never renewed. **Never measure from the branch's commit date** — a claim branch usually points at the base commit, whose date is far older, so every fresh claim would read as long expired. If an issue is assigned with no branch, or branched with no assignment, treat it as no claim at all. From 07a3e98ddf06080ef9293fcf577f4698958c1e2e Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm <nikolaiemildamm@icloud.com> Date: Sat, 18 Jul 2026 14:19:34 +0200 Subject: [PATCH 10/11] docs(agents): a human's assignment is never an agent lease MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third recurrence of the same class, and the one that actually matters: the run-loop copy still said to pick an issue up 'regardless of who's assigned' once a claim lapsed. The contract now says only a devantler assignment is a claim, so the skill contradicted it and would have had an agent take a human collaborator's assigned work after the 2h window — against the standing 'do not do work others are assigned to' direction. Also fixes the digest row it created: the shape said assignees=<n> (a count) while the survey step above requires logins, because only devantler is a claim signal. Now assignee=devantler. Deferred to devantler-tech/monorepo#2250 (already captured there): resolving the claim ref from the issue number so two sessions choosing different <area>-<desc> stems still collide on one ref. Part of devantler-tech/world-at-ruin#102 --- .claude/agents/portfolio-surveyor.md | 2 +- .claude/skills/portfolio-maintenance/SKILL.md | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index d586e70c..513313a7 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -319,7 +319,7 @@ nothing_on_fire: <true|false> # true only if NO CI red on main AND no actionab ### Advance - <repo>: roadmap-ready → #<n> "<title>" (<label>) - <repo>: NO roadmap yet → strategy-review candidate -- <repo> #<n> "<title>" — CLAIMED: assignees=<n>, claim-branch=<name>, no open PR +- <repo> #<n> "<title>" — CLAIMED: assignee=devantler, claim-branch=<name>, no open PR ``` Digest rules: diff --git a/.claude/skills/portfolio-maintenance/SKILL.md b/.claude/skills/portfolio-maintenance/SKILL.md index b3a96726..cb9c626c 100644 --- a/.claude/skills/portfolio-maintenance/SKILL.md +++ b/.claude/skills/portfolio-maintenance/SKILL.md @@ -368,10 +368,13 @@ actionable open issue**, and any new non-trivial find is **filed as an issue fir backlog. Use the [`product-engineering`](../product-engineering/SKILL.md) skill; in order: 7. **Resolve the oldest actionable open issue** *(the default advance action)* — pick the **oldest** open issue that's actually startable; skip one only if it's blocked, too under-specified to begin, or - it already has an open PR. A **bare assignee does *not* reserve** an issue **indefinitely** — an - assignment plus a **pushed branch** is a live claim for ~2h (contract *Claim protocol*), and with no - branch, or once that lapses with no PR, you may pick it up regardless of who's assigned (timed from - the issue's `assigned` timeline event, never a branch commit date). **Claim + it already has an open PR. A **bare `devantler` assignee does *not* reserve** an issue + **indefinitely** — a `devantler` assignment plus a **pushed branch** is a live claim for ~2h + (contract *Claim protocol*), and with no branch, or once that lapses with no PR, you may pick it up + (timed from the issue's newest `devantler` `assigned` timeline event, never a branch commit date). + **Only the agent account's assignment is a claim, and only it expires:** an issue assigned to a + **human collaborator** (or `Copilot`) is someone else's work-in-progress — respect it and pick a + different issue, never take it over on this window. **Claim before you build:** self-assign + push the branch **with the issue number in its name** the moment you select — and if `devantler` is ALREADY assigned (a stale bare assignment from an abandoned run), **remove then re-add**, since adding an existing assignee is a no-op that would leave your lease From 89a6aaebafb7cf9dd491bbb7738636e5f8cf6830 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm <nikolaiemildamm@icloud.com> Date: Sat, 18 Jul 2026 14:20:00 +0200 Subject: [PATCH 11/11] docs(agents): sync the human-assignee rule to product-engineering too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fourth copy carried the same unqualified 'regardless of who's assigned'. Found by grepping every echo site after fixing the other three rather than waiting for another review round to surface it — the same drift class this PR has now hit four times. Part of devantler-tech/world-at-ruin#102 --- .claude/skills/product-engineering/SKILL.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.claude/skills/product-engineering/SKILL.md b/.claude/skills/product-engineering/SKILL.md index f440b93d..47322762 100644 --- a/.claude/skills/product-engineering/SKILL.md +++ b/.claude/skills/product-engineering/SKILL.md @@ -120,10 +120,12 @@ Issues are the unit of work (contract *Issue-driven*) — this is where new work **ship the decision as a draft PR** (he steers there) — never a passive "awaiting-maintainer" note, never the end-of-run report (he rarely reads it), never an **`@devantler` mention** (no notification). Re-verify any "gated" against live state before trusting it (memory goes stale) and name the - blocker in the report. A **bare assignee does *not* reserve** it **indefinitely** — an assignment plus a - **pushed branch** is a live claim for ~2h (contract *Claim protocol*); with no branch, or once that - lapses with no PR, you may take it regardless of who's assigned (timed from the issue's `assigned` - timeline event, never a branch commit date). **Claim the lane before you build** (self-assign + push + blocker in the report. A **bare `devantler` assignee does *not* reserve** it **indefinitely** — a + `devantler` assignment plus a **pushed branch** is a live claim for ~2h (contract *Claim protocol*); + with no branch, or once that lapses with no PR, you may take it (timed from the issue's newest + `devantler` `assigned` timeline event, never a branch commit date). **Only the agent account's + assignment is a claim, and only it expires:** an issue assigned to a **human collaborator** (or + `Copilot`) is someone else's work-in-progress — respect it and pick a different issue. **Claim the lane before you build** (self-assign + push the branch **with the issue number in its name**, then harden), check open PRs / remote `claude/*` branches / assignees by **issue number rather than literal branch name**, and on a lost race **abandon** — then diff your build against the winner's and post only findings you have verified: