Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .claude/scripts/board-claim-token-contract.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
#
# Guards monorepo#2265: board-only claims (path-less project-board mutations) must carry a unique,
# greppable per-lane token. Every instance comments as `devantler`, so the disclosure line alone
# cannot tell siblings apart — without `board-claim:<lane>`, two runs racing the same board issue
# cannot recognise each other's claim, and the ~2h lease + reply-to-close rule has nothing durable
# to key on.
#
# Pins four properties across the contract + run-loop + board product card:
# 1. the greppable token template exists;
# 2. the three live lanes are the only admitted values;
# 3. the lease is timed from the comment's created_at (not assignment — there is none);
# 4. close-out is a reply to the claim comment.
#
# Phrases are kept on ONE line so `grep -Fq` cannot go silently red on a soft wrap (same class as
# monorepo#2250 / #2312 / #2349).

set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
constitution="${repo_root}/AGENTS.md"
run_loop="${repo_root}/.claude/skills/portfolio-maintenance/SKILL.md"
board_card="${repo_root}/.claude/skills/products/project-board/SKILL.md"

fail() {
echo "board-claim-token contract: FAIL — $*" >&2
exit 1
}

grep -Fq 'board-claim:<lane>' "${constitution}" ||
fail "constitution does not require the greppable board-claim:<lane> token"
grep -Fq 'exactly `claude`, `codex`, or `cursor`' "${constitution}" ||
fail "constitution does not pin the three live lanes as the only admitted board-claim values"
grep -Fq "timed from the comment's \`created_at\`" "${constitution}" ||
fail "constitution does not time the board-only lease from the claim comment's created_at"
grep -Fq 'Reply to your own claim comment when finished' "${constitution}" ||
fail "constitution does not require reply-to-close on a board-only claim"

grep -Fq 'board-claim:<lane>' "${run_loop}" ||
fail "portfolio-maintenance board Act step does not require board-claim:<lane>"
grep -Fq '`board-claim:claude`' "${run_loop}" ||
fail "run loop has no concrete lane example an instance can copy"
grep -Fq "timed from the comment's \`created_at\`" "${run_loop}" ||
fail "run loop does not time the board-only lease from created_at"
grep -Fq 'reply to your own claim' "${run_loop}" ||
fail "run loop does not require reply-to-close on a board-only claim"

grep -Fq 'board-claim:<lane>' "${board_card}" ||
fail "project-board card Mutation safety does not mention board-claim:<lane>"

