Copilot review round 2 on #2273: the list endpoint pages at 30, so a large
review round would silently drop later comments — and a workflow whose whole
point is replying to every comment cannot be documented with a command that
returns only the first thirty. --paginate combines with --jq fine (only --slurp
does not), verified against a live review.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 1 on #2273: the text said only a reply in the thread "can
resolve it", which is wrong — resolution is a separate act (the Resolve
conversation button, or the resolveReviewThread GraphQL mutation) and it is the
reviewer's to make. Reading it as satisfied by the documented REST reply is
exactly the wrong conclusion for a PR whose whole point is what a reply does.
State what the reply actually buys: it is the only thing a reviewer reading that
thread sees, and it is what makes resolving the thread defensible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
pr-flow §8's "mirror each round at PR level" was being read as permission to
skip the inline reply, so the rollup got posted and the per-thread replies never
happened. A bullet elsewhere on the page cannot be connected back to the thread
it answers: the thread stays open showing a finding and silence, and by round
three matching bullets to comments is reconstruction rather than reading.
- AGENTS.md, "Responding to Code Reviews": says where the response goes, which
was the ambiguity being resolved the wrong way — inline in the comment's own
thread, with the rollup in addition, never instead.
- pr-flow §8 reorders the two so the primary is unmistakable, and keeps the
reason the mirror exists (threads go outdated once the fix is pushed) as a
rationale for it rather than an alternative to the inline reply.
- Both name the exception: a finding in the "Suppressed comments" block has no
comment id and so no thread to reply into, making the mirror its only home.
- §8 gains the two mechanics, so the easy path is not the only documented one:
the reply endpoint keyed by comment id, and fetching comments per review by
review id, since the unpaginated /reviews listing hides later rounds behind
your own replies.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 2 on #2272: a jq failure on an unexpected shape leaves n
empty, `[ "" -ge 1 ]` then exits non-zero, break never fires, and the loop
sleeps and retries forever — the same unbounded wait as the awk pipe, reached
from the other end. Verified locally: jq exits 5 and the test exits 1.
Propagate the jq failure, guard that n is numeric, and generalize the prose to
say every step which can fail exits rather than retries.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 1 on #2272: piping the count through awk made awk's status
the loop's, so an auth or API failure read as a count of 0 and the job would
retry forever on a poll that can never succeed. The example also used sleep 20
against the 30s remote-API floor the same PR adds to AGENTS.md.
Capture the response first and exit non-zero on a gh failure; jq runs separately
because --slurp cannot be combined with --jq. sleep 30.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
Agents routinely spend turns tight-polling for a condition that was already
going to notify them — ~80 consecutive no-op turns waiting out one local:gate
run on #2250, with a completion notifier for that very process already armed.
The convention existed only in a per-user memory note, which is invisible to
anyone else and cannot be relied on mid-session.
- AGENTS.md gains a "Waiting on long-running work" section: arm a notifier and
stop; one notification is a backgrounded command or a single `until` loop, many
is a Monitor. It states the one genuine exception — state the harness cannot
observe — and says where the poll belongs in that case, inside one backgrounded
loop rather than spread across turns.
- pre-push-gate points at it where the gate is actually run.
- pr-flow §7 points at it where the Copilot review is actually waited on, with
the loop written out. The count-not-existence trap is called out: on round two
the first round's review is still there, so `-ge 1` returns immediately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 2 on #2271: `scripts/smoke-web-*.mjs` overclaimed —
smoke-web-app.mjs and smoke-web-browser.mjs create pages with no viewport at
all, so only the tabs and elicitation smokes back the number. Name those two
explicitly and say the other two set none.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 1 on #2271:
- §5 covers the TUI as well as the web UI, but a TUI capture has no Playwright
viewport and no fullPage mode. §5a is now explicitly the browser path, with a
one-line TUI instruction (size the terminal so nothing wraps) that hands off
to §5b, which is retitled to say it applies to both.
- The claim that every existing shot was taken at 1280x900 was wrong —
specification/screenshots holds 1554x1374 and 3184x1516 among others. 1280x900
is now presented as the standard being adopted, grounded in the web smokes,
with the assorted older sizes named as the problem rather than the precedent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
`/pr-flow` §5 said what to capture and where to put it, but nothing about the
viewport it is captured at — so screenshots clipped their contents, most
visibly the Monitor sidebar, and #2234 needed three full re-captures.
Split §5 into capture / read-back / upload, and add:
- A named capture size, 1280x900 full page — the viewport the web smokes
already use, so shots are comparable across PRs.
- The trap that widening the window does not widen the Monitor sidebar, with
both levers: seeding `inspector.monitor.width` before load, or driving the
ARIA separator with ArrowLeft.
- A no-clipping read-back before uploading.
- The crossfade wait and the focus-marking mechanic, previously unwritten.
The upload mechanics are unchanged, just moved under §5c.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nLLLTGc37hQiC8s3mNyhq
Signed-off-by: cliffhall <cliff@futurescale.com>
`skills:eval` was at 38/46 with several skills measuring 0-33%. The low
scores were mostly bad eval cases rather than bad descriptions, for two
reasons that were nowhere written down:
- The harness runs each case with `--max-turns 1`, so the skill has to be
the model's FIRST tool call. A prompt asking "how does X work here", or
naming a concrete file, sends the model to Read/Grep first — a better
answer to the question, scored as a miss. Every 0% case was one of these.
- `AGENTS.md` is resident on every turn, so a prompt it already answers is
answered without any skill and the case can never pass.
Replace the cases that measured the wrong thing, expand every model-invoked
skill from 3 to 5 positives (negatives kept) so one flake cannot cross the
threshold, and reshape `local-dev`'s description to the action-first form.
Add `docs/skill-authoring.md` with the case shapes that fire, the ones that
cannot, and the probe-then-measure loop.
RUNS=5 npm run skills:eval: 63/63 at or above threshold, 60 at 100%,
all 18 negatives clean.
Closes#2202
Signed-off-by: cliffhall <cliff@futurescale.com>
`disable-model-invocation: true` does not merely make a skill prefer `/name` —
the model cannot invoke it at all, and its description is absent from the skill
listing. So "create a PR for #2163", which is how that work actually starts,
could never reach `pr-flow`. The original rationale ("procedures with side
effects we'd invoke by name anyway") conflates invoking a skill, which only
loads instructions, with performing the procedure it describes.
Flipped `pr-flow`, `pre-push-gate`, `board-ops` and `test-servers` to
model-invocable. `user-invocable` is an independent axis, so this is purely
additive — every `/name` still works. `issue-create`, `issue-triage` and
`release` stay deliberate.
`board-ops` is in the set because `pr-flow` and `issue-create` delegate their
card steps to it: a pointer from a model-invocable skill to a manual-only one is
a dead end for the model.
Costs weighed: the listing share goes 1,199 → 2,256 of 4,000, and a spurious
load costs ~250 characters where a missed one costs a wrong base branch or an
unsigned commit. The rules stay resident in AGENTS.md either way — this improves
the odds of getting the procedure too, it does not become the guarantee.
Eval cases committed for all four, measured at RUNS=3: pre-push-gate, board-ops
and test-servers 5/5; pr-flow 4/5, with "take it through to a pull request" at
100% and the Copilot-review case borderline. Three initial cases were replaced
rather than tuned green: their answers are resident in AGENTS.md, so the model
answering without the skill is correct behaviour, not a miss — the rule this
file already records about what makes a valid trigger case.
AGENTS.md's maintenance rule now says to default to `false` and explains why,
so the advice this change walks back does not get re-applied.
Signed-off-by: cliffhall <cliff@futurescale.com>
- verify-skills-cli: use a local CLI only when it matches the pin EXACTLY. A
floor let a maintainer on a newer CLI validate against a different schema than
CI's, so the same `local:gate` could disagree across machines — the failure a
pin exists to prevent.
- verify-skills-cli: extract `runValidator({probe, spawn, log, error})` and
cover the orchestration — validator accepts, validator rejects, spawn fails,
and the three unusable-probe shapes. On a machine with a working CLI an actual
run only ever walks the happy path.
- pr-flow: stop putting the `gh` token in curl's argv, where any local process
can read it off the process table. Feed the header through `curl --config -`.
- docs/quality-gate: three places had drifted — the CI tier row, the
`local:gate` chain, and the `test:scripts` inventory (which now names all four
suites this PR adds, not two).
Signed-off-by: cliffhall <cliff@futurescale.com>
- verify-skills: version-gate the optional `claude plugin validate`. Probing
only for the executable failed the MANDATORY gate for anyone on a CLI older
than 2.1.233, which has no such subcommand and exits nonzero on it. Skips
below the floor; a failure now means a supported validator rejected the skills.
- skill-eval: match the invoked skill structurally. A substring test counted
`{"skill":"not-testing"}` as a hit for `testing` and inflated the rate.
- skill-eval: `runOnce` → exported `runPrompt` with an injectable `spawnFn`, so
the nonzero-exit branch is actually pinned. Reverting it now fails a test
(verified by mutation) rather than leaving the suite green.
- board-ops: filter the item lookups by repository. Issue numbers are repo-local
and these are org boards — #11 really does carry a `servers` card — so an
unfiltered `select` can move or delete the wrong card, or pass two ids.
- issue-triage: the audit's `own()` filter dropped draft cards, which carry no
`.content.repository`, so the "non-Issue on a board" check reported 0 while
the invariant was being violated. Verified against a fixture.
- pr-flow: cut the branch from the base it will target. The blanket "cut from
v2/main" was wrong for v1, whose history is unrelated.
Signed-off-by: cliffhall <cliff@futurescale.com>
- skill-manifest: the closing `---` fence must occupy a whole line. `---oops`
was accepted as a terminator, silently truncating the frontmatter and passing
validation — the exact failure the guard exists to catch. Regression cases
added (prefix, trailing whitespace, EOF, CRLF).
- skill-eval: reject a nonzero `claude -p` exit instead of resolving it as an
empty observation. An auth error or rate limit was scoring as "no skill
invoked", which passes every negative case and reads as a trigger miss on
every positive one.
- skill-eval: extract `collectSkillInvocations` / `sampleHit` behind a `main()`
guard and cover them in `skill-eval.test.mjs` — the stream parsing and the
process-outcome handling are unreachable from an eval run's happy path.
- docs/test-servers: annotate the era on every catalogue row. Six legacy rows
carried no annotation under a "unless noted, Modern" default, so the doc sent
readers to the wrong era for the feature they were trying to see.
- docs/quality-gate: `validate` runs four guards, not three; add the
`verify:skills` row and the two new `test:scripts` suites; fix the `#publishing`
fragment left dangling by the split.
- issue-create / pr-flow: delegate the card steps to `/board-ops` rather than
restating its option IDs, which are explicitly unstable.
- issue-triage: the audit counted presence, not cardinality — an issue carrying
both `v1` and `v2` passed a check that claims "exactly one". Same for the five
type labels. Also scope the two milestone checks to board #28.
- AGENTS.md: codify the v1 milestone exception. Every milestone is a v2 release
bucket, so a v1 issue has none to take and the Incoming-milestone invariant
cannot apply to board #11.
- clients/launcher/README: its pointer promised an explanation of why the cli/tui
manifests ship, which the extracted guide does not make.
Signed-off-by: cliffhall <cliff@futurescale.com>
Closes#2163
`AGENTS.md` was one 1,223-line contract loaded in full on every turn, whether
the task was a one-line CSS fix or a release cut. This splits it: rules stay
resident, procedures move to `.claude/skills/`.
- Ten skills under `.claude/skills/`, seven `disable-model-invocation: true`
(invoked by name only, so they cost no listing context and have no trigger
question) and three model-invoked with committed eval cases.
- `AGENTS.md` 1,223 → 380 lines: rules, a skills index, and a project tree
reduced to one line per entry. The removed recipes are not duplicated.
- `README.md` 811 → 105 lines, split into `docs/{architecture,quality-gate,
test-servers,publishing,docker}.md`, and dropped from `CLAUDE.md`'s auto-load.
- `.github/copilot-instructions.md` and its mirroring rule removed — Copilot
reads `AGENTS.md`.
- `npm run verify:skills` (in `validate`, so in `local:gate` and CI) parses each
SKILL.md the way Claude Code does, requires an explicit invocation mode and
eval cases for model-invoked skills, and budgets the listing.
- `npm run skills:eval` runs the committed trigger cases headless.
Signed-off-by: cliffhall <cliff@futurescale.com>