Repository navigation
Description
Filed by the skills seat (session session_01AeA3nU1B5Q2pgxqxgUrexd) from a maintainer-reported incident (2026-08-19, PM chat with the acting identity/services PM), graded at filing under the own-lane exception.
The incident, as self-diagnosed by the seat involved
A chat-session PM covering two lanes declared the domain:identity queue empty from memory while is:issue state:open label:pm:queue label:domain:identity returned "很多". Its own post-mortem (accurate — this seat re-verified every point against current SKILL.md) found five protocol misses in one shift: no backlog enumeration ever run, no pm:queue→pm:dispatched label swaps, eight claim comments written as **Claiming** — … instead of the machine-required Claim: first line, none of the four round-report health metrics computed, and a stand-down where standby was required.
Every one of those rules already exists in the skill text. This is not a rule gap; per the standing principle it is an enforcement gap: 执行缺失 ⇒ 机器.
The machinery already exists too — and never fired
scripts/pm/check-half-states.mjs (report-only, exit-0 by design) already carries the exact predicates for the states this incident produced:
- H2 — assignee set, no comment with a
Claim:-leading line: catches all eight malformed claims. - H3 / H8 — label-pair halves for the missing swaps.
Its header names its consumer: "a PM seat's patrol round (the standby posture in SKILL.md documents the invocation)". Both lanes' seats are vacant (活性巡查降级 2026-08-18), and the acting chat-session PM never ran it. So the sweeper watched nothing. This is the same failure family as the domain-without-pm-state incident already in flight (age-alarm detector being added to this same script): a healing mechanism with no scheduled caller heals only in the counterfactual. An alarm added to a script nobody runs is still silence.
Prior art constraining the fix: the closed finding that the live sweep cannot run inside a PM session container (direct api.github.com 403 with or without the token) — which is precisely why "some seat should run it" keeps not happening.
Direction (graded at filing: pm:queue, M)
Give the sweeper a standing caller that does not depend on any seat's discipline or container network:
- A scheduled GitHub Actions workflow (cron, a few times a day) that runs the live sweep with the workflow's
GITHUB_TOKENand lands the result where seats already look — updating a single pinned anchor issue (edit-in-place, edit history as archive; ⛔ not a new comment per run) with the violation rows and a run timestamp. Report-only stays report-only: the workflow never fails the build. - Rows should carry the age-alarm loudness once the in-flight detector lands.
- ⛔ Not in scope: turning any H-predicate into a blocking gate (the script header's own argument), or teaching the workflow to fix states.
Serial note for dispatch: the in-flight half-state card edits scripts/pm/check-half-states.mjs; this card's surface is expected to be .github/workflows/** + possibly a tiny --format addition to the script — dispatch after that PR lands, and re-derive the surface then.
Dedup: searched standing-caller/patrol/sweep phrasings — the governed-diff round-report-slot card covers a different report-only guard's missing caller (the audit script); no open card covers this one. If the eventual shape wants to solve both with one workflow, that is a family-dispatch decision at dispatch time, not a reason to merge the cards now.
Activity
os-warren commentedon Aug 19, 2026
Claim: PM loop round 3
Session:
session_01AeA3nU1B5Q2pgxqxgUrexdBranch:
claude/issue-9844-half-state-patrol-callerWorktree:
objectstack-issue-9844Domain:
domain:skillsFile surface:
.github/workflows/(one new scheduled workflow) +scripts/pm/check-half-states.mjs(only if a small output-format/anchor-update mode is needed) + the pinned anchor issue it maintains (stop on breach; explain in the report)Container & model: M,
mode:subagent,model: opus— this dispatch'snode scripts/pm/dispatch-gates.mjs --tierrun over the surface: "no path-derived mandate … tier stays the PM's per-card judgment call"; PM call = opusClause-②: no
Serial constraints cleared: the card's stated serial (the in-flight half-state PR) is discharged — #9842 merged as
812be6ef6, H13 and the unscoped listing are onorigin/main; surface re-derived per the card's own instruction. No open PR touches check-half-states.mjs or adds workflows (open-PR list re-read this round).Generated by Claude Code
feat(pm): give check-half-states a standing caller — scheduled patrol…
os-warren commentedon Aug 19, 2026
os-dev-report
{ "issue": 9844, "status": "done", "branch": "claude/issue-9844-half-state-patrol-caller", "pr": "https://github.com/objectstack-ai/objectstack/pull/9862", "premise_still_valid": true, "summary": "Gave check-half-states a standing caller: .github/workflows/half-state-patrol.yml runs the live sweep four times a day (cron '37 1,7,13,19 * * *', minute offset so the patrol never reads the board mid-heal against the hourly triage Routine) with the workflow's own GITHUB_TOKEN, and lands the result by rewriting pinned anchor issue #9857 in place — never a comment per run. The sweeper gained --format=markdown and --provenance=; the anchor body is rendered inside the script where --self-test pins it, and loud H13 P0-SUSPECT rows sort above the fold and out of truncation's reach. Report-only held: no H-predicate became a gate, no label is ever written, findings never fail the run; the job fails only when the sweep could not RUN or its report could not be delivered, and the anchor is updated with the did-not-run report BEFORE the job goes red. The card's premise was confirmed rather than assumed — this PR's own pull_request run executed the live sweep on a real runner and printed 'check-half-states exited 0'.", "tests": "All gates run on the final commit 6fc4612e1 (tree clean, pushed). Gate set derived by `node scripts/pm/dispatch-gates.mjs` with NO paths — it read the change set from the merge base itself: '2 path(s) vs merge base 55d29935b', 113 families discovered, 11 matched. Verdict lines, each quoted from the gate's own output (exit codes captured before any pipe, per `set -o pipefail` / direct redirection): check:pm-half-states -> 'check-half-states self-test: 194 cases pass.' (154 pre-existing + 40 new); check:cross-package-test-inputs -> 'OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; check:node-version -> 'check-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).'; check:partof-closing-keyword -> 'check-partof-closing-keyword self-test: 28 cases pass.'; check:required-contexts -> exit 0; check:shard-attestation -> 'check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).'; check:workflow-status-functions -> 'OK (scanned 26 workflow file(s), 45 job(s), 24 job-level if: expression(s); 9 read needs.*.outputs.*, all naming a status function).'; check-nul-bytes -> 'OK (scanned 6282 text file(s) ... no raw ASCII control bytes).' Workflow YAML: actionlint 1.7.7 exit 0 on the new file AND on all 26 workflows; a python yaml.safe_load structural check; every run: block bash -n parses; the inline github-script body passes node --check. LIVE END-TO-END, the strongest evidence here: this PR's own pull_request-triggered run of the new workflow finished conclusion=success in 23s with 'check-half-states exited 0' (run 32220278666, job 95969338627) — the transport prerequisite IS met on an Actions runner, --format=markdown renders there, and the anchor write correctly skipped on the pull_request event. NO ablation was performed and none is claimed. Two extra harnesses (scratchpad, not committed): plain-output byte parity against a verbatim copy of the pre-change inline printer -> 'identical: true | bytes before/after: 393 393'; and the inline workflow script driven against mocks across all four branches (exit 0 verbatim passthrough, exit 3 prerequisite body, exit 2 unclassified body, and exit 0 + empty report THROWS rather than blanking the anchor) -> 10/10.", "open_questions": [ { "question": "The workflow's inline 'sweep did not run' composition (exit 3 / exit 2 branches) is verified by a scratchpad mock harness but is not pinned by any standing test in the repo — and it is the branch that fires exactly when things are already broken. Should it get one?", "options": [ "A - leave as is: the branch is ~25 lines of string composition, actionlint + node --check cover its syntax, and the mock harness confirmed all four branches before merge", "B - commit the mock harness as a self-test wired into lint.yml, which means a test that parses workflow YAML to extract inline JS (new, fragile surface that breaks on any formatting change to the step)", "C - move the did-not-run composition into check-half-states.mjs behind a flag so the existing --self-test covers it, at the cost of the script rendering a post-mortem of its own failure" ], "recommendation": "A for this PR, with C as the right shape if this branch ever grows. B buys coverage with a test whose failure mode is fragility unrelated to the thing it guards. Non-blocking — flagged for the review, not a reason to hold the card." } ], "out_of_scope_findings": [] }Gates run + verdict lines
Derived with
node scripts/pm/dispatch-gates.mjsand no paths — the script read the change set itself (2 path(s) vs merge base 55d29935b; committed 2, working tree 0, untracked 0). All run on final commit6fc4612e1.$?)pnpm check:pm-half-states✓ check-half-states self-test: 194 cases pass.pnpm check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.pnpm check:node-versioncheck-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).pnpm check:partof-closing-keyword✓ check-partof-closing-keyword self-test: 28 cases pass.pnpm check:required-contextspnpm check:shard-attestation✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).pnpm check:workflow-status-functionscheck-workflow-status-functions: OK (scanned 26 workflow file(s), 45 job(s), 24 job-level if: expression(s); 9 read needs.*.outputs.*, all naming a status function).node scripts/check-nul-bytes.mjscheck-nul-bytes: OK (scanned 6282 text file(s) ... no raw ASCII control bytes).The eleventh entry dispatch-gates listed is
node scripts/pm/check-half-states.mjs [half-state-patrol.yml]— that is the live sweep itself, discovered because the new workflow now names it. It cannot run in this container (see deviations) and it ran green on the runner instead.check-partof-closing-keywordwas additionally run against the PR body text before the PR was opened (PR_BODY="$(cat pr-body.md)" node scripts/check-partof-closing-keyword.mjs→✓ this PR carries no Part-of/closing-keyword contradiction.). The PR usesPart of #9844, not a closing keyword: merging must not close this card while the anchor still needs a human pin and the first scheduled run is unobserved.Deviations
skip-changesetapplied, read back. Workflows +scripts/pm/**publish nothing. The label was written as a union with what the bots had already set, then re-read: PR feat(pm): give check-half-states a standing caller — scheduled patrol workflow + pinned anchor report #9862 now carries["size/l", "skip-changeset"].Check Changesetis green.tracking-labeled, titled "[Half-state patrol] check-half-states live sweep — generated view (please pin)". Number hardcoded in the workflow'sANCHOR_ISSUEenv with a rotation comment. It carriestrackingand deliberately nodomain:*label, so it can never appear as an H13 finding in the sweep it hosts (trackingis already inH13_EXEMPT_LABELS). It still needs a human pin.--probe→ exit 3,PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential,prox… (len 14), 28 requests left on the shared anonymous per-egress-IP quota ([finding]scripts/pm/check-half-states.mjslive sweep cannot run in a PM session container — direct api.github.com is 403 with or without the token, and the docblock's "unauthenticated works" is false here #7412 class 3). Spending that shared quota on a sweep guaranteed to die mid-run was not worth it; the runner proved it instead.pull_requesttrigger added to the workflow beyond the card's letter (cron + dispatch), scoped to the workflow and the sweeper only, with the anchor write skipped on that event. Same posture asengine-split-metric.yml. This is what produced the end-to-end evidence before merge, and it keeps producing it for every future edit to the patrol.files_changed
.github/workflows/half-state-patrol.yml(new, 254 lines)scripts/pm/check-half-states.mjs(+335 / -10):--format=markdown,--provenance=, exportedP0_SUSPECT_MARKER/renderPlain/renderMarkdown/summaryLine/parseOutputOptions/isLoudFinding/normalizeProvenance, header section on the standing caller, 40 new self-test cases. No predicate changed.Total 2 files, +579 / -10.
Generated by Claude Code
Generated by Claude Code
os-warren commentedon Aug 19, 2026
ACCEPT — reviewed against GitHub, not the report. PR: #9862
Swepttimestamp is an explicit heartbeat (a stopped timestamp is the caller's death made visible — the exact defect class this card exists to close); report-only strictly held (findings never fail the run, no labels written) with the one correct exception: the job goes red when the sweep could not run or the report could not land, updating the anchor with the did-not-run body FIRST — "could not read" never reads as "clean".tracking-labeled (insideH13_EXEMPT_LABELS, so the anchor can never appear in its own sweep), rotation procedure documented in-body. Maintainer action item: pin it (pinning needs repo admin; the title asks for it).pull_requestrun executed the live sweep on a real Actions runner — conclusion=success, "check-half-states exited 0" — the transport prerequisite is measured, not assumed. 194 self-test cases green; actionlint over all 26 workflows; byte-parity harness for the plain output.completed: success(checked next).Generated by Claude Code
os-warren commentedon Aug 19, 2026
Close-out (Part-of accounting): delivering PR #9862 MERGED (
be49304) — the card'sPart ofrelationship does not auto-close, so this is the manual close with the paired label write (pm:dispatcheddropped in the same stroke). The scheduled patrol is live: first cron heartbeat lands at 07:37Z today on anchor #9857 (the anchor-PATCH path's first real execution — the one path the PR's own live run could not exercise; the anchor'sSwepttimestamp is the check). The maintainer's pin on #9857 remains the one outstanding nicety. Skills seat, sessionsession_01AeA3nU1B5Q2pgxqxgUrexd.Generated by Claude Code
feat(pm): give check-half-states a standing caller — scheduled patrol…