From f1f4f7f16cbdc96f02cd2652ba1c5b880cbf37cc Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Wed, 16 Sep 2026 21:11:31 +0700 Subject: [PATCH 1/3] feat(spec): mint spec numbers against open PR heads too Three parallel workers this week each got the same SPEC number because spec-next only scanned docs/specs/, local branches, and commit subjects; none of those surfaces show a number an unmerged open PR already holds, and none of the workers called reserve. Fold in a gh-backed scan of every open PR head's docs/specs/ listing via the contents API (no clone, no fetch). Falls back to the old local-only scan with a stderr note when gh is missing, unauthenticated, or any bootstrap call fails; skippable via SPEC_NEXT_NO_PR_SCAN=1 for hermetic tests. --- _meta/BACKLOG.md | 2 + docs/FEATURES.md | 6 +- .../spec-next-open-pr-heads.md | 27 +++++ lib/spec/spec-next.sh | 41 ++++++- tests/test-spec-next-pr-scan.sh | 100 ++++++++++++++++++ 5 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 docs/implementation-notes/spec-next-open-pr-heads.md create mode 100755 tests/test-spec-next-pr-scan.sh diff --git a/_meta/BACKLOG.md b/_meta/BACKLOG.md index 57986b8d..a92fb2b7 100644 --- a/_meta/BACKLOG.md +++ b/_meta/BACKLOG.md @@ -35,6 +35,8 @@ Lane = the WORKFLOW.md risk tier (`tiny` / `normal` / `full`). | ID | Title | Source | Target artifact | Lane | Status | |----|-------|--------|-----------------|------|--------| +| ID-904 | spec-next mints against open PR heads, not only the local listing #spec #concurrency | Intent: three parallel workers on 2026-09-16 each got SPEC-289 from spec-next because it only scanned docs/specs/, local branches, and commit subjects, none of which show a number an open unmerged PR already holds, and none of them called reserve. Added a gh-backed scan of every open PR head's docs/specs/ listing (contents API, no clone), skippable via SPEC_NEXT_NO_PR_SCAN=1, degrading to the old local-only scan with a stderr note when gh is missing or unauthenticated. lane=normal. Source: wrap step 7b, 2026-09-16 kit sweep session. | queued | +| ID-903 | session observe tools: an --errors flag that groups a tool's error results by message prefix #observe #session #u-mid | Intent: session observe tools shows a tool's error COUNT (EnterWorktree 15 percent, ExitWorktree 17 percent this week) and nothing about WHY; a 30-line python over ~/.claude/projects grouping is_error tool_result content by its first 160 chars answered it in one run (17 of 19 EnterWorktree errors were subagents with a cwd override). Precedent: lib/session/observe/bin/session-observe owns the tools view; this is a flag on it, not a sibling script. lane=full. Goal draft: .claude/goals/observe-tool-errors.md. Source: wrap step 7b, 2026-09-16 kit sweep session. | queued | | ID-885 | dispatch Step 6 releases a settled task's attempt record, and the lane parsers read a markdown-bold `Lane:` header #kit #dispatch #gate | Source: this session's follow-ups from SPEC-290 and SPEC-289. SPEC-290 wired most attempt-state verbs into commands/dispatch.md but never `release`, so kit-attempts/ grew forever. A worker writing `**Lane**: full` had its push BLOCKED with "Spec has no 'Lane:' header"; three files parsed `^Lane:` with the same expression, so all three had to change together. | commands/dispatch.md, hooks/ship-gate.sh, commands/ship.md, commands/mega.md, commands/spec.md, tests/test-ship-gate-fail-closed.sh, tests/test-meta.sh | full | shipped feat/attempt-release-lane-header [SPEC-293, proof docs/verification/attempt-release-lane-header.md] | | ID-883 | Blocking quality gates opt-out per project: `[gate]` block in kit.toml, one reader in lib/gate/gate-policy.sh, hooks call it #gate #config #adopt | Han 2026-09-16: members adopting the kit did not want the proof-of-done gate; adopt always wrote the marker, so for adopters the gate was mandatory. Keys: proof_of_done, lane_gates, understanding_gate, commit_format; a project .kit.toml or the operator overlay sets one to false and the hook passes from the next fire, logging OFF-BY-CONFIG. Safety gates have no key. The classifier treats .kit.toml as inert so the flip itself owes no proof. Proof: docs/verification/gate-opt-out.md. Follow-up the same day (Han: make them default opt-in with the preset in the config file): the four keys default false, adopt seeds the block with resolved values and per-key comments, install prints the how-to; the operator overlay on Han's machines turns them on (dotfiles feat/kit-gates-on). Proof: docs/verification/gate-opt-in.md | lib/gate/gate-policy.sh, hooks/ship-gate.sh, hooks/anti-rationalization.sh, hooks/commit-format.sh, kit.toml [gate], lib/adopt.sh seed, tests/test-gate-opt-out.sh | normal | shipped feat/gate-opt-out | | ID-881 | bin/wrap merge retries transient GitHub failures #wrap #resilience | bin/wrap merge has no retry for a transient GitHub failure (502/503/GraphQL executing-query errors); a real refusal (not mergeable, not green, conflict, draft) must still fail immediately. Operator hand-rolled 3 shell retry loops for ~25 PR merges during a ~90min GitHub outage. Same failure class as memory note hand-rolled-merge-loop-instead-of-wrap-merge.md, a second occurrence, missing retry is the root cause. cmd_merge in lib/wrap/wrap.sh (~line 812, gh pr merge call ~line 917). lane=full. Source: board sweep session 2026-09-13 and 14. Goal draft: .claude/goals/wrap-merge-retry.md in the kit clone. | queued | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 0d51a7a5..eee91c65 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -20,7 +20,7 @@ GENERATED , do not hand-edit. Regenerate: `bash lib/registry/feature-registry.sh | `/kit:design` | `[H/I]` | Opt-in interactive solution-design beat between /think and /spec. Explores 2-3 approaches one question at a time, holds for your approval p… | SPEC-003, SPEC-004, SPEC-005 +105 | test-command-emit-sweep.sh, test-command-triggers.sh, test-design-record.sh +21 | | `/kit:devs-team` | `[H/I]` | Parallel multi-lens critique of a solution design (the active spec if present, else the decision brief). Dispatches 5 engineering lenses, m… | SPEC-016, SPEC-018, SPEC-019 +11 | test-gate-vocab-recording.sh, test-meta.sh, test-outcome-emit-sweep.sh | | `/kit:dispatch` | `[H/I]` | Fire several disjoint VALIDATED specs concurrently, each in its own worktree, then converge. Cross-goal fan-out behind a disjointness gate … | SPEC-002, SPEC-016, SPEC-017 +80 | test-advisor-ledger-emit.sh, test-agent-effectiveness.sh, test-attempt-state.sh +31 | -| `/kit:docs` | `[H/I]` | Update all project documentation to match the current codebase. Cross-references the diff against every doc file and fixes drift. | SPEC-001, SPEC-002, SPEC-003 +194 | proof-loop-09-scenario-b.sh, run-all.sh, run-workflow.sh +69 | +| `/kit:docs` | `[H/I]` | Update all project documentation to match the current codebase. Cross-references the diff against every doc file and fixes drift. | SPEC-001, SPEC-002, SPEC-003 +194 | proof-loop-09-scenario-b.sh, run-all.sh, run-workflow.sh +70 | | `/kit:draft-agent` | `[H/I]` | Meta-agent agent-builder. From a one-line description, generates a new subagent definition OR a mega-goal sub-goal file and (by default) in… | SPEC-089, SPEC-108, SPEC-139 +1 | test-agent-effectiveness.sh, test-command-emit-sweep.sh, test-meta-agent.sh +1 | | `/kit:execute` | `[H/I]` | Autonomous spec execution with verification. Dispatches worker subagents per task, verifies each with task-verifier, retries fixable failur… | SPEC-001, SPEC-003, SPEC-004 +60 | test-break-it.sh, test-gate-vocab-recording.sh, test-hooks.sh +9 | | `/kit:explain` | `[H/I]` | Turn a merged change into a literate-diff explainer a human READS to understand: background -> goal + intuition -> a prose-ordered diff -> … | SPEC-050, SPEC-060, SPEC-094 +18 | proof-loop-09-scenario-b.sh, test-boundary-lint.sh, test-command-emit-sweep.sh +11 | @@ -30,7 +30,7 @@ GENERATED , do not hand-edit. Regenerate: `bash lib/registry/feature-registry.sh | `/kit:grill` | `[H/I]` | Universal intake interview: one type-shaped question at a time, each with a recommended answer, until the task is actually understood. Answ… | SPEC-058, SPEC-059, SPEC-063 +21 | test-config-stamp.sh, test-e2e.sh, test-gate-ledger-plan-record.sh +8 | | `/kit:kit-health` | `[H/I]` | Run a self-assessment of the kit against its own philosophy. Checks file count, hook performance, source citations, and structural health. | SPEC-001, SPEC-002, SPEC-004 +16 | test-command-emit-sweep.sh, test-meta.sh | | `/kit:mega` | `[H/I]` | Turn a multi-objective destination into a sequenced roadmap of dependent sub-goals: decompose, front-load every clarification once, set the… | SPEC-034, SPEC-036, SPEC-088 +35 | proof-loop-09-scenario-b.sh, test-advisor-ledger-emit.sh, test-bin-forwarders.sh +23 | -| `/kit:next` | `[H/I]` | Pick up the next undone task from the spec. Loads context, shows acceptance criteria, lets you drive the implementation. | SPEC-001, SPEC-002, SPEC-003 +87 | run-all.sh, test-advisor-ledger-emit.sh, test-advisor.sh +34 | +| `/kit:next` | `[H/I]` | Pick up the next undone task from the spec. Loads context, shows acceptance criteria, lets you drive the implementation. | SPEC-001, SPEC-002, SPEC-003 +87 | run-all.sh, test-advisor-ledger-emit.sh, test-advisor.sh +35 | | `/kit:onboard` | `[H/I]` | Guided first-run: detect the install mode, offer /kit:adopt for this repo, pick modules, capture the consumer knobs that make them work, di… | SPEC-199, SPEC-213, SPEC-232 +1 | proof-loop-09-scenario-b.sh, test-command-emit-sweep.sh, test-onboard-detect.sh | | `/kit:pitch` | `[H/I]` | Assemble an outward buy-in doc from what a gated run already produced: the spec, the proof-of-done, the implementation-notes, and the gate … | SPEC-140, SPEC-141, SPEC-193 +1 | test-outcome-emit-sweep.sh, test-pitch.sh | | `/kit:prototype` | `[H/I]` | Opt-in throwaway-spike beat beside /kit:design. Builds throwaway code that answers ONE design question: a logic/state model driven by hand … | SPEC-075, SPEC-206, SPEC-207 +2 | test-picture-section.sh | @@ -40,7 +40,7 @@ GENERATED , do not hand-edit. Regenerate: `bash lib/registry/feature-registry.sh | `/kit:review` | `[H/I]` | Paranoid code review. Security, architecture, regressions, missing tests, edge cases. Produces actionable TODOS. | SPEC-001, SPEC-002, SPEC-003 +102 | test-adopt.sh, test-advisor-ledger-emit.sh, test-agent-effectiveness.sh +42 | | `/kit:ship` | `[H/I]` | Ship: review gate, tests, version bump, changelog, conventional commit, docs update, PR. Complete pipeline from done to merged. | SPEC-001, SPEC-002, SPEC-003 +78 | test-adopt.sh, test-board-mirror.sh, test-cheap-guards.sh +28 | | `/kit:spec-validate` | `[H/I]` | Adversarial review of a spec before implementation. 6 specialist lenses attack the spec from different angles (5 advisory, 1 blocking on th… | SPEC-002, SPEC-003, SPEC-004 +58 | test-command-emit-sweep.sh, test-design-record.sh, test-every-step-review.sh +8 | -| `/kit:spec` | `[H/I]` | Generate a development spec from a feature idea or decision brief. Creates docs/specs/ with structured requirements. | SPEC-001, SPEC-002, SPEC-003 +165 | test-bin-forwarders.sh, test-break-it.sh, test-codex-hooks.sh +47 | +| `/kit:spec` | `[H/I]` | Generate a development spec from a feature idea or decision brief. Creates docs/specs/ with structured requirements. | SPEC-001, SPEC-002, SPEC-003 +165 | test-bin-forwarders.sh, test-break-it.sh, test-codex-hooks.sh +48 | | `/kit:start` | `[H/I]` | Detect project state and suggest the right next command. The entry point for any session. | SPEC-002, SPEC-003, SPEC-004 +42 | test-command-emit-sweep.sh, test-config-stamp.sh, test-e2e.sh +30 | | `/kit:test-plan-review-team` | `[H/I]` | Parallel multi-lens adversarial critique of a spec's test plan (the ## Test plan section), with a bounded revise loop that tightens it. Dis… | SPEC-031, SPEC-052, SPEC-062 +10 | test-meta.sh, test-outcome-emit-sweep.sh | | `/kit:test-plan` | `[H/I]` | Derive a test-case coverage matrix from a spec's acceptance criteria before /kit:execute. Writes a `## Test plan` section into the active s… | SPEC-004, SPEC-016, SPEC-018 +27 | test-e2e.sh, test-gate-ledger-plan-record.sh, test-gate-vocab-recording.sh +5 | diff --git a/docs/implementation-notes/spec-next-open-pr-heads.md b/docs/implementation-notes/spec-next-open-pr-heads.md new file mode 100644 index 00000000..a0cead72 --- /dev/null +++ b/docs/implementation-notes/spec-next-open-pr-heads.md @@ -0,0 +1,27 @@ +# Implementation notes: spec-next open-PR-head scan + +No spec doc for this change (ID-904, override recorded via gate-ledger: scoped in +the dispatch prompt, a single additive scan function on an existing script). + +## 2026-09-16 09:00 Bootstrap-failure scope + +Context: the task says "when any call fails, behave exactly as today." A per-PR +`gh api .../contents/docs/specs?ref=` call 404s whenever that PR does not +touch `docs/specs/`, which is the common case, not a failure. + +Decision: only `gh auth status`, `gh repo view`, and `gh pr list` are treated as +bootstrap calls whose failure triggers the local-only fallback + stderr note. A +per-PR contents-API miss is silently skipped (that PR contributes no numbers). + +Why: treating every PR's 404 as "the scan failed" would print a misleading note +on the majority of runs and would still be correct behavior (that PR just holds +no spec numbers), so scoping the failure note to bootstrap calls only avoids +false alarms without losing the fallback contract. + +Alternatives: fail the whole scan on any single PR's API error. Rejected: one +PR without a `docs/specs/` dir would silently blind the scan to every other +open PR's numbers, defeating the point. + +Impact: `lib/spec/spec-next.sh::_scan_pr_numbers`. + +Open questions: none. diff --git a/lib/spec/spec-next.sh b/lib/spec/spec-next.sh index dd8e8078..f50d5f89 100755 --- a/lib/spec/spec-next.sh +++ b/lib/spec/spec-next.sh @@ -18,6 +18,14 @@ # portable mkdir-mutex and records it in a reservations ledger that `_numbers()` folds in, so # a reserved number reads as TAKEN by the very next caller. `next`/`check` are unchanged in # contract: with an empty ledger they behave byte-identically to before. +# +# A second concurrency gap: three workers each opened a PR before any of them merged, so none +# of their local scans (docs/specs/, local branches, commit subjects) saw the others' numbers, +# and none of them called `reserve`. `_numbers()` also folds in every OPEN PR's `docs/specs/` +# listing (fetched via `gh api .../contents/docs/specs?ref=`, no clone, no fetch) when +# `gh` is on PATH and authenticated. Set SPEC_NEXT_NO_PR_SCAN=1 to skip this source (tests use +# it to keep runs hermetic); without `gh`, or if any bootstrap call fails, the scan falls back +# to the original local-only behavior and prints one stderr note. set -euo pipefail ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" @@ -63,6 +71,37 @@ _scan_numbers() { } | grep -oE '[0-9]+' | sort -n | uniq } +# Open-PR SPEC numbers: for each open PR's head ref, list docs/specs/ via the GitHub contents +# API (no clone, no fetch) and pull out SPEC-NNN filenames. Fails soft everywhere: any missing +# tool, failed auth, or failed bootstrap call prints one stderr note and returns empty so +# `_numbers()` degrades to the original local-only scan. +_scan_pr_numbers() { + if [ "${SPEC_NEXT_NO_PR_SCAN:-0}" = "1" ]; then + echo "spec-next: open PR heads not scanned (SPEC_NEXT_NO_PR_SCAN=1)" >&2 + return 0 + fi + command -v gh >/dev/null 2>&1 || { echo "spec-next: open PR heads not scanned (gh not on PATH)" >&2; return 0; } + gh auth status >/dev/null 2>&1 || { echo "spec-next: open PR heads not scanned (gh not authenticated)" >&2; return 0; } + local owner_repo heads + owner_repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)" || owner_repo="" + [ -n "$owner_repo" ] || { echo "spec-next: open PR heads not scanned (gh repo view failed)" >&2; return 0; } + heads="$(gh pr list --state open --json headRefName --limit 100 --jq '.[].headRefName' 2>/dev/null)" + local status=$? + if [ "$status" -ne 0 ]; then + echo "spec-next: open PR heads not scanned (gh pr list failed)" >&2 + return 0 + fi + [ -n "$heads" ] || return 0 + local ref + while IFS= read -r ref; do + [ -n "$ref" ] || continue + # A PR with no docs/specs/ dir 404s here; that is normal (not every PR touches specs), so + # it is skipped silently rather than treated as a bootstrap failure. + gh api "repos/$owner_repo/contents/docs/specs?ref=$ref" --jq '.[].name' 2>/dev/null \ + | grep -oE 'SPEC-[0-9]+' || true + done <<< "$heads" +} + # LIVE reservation numbers for THIS repo: repo-scoped AND within TTL (an expired line stops # counting even before it is physically pruned). Empty when no ledger exists (the common case, # which keeps `next`/`check` byte-identical to the original scan-only behavior). @@ -89,7 +128,7 @@ _reservations() { # The full union readers see: the real scan PLUS live reservations. `_scan_numbers` is the # original scan-only body verbatim; folding reservations in is purely additive. _numbers() { - { _scan_numbers; _reservations; } | sort -n | uniq + { _scan_numbers; _scan_pr_numbers | grep -oE '[0-9]+'; _reservations; } | sort -n | uniq } next() { diff --git a/tests/test-spec-next-pr-scan.sh b/tests/test-spec-next-pr-scan.sh new file mode 100755 index 00000000..02975aff --- /dev/null +++ b/tests/test-spec-next-pr-scan.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# test-spec-next-pr-scan.sh -- spec-next mints against open PR heads, not only the local scan. +# +# Three parallel workers each got SPEC-289 from spec-next because it only scanned docs/specs/, +# local branches, and commit subjects: none of those surfaces show a number an OPEN PR is +# already holding. This proves the added `gh`-backed scan folds an open PR's docs/specs/ +# listing into the max, degrades cleanly without `gh`, and is skippable for hermetic tests via +# SPEC_NEXT_NO_PR_SCAN=1. +# +# Run: bash tests/test-spec-next-pr-scan.sh +# Exit 0 = all pass. Exit 1 = failures. +set -uo pipefail + +KIT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SN="$KIT_DIR/lib/spec/spec-next.sh" +GREEN='\033[0;32m'; RED='\033[0;31m'; NC='\033[0m' +PASS=0; FAIL=0; TOTAL=0 +ok() { TOTAL=$((TOTAL+1)); PASS=$((PASS+1)); echo -e " ${GREEN}PASS${NC} $1"; } +bad() { TOTAL=$((TOTAL+1)); FAIL=$((FAIL+1)); echo -e " ${RED}FAIL${NC} $1"; } +eq() { if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (want '$3' got '$2')"; fi; } +expect() { if { trap '' PIPE; printf '%s' "$3" 2>/dev/null || :; } | grep -q "$2"; then ok "$1"; else bad "$1 (missing '$2' in: $3)"; fi; } + +# A temp git repo whose only local spec is SPEC-0449 (local max, before the PR scan). +mk_repo() { + local r; r="$(mktemp -d "${TMPDIR:-/tmp}/kit-spec-next-pr.XXXXXX")" + git -C "$r" init -q; git -C "$r" config user.email t@t.t; git -C "$r" config user.name t + mkdir -p "$r/docs/specs"; : > "$r/docs/specs/SPEC-0449-x.md" + git -C "$r" add -A; git -C "$r" commit -qm init >/dev/null 2>&1 + printf '%s\n' "$r" +} + +# A stub `gh` on PATH. $1 selects behavior: "ok" (one open PR holding SPEC-0450) or "fail" +# (auth fails, as if `gh` is present but not logged in). +mk_stub_gh() { + local mode="$1" bin; bin="$(mktemp -d "${TMPDIR:-/tmp}/kit-stub-gh.XXXXXX")" + cat > "$bin/gh" < next folds it in ===" +# ============================================================ +R="$(mk_repo)"; STUB="$(mk_stub_gh ok)" +OUT="$(cd "$R" && PATH="$STUB:$PATH" bash "$SN" next 2>/dev/null)" +eq "T1 next is 451 (local max 449, PR head holds 450)" "$OUT" "451" + +# ============================================================ +echo "=== T2: gh present but NOT authenticated -> local-only fallback + stderr note ===" +# ============================================================ +R="$(mk_repo)"; STUB="$(mk_stub_gh fail)" +OUT2="$(cd "$R" && PATH="$STUB:$PATH" bash "$SN" next 2>/dev/null)" +ERR2="$(cd "$R" && PATH="$STUB:$PATH" bash "$SN" next 2>&1 >/dev/null)" +eq "T2 next falls back to local max+1 (450)" "$OUT2" "450" +expect "T2 stderr carries the not-scanned note" "spec-next: open PR heads not scanned" "$ERR2" + +# ============================================================ +echo "=== T3: SPEC_NEXT_NO_PR_SCAN=1 skips the scan even with a working gh stub ===" +# ============================================================ +R="$(mk_repo)"; STUB="$(mk_stub_gh ok)" +OUT3="$(cd "$R" && SPEC_NEXT_NO_PR_SCAN=1 PATH="$STUB:$PATH" bash "$SN" next 2>/dev/null)" +ERR3="$(cd "$R" && SPEC_NEXT_NO_PR_SCAN=1 PATH="$STUB:$PATH" bash "$SN" next 2>&1 >/dev/null)" +eq "T3 opt-out ignores the PR head (local max+1 = 450)" "$OUT3" "450" +expect "T3 stderr names the opt-out reason" "SPEC_NEXT_NO_PR_SCAN=1" "$ERR3" + +# ============================================================ +echo "=== T4: no gh on PATH at all -> local-only fallback + stderr note ===" +# ============================================================ +R="$(mk_repo)" +# Strip PATH down to no `gh`: symlink only `git` (whatever binary it really resolves to, +# alias or not) into an isolated bin dir rather than trusting its parent dir to be gh-free +# (Homebrew installs git and gh side by side under the same prefix). +NOGH_BIN="$(mktemp -d "${TMPDIR:-/tmp}/kit-no-gh.XXXXXX")" +REAL_GIT="$(bash -c 'command -v git' 2>/dev/null)" +[ -n "$REAL_GIT" ] && ln -sf "$REAL_GIT" "$NOGH_BIN/git" +NOGH_PATH="$NOGH_BIN:/usr/bin:/bin:/usr/sbin:/sbin" +OUT4="$(cd "$R" && PATH="$NOGH_PATH" bash "$SN" next 2>/dev/null)" +ERR4="$(cd "$R" && PATH="$NOGH_PATH" bash "$SN" next 2>&1 >/dev/null)" +eq "T4 next falls back to local max+1 (450) with no gh" "$OUT4" "450" +expect "T4 stderr says gh not on PATH" "gh not on PATH" "$ERR4" + +# ============================================================ +echo "" +echo "=== Results ===" +echo -e "Passed: ${GREEN}$PASS${NC} / $TOTAL" +if [ "$FAIL" -gt 0 ]; then echo -e "${RED}$FAIL assertions failed.${NC}"; exit 1; fi +echo -e "${GREEN}spec-next-pr-scan green.${NC}" From 822456696b7746cf67901a3fce34c0c03b86fec9 Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Wed, 16 Sep 2026 21:25:15 +0700 Subject: [PATCH 2/3] docs(verify): proof of done for the spec-next open-PR-head scan --- docs/verification/spec-next-open-pr-heads.md | 75 ++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 docs/verification/spec-next-open-pr-heads.md diff --git a/docs/verification/spec-next-open-pr-heads.md b/docs/verification/spec-next-open-pr-heads.md new file mode 100644 index 00000000..4562db2a --- /dev/null +++ b/docs/verification/spec-next-open-pr-heads.md @@ -0,0 +1,75 @@ +# Proof of done: spec-next mints against open PR heads, not only the local listing + +2026-09-16. Acceptance: `lib/spec/spec-next.sh next` folds an open PR's `docs/specs/` +listing into its max-number scan when `gh` is on PATH and authenticated, via the GitHub +contents API on the PR's head ref (no clone, no fetch). Falls back to the original +local-only scan (docs/specs/, local+remote branches, recent commit subjects) with one +stderr note when `gh` is missing, unauthenticated, or a bootstrap call fails. +Skippable via `SPEC_NEXT_NO_PR_SCAN=1`. No spec doc; override recorded via gate-ledger +(scoped in the dispatch prompt). Board: ID-904. Files: `lib/spec/spec-next.sh`, +`tests/test-spec-next-pr-scan.sh`. + +## The failure this closes + +`spec-next` only scanned `docs/specs/` filenames, local/remote branch names, and recent +commit subjects. None of those surfaces show a number an OPEN, unmerged PR already holds. +On 2026-09-16 three parallel workers each called `next` before any of them merged; two got +SPEC-289, the third had to move to 290, a fourth PR later collided at 291 with a PR holding +288. None of the workers called the existing `reserve` atomic-claim path either, so the +reservation ledger did not help. + +## Green run + +Command: `bash tests/test-spec-next-pr-scan.sh` +Exit: 0 +Output: +``` +=== T1: gh present + authed, one open PR holds SPEC-0450 -> next folds it in === + PASS T1 next is 451 (local max 449, PR head holds 450) +=== T2: gh present but NOT authenticated -> local-only fallback + stderr note === + PASS T2 next falls back to local max+1 (450) + PASS T2 stderr carries the not-scanned note +=== T3: SPEC_NEXT_NO_PR_SCAN=1 skips the scan even with a working gh stub === + PASS T3 opt-out ignores the PR head (local max+1 = 450) + PASS T3 stderr names the opt-out reason +=== T4: no gh on PATH at all -> local-only fallback + stderr note === + PASS T4 next falls back to local max+1 (450) with no gh + PASS T4 stderr says gh not on PATH +Passed: 7 / 7 +spec-next-pr-scan green. +``` +Verdict: PASS. Covers the fold-in case (T1), an authenticated-but-degraded case (T2), the +test-only opt-out (T3), and the no-`gh` case (T4), each checking both the returned number +and the stderr note's presence/absence. + +Command: `bash tests/test-spec-reserve.sh` +Exit: 0 +Output: `Passed: 41 / 41`, `spec-reserve green.` +Verdict: PASS. The existing reservation-race suite is unaffected: it isolates its own +temp repos and sets `SPEC_RESERVE_FILE`, and none of its fixture repos have `gh` reachable +in a way that changes a `next`/`reserve` result (no open PRs against those throwaway repos). + +Command: `bash tests/run-all.sh` +Exit: 0 +Output: `run-all: all 152 suites passed, 0 skipped for missing tooling` +Verdict: PASS. First run failed on `docs/FEATURES.md is fresh (regenerate == committed, +SPEC-219)` because the new test file shifts per-command touched-file counts in that +generated projection; `bash lib/registry/feature-registry.sh generate` produced a +counts-only diff and the suite went green on the next run. + +## Negative control + +Command: `bash lib/gate/negctl.sh "$PWD" "bash tests/test-spec-next-pr-scan.sh" "sed -i '' 's/_scan_pr_numbers | grep -oE .[0-9]+.; /true; /' lib/spec/spec-next.sh"` +Exit (pre-mutation): 0 (green) +Mutation: nulls the `_scan_pr_numbers` fold in `_numbers()` so an open PR's SPEC number +is never counted. +Exit (post-mutation): 1 (RED, as expected: T1 expects 451 and gets 450 with the fold +disabled) +Restore: `git checkout HEAD -- lib/spec/spec-next.sh`, exit 0 (green again) +Verdict: PASS (negctl printed `Verdict: PASS`). + +## Reproducible + +Any operator can re-run `bash tests/test-spec-next-pr-scan.sh` or `bash tests/run-all.sh` +from a checkout of this branch; the new test stubs `gh` on `PATH` so it needs no live +GitHub credentials or network access. From 151027c6caba106a878338e50b0800459549c82c Mon Sep 17 00:00:00 2001 From: Han Ngo Date: Wed, 16 Sep 2026 21:25:53 +0700 Subject: [PATCH 3/3] chore(board): flip ID-904 to shipped (PR #661) --- _meta/BACKLOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_meta/BACKLOG.md b/_meta/BACKLOG.md index a92fb2b7..65392176 100644 --- a/_meta/BACKLOG.md +++ b/_meta/BACKLOG.md @@ -35,7 +35,7 @@ Lane = the WORKFLOW.md risk tier (`tiny` / `normal` / `full`). | ID | Title | Source | Target artifact | Lane | Status | |----|-------|--------|-----------------|------|--------| -| ID-904 | spec-next mints against open PR heads, not only the local listing #spec #concurrency | Intent: three parallel workers on 2026-09-16 each got SPEC-289 from spec-next because it only scanned docs/specs/, local branches, and commit subjects, none of which show a number an open unmerged PR already holds, and none of them called reserve. Added a gh-backed scan of every open PR head's docs/specs/ listing (contents API, no clone), skippable via SPEC_NEXT_NO_PR_SCAN=1, degrading to the old local-only scan with a stderr note when gh is missing or unauthenticated. lane=normal. Source: wrap step 7b, 2026-09-16 kit sweep session. | queued | +| ID-904 | spec-next mints against open PR heads, not only the local listing #spec #concurrency | Intent: three parallel workers on 2026-09-16 each got SPEC-289 from spec-next because it only scanned docs/specs/, local branches, and commit subjects, none of which show a number an open unmerged PR already holds, and none of them called reserve. Added a gh-backed scan of every open PR head's docs/specs/ listing (contents API, no clone), skippable via SPEC_NEXT_NO_PR_SCAN=1, degrading to the old local-only scan with a stderr note when gh is missing or unauthenticated. lane=normal. Source: wrap step 7b, 2026-09-16 kit sweep session. | shipped [PR #661] | | ID-903 | session observe tools: an --errors flag that groups a tool's error results by message prefix #observe #session #u-mid | Intent: session observe tools shows a tool's error COUNT (EnterWorktree 15 percent, ExitWorktree 17 percent this week) and nothing about WHY; a 30-line python over ~/.claude/projects grouping is_error tool_result content by its first 160 chars answered it in one run (17 of 19 EnterWorktree errors were subagents with a cwd override). Precedent: lib/session/observe/bin/session-observe owns the tools view; this is a flag on it, not a sibling script. lane=full. Goal draft: .claude/goals/observe-tool-errors.md. Source: wrap step 7b, 2026-09-16 kit sweep session. | queued | | ID-885 | dispatch Step 6 releases a settled task's attempt record, and the lane parsers read a markdown-bold `Lane:` header #kit #dispatch #gate | Source: this session's follow-ups from SPEC-290 and SPEC-289. SPEC-290 wired most attempt-state verbs into commands/dispatch.md but never `release`, so kit-attempts/ grew forever. A worker writing `**Lane**: full` had its push BLOCKED with "Spec has no 'Lane:' header"; three files parsed `^Lane:` with the same expression, so all three had to change together. | commands/dispatch.md, hooks/ship-gate.sh, commands/ship.md, commands/mega.md, commands/spec.md, tests/test-ship-gate-fail-closed.sh, tests/test-meta.sh | full | shipped feat/attempt-release-lane-header [SPEC-293, proof docs/verification/attempt-release-lane-header.md] | | ID-883 | Blocking quality gates opt-out per project: `[gate]` block in kit.toml, one reader in lib/gate/gate-policy.sh, hooks call it #gate #config #adopt | Han 2026-09-16: members adopting the kit did not want the proof-of-done gate; adopt always wrote the marker, so for adopters the gate was mandatory. Keys: proof_of_done, lane_gates, understanding_gate, commit_format; a project .kit.toml or the operator overlay sets one to false and the hook passes from the next fire, logging OFF-BY-CONFIG. Safety gates have no key. The classifier treats .kit.toml as inert so the flip itself owes no proof. Proof: docs/verification/gate-opt-out.md. Follow-up the same day (Han: make them default opt-in with the preset in the config file): the four keys default false, adopt seeds the block with resolved values and per-key comments, install prints the how-to; the operator overlay on Han's machines turns them on (dotfiles feat/kit-gates-on). Proof: docs/verification/gate-opt-in.md | lib/gate/gate-policy.sh, hooks/ship-gate.sh, hooks/anti-rationalization.sh, hooks/commit-format.sh, kit.toml [gate], lib/adopt.sh seed, tests/test-gate-opt-out.sh | normal | shipped feat/gate-opt-out |