diff --git a/.claude/agents/portfolio-surveyor.md b/.claude/agents/portfolio-surveyor.md index 01567180..513313a7 100644 --- a/.claude/agents/portfolio-surveyor.md +++ b/.claude/agents/portfolio-surveyor.md @@ -29,11 +29,30 @@ public and private — no per-repo loop needed to enumerate): 1. **Open PRs (org-wide, one call):** `gh search prs --owner devantler-tech --archived=false --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 --archived=false --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 --archived=false --state open --limit 300 --json number,repository,title,labels,updatedAt,url,assignees` (`--archived=false` keeps archived repos' stale PRs/issues — e.g. `data-product`'s 2025 bot PRs — out of every survey; archived repos are read-only and carry no actionable signal.) (`gh search issues` returns issues only — not PRs; treat label-less issues as untriaged.) + 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/*-` 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///branches --paginate --jq '.[].name' | grep '^claude/'` — report any + `claude/*` branch that ends in `-`, ends in a **takeover suffix** (`--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 + `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 @@ -300,12 +319,21 @@ 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: assignee=devantler, 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 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. - **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/.claude/skills/portfolio-maintenance/SKILL.md b/.claude/skills/portfolio-maintenance/SKILL.md index 0162143b..cb9c626c 100644 --- a/.claude/skills/portfolio-maintenance/SKILL.md +++ b/.claude/skills/portfolio-maintenance/SKILL.md @@ -368,8 +368,23 @@ 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 `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 + 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 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 + @@ -435,7 +450,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>` 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/.claude/skills/product-engineering/SKILL.md b/.claude/skills/product-engineering/SKILL.md index 5b31014a..47322762 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 @@ -118,8 +120,19 @@ 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 `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: + 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 automation-owned dependency PRs to repository automation, other authors' drafts to their owners, diff --git a/.claude/skills/products/monorepo/SKILL.md b/.claude/skills/products/monorepo/SKILL.md index 75ba4272..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>`. 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`. diff --git a/AGENTS.md b/AGENTS.md index c19f28c9..2dec3411 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 @@ -226,8 +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:** 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:** 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 @@ -245,6 +253,110 @@ 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 **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 + 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 + 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. + **Put the issue number IN the claim branch name** — `claude/<area>-<desc>-<issue>` (e.g. + `claude/war-foliage-spatial-hash-109`). Before a PR exists there is no body to grep, so a bare + `claude/<area>-<desc>` 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 **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" 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. + **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 — 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. **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, 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 + 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 **categorically out of scope**. This includes repositories owned by an employer, client, customer, @@ -926,8 +1038,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 @@ -1012,7 +1126,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>`, 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