echo "board-claim-token contract: all assertions passed"
24 changes: 17 additions & 7 deletions .claude/skills/portfolio-maintenance/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,13 +297,23 @@ submodule. Split its work in two, because only one half is path-less:
check out and no build to validate. Don't let the repo-shaped Act step below cause the board to be
skipped for want of a `<path>`. ⚠️ **But you still need a CLAIM**: with no branch to push, a bare
assignment is not a claim, so two instances can pick the same board issue and mutate the board
concurrently. Before mutating, **comment the claim on the issue** (disclosure line + what you are
about to change) and **re-read the issue immediately before acting** — if a sibling's disclosed
claim is already there, that lane is owned; pick something else. **The claim MUST expire and MUST be
closed out**, or a crashed run blocks the issue forever: treat a disclosed claim as **live for ~2
hours** (matching the branch-claim lease) and **stale after that — take it over and say so in a
reply**. On finishing, **reply to your own claim** stating what changed; an un-replied claim older
than the lease is abandoned, not owned.
concurrently. Before mutating, **comment the claim on the issue** and **re-read the issue
immediately before acting**. The comment shape is load-bearing — every instance still comments as
`devantler`, so the disclosure line alone cannot tell siblings apart (monorepo#2265):
1. **Unique token.** Every board-only claim comment MUST include the greppable token
`` `board-claim:<lane>` `` on its own line, where `<lane>` is exactly `claude`, `codex`, or
`cursor` (the posting instance's lane — never invent a fourth). Example for this skill's local
Claude instance: `` `board-claim:claude` ``. Match **your own** claim by that exact token; a
different lane's token is a sibling's.
2. **Body.** Disclosure line → the `board-claim:<lane>` token → one sentence naming what you are
about to change. No other prose is required.
3. **Lease.** A claim is **live for ~2 hours** timed from the comment's `created_at` (matching the
branch-claim lease) and **stale after that**. An unreplied claim older than the lease is
abandoned, not owned — take it over and say so in a **new** claim comment (do not edit the
stale one). If a sibling's live claim is already there, that lane is owned; pick something else.
4. **Close-out.** On finishing (or abandoning mid-flight), **reply to your own claim** stating
what changed. The reply is what ends the lease early; without it, siblings wait out the full
~2h window.
- **Any accompanying file change** (an `add-to-project` workflow, an agent-definition or card update)
is **ordinary monorepo work and keeps the FULL discipline** — per-run worktree, validate, draft PR.
**Never skip isolation for it:** several instances run concurrently, and editing the shared checkout
Expand Down
4 changes: 4 additions & 0 deletions .claude/skills/products/project-board/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ Until that is solved, backfill is a standing duty, not an exception.

## Mutation safety

- **Claim before you mutate (board-only).** Path-less board work has no branch to push, so the claim
is a comment carrying `` `board-claim:<lane>` `` (`claude` / `codex` / `cursor`) — see the contract
*Claim protocol* rule 6 and the `portfolio-maintenance` project-board Act step (monorepo#2265).
Re-read comments before acting; stand down on a sibling's live unreplied claim; reply to close yours.
- `updateProjectV2Field` with `singleSelectOptions` **replaces the whole option list** — always pass the
existing option **`id`**s or every assignment is destroyed. Verify emoji codepoints after writing
(🫴 Ready is **U+1FAF4**; a wrong codepoint silently rewrites the option name).
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
review-provider-loop-contract: ${{ steps.filter.outputs.review-provider-loop-contract }}
agent-role-delivery-contract: ${{ steps.filter.outputs.agent-role-delivery-contract }}
work-priority-ladder: ${{ steps.filter.outputs.work-priority-ladder }}
board-claim-token: ${{ steps.filter.outputs.board-claim-token }}
portfolio-surveyor: ${{ steps.filter.outputs.portfolio-surveyor }}
product-value: ${{ steps.filter.outputs.product-value }}
agent-telemetry: ${{ steps.filter.outputs.agent-telemetry }}
Expand Down Expand Up @@ -136,6 +137,12 @@ jobs:
- '.claude/skills/portfolio-maintenance/SKILL.md'
- '.claude/scripts/work-priority-ladder.test.sh'
- '.github/workflows/ci.yaml'
board-claim-token:
- 'AGENTS.md'
- '.claude/skills/portfolio-maintenance/SKILL.md'
- '.claude/skills/products/project-board/SKILL.md'
- '.claude/scripts/board-claim-token-contract.test.sh'
- '.github/workflows/ci.yaml'
portfolio-surveyor:
- 'AGENTS.md'
- '.claude/agents/daily-maintainer.md'
Expand Down Expand Up @@ -514,6 +521,21 @@ jobs:
- name: Verify the portfolio surveyor contract
run: bash .claude/scripts/portfolio-surveyor.test.sh

test-board-claim-token-contract:
name: Test board-claim token contract
needs: changes
if: needs.changes.outputs.board-claim-token == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Verify the board-claim token contract
run: bash .claude/scripts/board-claim-token-contract.test.sh

status:
name: "CI - Required Checks"
runs-on: ubuntu-latest
Expand All @@ -535,6 +557,7 @@ jobs:
- test-review-provider-loop-contract
- test-agent-role-delivery-contract
- test-work-priority-ladder
- test-board-claim-token-contract
- test-portfolio-surveyor-contract
- test-product-value-contract
permissions: {}
Expand All @@ -559,5 +582,6 @@ jobs:
${{ needs.test-review-provider-loop-contract.result }}
${{ needs.test-agent-role-delivery-contract.result }}
${{ needs.test-work-priority-ladder.result }}
${{ needs.test-board-claim-token-contract.result }}
${{ needs.test-portfolio-surveyor-contract.result }}
${{ needs.test-product-value-contract.result }}
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,16 @@ touch of an unconfirmed repo:
**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).

6. **Board-only claims need a unique token — assignment+branch cannot cover them.** Path-less
project-board mutations have no branch to push, so the claim is a **comment**. Every instance
still comments as `devantler`, and the disclosure line alone cannot tell siblings apart
(monorepo#2265). Every board-only claim comment MUST carry the greppable token
`` `board-claim:<lane>` `` where `<lane>` is exactly `claude`, `codex`, or `cursor`. An instance
matches **its own** claim by that token; a different lane's unreplied claim younger than ~2h
(timed from the comment's `created_at`) is a live sibling claim — stand down. Reply to your own claim comment when finished (or when abandoning); an unreplied claim older than the lease is
abandoned and may be taken over. Full procedure lives in the `portfolio-maintenance` skill's
project-board Act step.

**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
Expand Down
Loading