Skip to content

Commit 3b56070

Browse files
feat(pm): close-cards --stateless acts on a card with no pm-state, and a half-written close resumes from the board (#19956)
Fixes #19954 Clause-②: no `scripts/pm/close-cards.mjs` is the only file changed. It gains the two capabilities the card asks for, inside the existing one-card-one-dispatch shape. Maintainer, verbatim from the card: 「车队唯一的关单工具 close-cards.mjs 按设计只收带恰好一个 pm:* 状态标签的卡。 这个派卡处理。」 The spelling and the mechanism are the `domain:skills` seat's ruling at dispatch. ## What changes **`--stateless`: a card that never carried a pm-state** - A mode FLAG, never an `--expect-state` value: `--expect-state` still accepts exactly the imported `PM_EXCLUSIVE_STATE_LABELS`, so `--expect-state none` is refused (its refusal now points at `--stateless`). The two flags together are usage exit 2 at the real door; `--stateless=false` is refused rather than read as ON; the default `pm:queue` is CLEARED under the flag rather than left beside it. - Skip matrix: only the state row moves. Under `--stateless` a card carrying ANY pm-state (one or two; `needs-user-decision` included) is skipped, naming them; the assignee, `pm:retriage`, not-open and open-PR rows stand unchanged. An ordinary run's no-state skip now names `--stateless`. - A stateless card packs `comment → issue_patch`: 2 actions, ONE dispatch. **Resume: only the steps the BOARD still owes** - Before anything is sent, every actionable card's thread is read (paged by number, like the timeline). A thread that cannot be read STOPS the run with zero writes on that card (exit 3 / 5), exactly as an unread timeline does. - `priorClosingComment`: is THIS row's closing comment already on the card? The row's text is re-rendered by post-stamped's `renderBody` on the clock of each stamp the stored comment spells (`protocolStamps`), and judged against the stored bytes by post-stamped's `pickRelayComment`. No similarity of this file's own (not `burstKey`, which masks every digit run and would read two different rulings as one comment). - `residualOps`: `PACK_ORDER` minus what the board shows. The comment unless this row's is there (never a second one); `labels_remove` while any label the close strips is still on the card; `issue_patch` always. `packCard` now demands exactly that residue; it still refuses a label plan that ADDS, and under an ordinary run still refuses a strip that does not take the expected state. - Ordinary run: row 4 still requires the state, so the residue can only be the comment: `labels_remove → issue_patch`. Under `--stateless` both can be gone: the measured half-write shape (comment posted, state stripped, the close refused by the throttle) resumes as `issue_patch` alone. - Record lines: `RESUME — this row's closing comment is already on the board: comment ID (posted …, stamped … by its own act) … Still owed: …`; the dry run prints `WOULD CLOSE … (RESUME — comment ID already on the board)`; the summary counts `N RESUMED`. - Step ④ is unchanged in kind: all three effects are read back every time. A resumed comment is read back off the board by id (the `since` window reaches back to it); a strip that was not owed reads as nothing surviving removal. "Nothing landed" (exit 5) is judged on the steps THIS run carried, so a failed resume or stateless pack is not misreported as a new half-write. - The DIRECT transport follows the same residue: post-stamped is not spawned when the comment is on the board, and `label-write` is not called with nothing to strip. - Still ONE card, ONE dispatch, ONE write on `write-pace`: a residual pack is fewer actions, never more writes. `write-pace.mjs`, `check-half-states.mjs` and `package.json` are untouched (grep: `WIRED_WRITE_TOOLS` names the file and nothing pins its usage or header). One refinement the ruling's text does not spell, stated so it can be corrected: under `--stateless` the strip is still owed when a `PM_RESIDUE_LABELS` label that is not a state (today only `pm:blocking`) is on the card; that pack is `comment → labels_remove → issue_patch`. The header already defines the label step as "the pm-state plus its residue"; dropping the residue would leave a claim of work in flight on a closed card. Pinned by its own self-test row. ## Evidence (tree `ab2ee1ee`) ### Self-test `pnpm check:pm-close-cards` → `OK close-cards self-test: 227 cases pass across 20 batteries` (base `f7a3495a`: 173 across 18). Two new pinned batteries (the stateless close, 13; the resume, 27); the CLI floor 14 → 22, the skip-matrix floor 14 → 21, the roster floor 18 → 20. Three existing assertions were re-aimed, because the thread read is a new GET: the call sequence (now `read, timeline, comments, read, comments`); the read-back `since` count (filtered to `since=` calls); and the unreadable-read-back case, whose canned 502 now hits the SECOND comments read through a pass-through `null` in the fake's hook queue, with a new `sent.length === 1` leg proving the dispatch happened. Without the re-aim that case went on passing while it tested the new thread-read stop instead. The dispatch's self-test rows, each a case: stateless close = 2-action pack · half-write resume = 1-action pack · `--stateless` + a pm-state = skipped · two pm-states = skipped (both modes) · an assignee still skips · a dry run prints both packs and sends none · ordinary "comment there, state there" = `labels_remove → issue_patch` · `--stateless` + `--expect-state` = usage 2 (driven through `main`). ### Ablations One-shot, each through `scripts/ablation-replace.mjs` (anchor hit once, blob changed), each restored and proven: blob == HEAD `c66f7a10`, `git diff HEAD` empty. | leg | mutation | self-test | |---|---|---| | A1 | the resume detector never matches | red, 16 cases | | A2 | the pack always carries the comment | red, 12 | | A3 | "nothing landed" counts steps already on the board | red, 2 | | A4 | the stateless skip row removed | red, 26 | | A5 | an unread thread does not stop the run | red, 1 | | A6 | the mode clash not refused | red, 3 | Control after the six legs: 227 pass. Predicted direction for every leg: red; observed: red. ### The live specimen, read-only The resume detector against the stored bytes of the only measured half-write comment (5807380767 on #19334, read over REST; the row text rebuilt by putting `{{NOW}}` back where its stamp stands): found, stamp `2026-09-24T04:02Z`. Control, the same text with one card number changed: not found. ### Acceptance dry runs (read-only, zero writes; the probe comment files were never posted) `node scripts/pm/close-cards.mjs --repo objectstack-ai/objectstack --plan plan.txt --stateless --dry-run` → exit 0: ```text #9180 ① 取现集 `tracking`, `domain:spec` → ② target `tracking`, `domain:spec` · strip none #9180 ③ ONE dispatch, request fw-20260924T065047Z-7aa1d8: comment → issue_patch (2 actions · 352 of 65536 payload bytes) #9180 → WOULD CLOSE `completed` — ⛔ nothing sent #11592 ① 取现集 `tracking`, `domain:spec` → ② target `tracking`, `domain:spec` · strip none #11592 ③ ONE dispatch, request fw-20260924T065048Z-158be8: comment → issue_patch (2 actions · 371 of 65536 payload bytes) #11592 → WOULD CLOSE `completed` — ⛔ nothing sent #15210 ① 取现集 `documentation`, `tracking`, `domain:spec` → ② target `documentation`, `tracking`, `domain:spec` · strip none #15210 ③ ONE dispatch, request fw-20260924T065049Z-838228: comment → issue_patch (2 actions · 343 of 65536 payload bytes) #15210 → WOULD CLOSE `completed` — ⛔ nothing sent close-cards: DRY RUN — nothing was written. 3 read · 3 actionable · 0 skipped · 3 payload(s) packed, 0 sent ``` Control, the same plan without `--stateless` → exit 0, `3 read · 0 actionable · 3 skipped`, each line `SKIP carries no pm-state label, and this run acts only on pm:queue (a card carrying none closes under --stateless)`. `--stateless --expect-state pm:queue` at the real door → exit 2. NOT MEASURED: live resume — reason: the only specimen was closed by hand at 05:22Z. The resume shape is proven by the self-test fixtures and by the read-only specimen probe above. ### Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; change set from merge base `f7a3495a4`: 1 path) → 31 commands, every one run, every one exit 0; `--ran` → `31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED`. The dispatch's named gates are all in that set: `check:pm-close-cards`, `check:pm-write-pace` (113 cases), `check:pm-dispatch-gates` (1925 cases), `check-scripts-symbol-anchors` (and its self-test), `check-self-test-wired` (and its self-test), `check-whole-set-label-write` (and its self-test), `check:nul-bytes`. Lint, narrowed and proven: `eslint --no-inline-config --format json scripts/pm/close-cards.mjs` → 1 file, 0 errors, 0 warnings. The file is in eslint's population (`--print-config` resolves 2 enabled rules for it, not an ignore), and no type-aware linting is configured (`parserOptions.project` is null), so this one-file diff cannot move any untouched file's verdict. Declared to CI: the path-scheduled `Test Core` job and the families the derivation prints outside its 31. ## Acceptance notes - `.claude/skills/pm-dispatch/references/platform-readings.md` still reads `close-cards` 每步一次 dispatch, stale since the one-dispatch-per-card landing. Not touched here: a governed surface outside this file surface. Carrier: none named. - The comment above `.github/workflows/lint.yml`'s `Card-closure helper self-test` step lists what the self-test instruments and does not mention the stateless mode or the resume: incomplete, not wrong. Carrier: none named. - First use on #9180 · #11592 · #15210 is the seat's, after landing. This PR sent nothing to any card. - No changeset: `scripts/pm/**` publishes nothing (`skip-changeset`). ## 维护者速读(草稿) **改了什么:** 车队唯一的关单工具 `close-cards.mjs` 多了两种能力。① `--stateless` 模式:专门处理「从来没挂过 pm:* 状态标签」的记录卡(例如 tracking 卡),一张卡仍是一次 dispatch,只发「评论 → 关卡」两步。② 半写续接:动手前先读卡的评论线,如果这一行的关卡评论已经在板上(上次关到一半),就只补发还欠的步骤,永不再发第二条关卡评论。 **为什么改:** 本次召集里有五张维护者已批准关闭的卡,工具处理不了,只能手工补:#4606、#19334 已由维护者手工处理;#9180、#11592、#15210 还欠着。原因是工具按设计只收恰好带一个 pm:* 状态的卡,而且一次关卡半途失败后无法续接。 **风险与代价(含回滚):** 只改一个内部工具脚本,不发布任何包。新模式必须显式传 `--stateless` 才生效,默认行为不变;普通模式唯一新增的是先读评论线,读不到就停手、零写。自检 227 例全绿,6 次消融均按预期转红。回滚 = revert 本 PR。 **席位意见:** **你要做的:** 无需操作。落地后由 skills 席用新工具处理 #9180、#11592、#15210 三张卡,并在卡上注明。 --- _Generated by [Claude Code](https://claude.ai/code/session_01A22sUB3mUWs6M36VgfijBq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f5a7250 commit 3b56070

1 file changed

Lines changed: 518 additions & 116 deletions

File tree

0 commit comments

Comments
 (0)