Skip to content

docs(agents): wait only on a PID you recorded — the WAIT side of the shared process table - #17809

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-17104-wait-only-on-a-recorded-pid
Sep 12, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-17104-wait-only-on-a-recorded-pid

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #17104

The shared-process-table rule is written for SIGNALS only. guard-process-kill.sh's ALLOWED
list ends by calling pgrep -f a harmless READ — correct about killing, and exactly where the
card's measured trap sits. A name-matched WAIT has the same root cause (one process table, text
matching, no ownership check) and the same property that makes the kill rule a rule rather than
advice: it reports success. A pgrep -f "$PAT" run from a shell whose own command line
contains $PAT matches the asker itself, head -1 takes it, and the wait never returns.

Two prose edits, no guard logic and no new refusal — per the grading, a wait that never returns
spends only the asker's own budget, so this half is a written rule, not a hook.

Edit 1 — AGENTS.md, Prime Directive #8 (the kill rule's line)

Before (lines 533–534, 89 + 113 = 202 bytes):

   the task is done** (`kill $(lsof -ti tcp:PORT)`). Don't leave orphan servers behind.
   ⛔ One process table per container: **kill only a PID you recorded, never a name** (`guard-process-kill.sh`).

After (lines 533–534, 113 + 116 = 229 bytes):

   the task is done** (`kill $(lsof -ti tcp:PORT)`). ⛔ One process table per container: **kill only a PID you
   recorded, and wait only on one, never a name** — `pgrep -f` matches the asking shell (`guard-process-kill.sh`).

File line count: 1075 before, 1075 after. No ceiling raise.

How the line was paid — and why the dispatch's first route is not available

The dispatch offered three routes and noted the ratchet "pins a widest-table-row byte count, not
prose width". That is true of MAX_TABLE_ROW_BYTES (AGENTS.md 768, headroom 0, untouched here),
but it is not the whole ratchet: check-skill-line-ratchet.mjs also carries a flat
MAX_LINE_BYTES = 120
prose rule, and line 534 measured 113 bytes — 7 bytes of headroom
against a clause that needs ~80. Route (a), riding line 534, is arithmetically impossible.

Route (b) — folding into the paragraph's existing lines — is re-wrapping slack into new content.
os-dev.md forbids that spelling explicitly (「付行数棘轮的唯一合法货币是删内容:⛔ 不拿 re-wrap
(折行合并)当筹行,新增以删减付账」), and the ratchet's own header names widening-to-budget as the
defect it was built against. Flagging rather than silently choosing: the dispatch offers a route
os-dev.md closes, and os-dev.md wins.

So route (c), paid by deletion in the same edit: Don't leave orphan servers behind. (34
bytes including its leading space) is deleted. It loses nothing — it is a negative restatement of
the bolded shut it down yourself when the task is done that immediately precedes it in the
same sentence pair; the rule's positive form, its prescribed teardown command and its scope all
survive untouched. No other content was removed.

Both new lines are within the 120-byte budget (113, 116). The wrap falls inside a bold span,
which is this file's own established convention — lines 532→533 already wrap inside
shut it down yourself when the task is done.

Edit 2 — .claude/hooks/guard-process-kill.sh (header comment only)

Line 60's sentence keeps its meaning about signals and gains the caveat (85 → 91 bytes, plus
an 88-byte continuation line):

#   pgrep -f foo / ps aux | grep node    — a READ. Nothing dies; look all you like. But a
#                                          READ is not a WAIT — see the section below.

A new header section then carries the positive form and the spelling detail the AGENTS.md ceiling
could not hold (cmd & pid=$! then wait "$pid", a pidfile, or a wrapper that prints its own
PID), the measured five-hour self-deadlock, and the reason this half is deliberately unguarded.
.claude/hooks/ carries no ceiling (the ratchet header states this explicitly), so the detail
belongs here — which is also the file's existing division of labour with the kill rule.

File: 333 → 361 lines. New comment lines are ≤94 bytes, matching the file's existing widest
comment line exactly. bash -n clean; hook still wired in .claude/settings.json and still
-rwxr-xr-x.

Verification

Final commit 35a20c6.

Governed verdict — node scripts/pm/check-governed-merges.mjs --test AGENTS.md .claude/hooks/guard-process-kill.sh, exit 3:

governed-surface predicate: 2 of 2 path(s) hit the register (5 surfaces, repo-agnostic).
  ⛔  GOVERNED — a human merge is the review record for this PR (#9495 regime).
      No seat flips it ready, enqueues it, or arms auto-merge (AGENTS.md Prime Directive #14).

Guard self-test — .claude/hooks/guard-process-kill.selftest.sh: 69 passed, 0 failed both
before and after the edit (exit 0). It pins the refusal text, not the header comment, so a
header-only change leaves it green and no pin needed updating.

Ratchet — pnpm check:pm-skill-ratchet, exit 0:

✓ check-skill-line-ratchet: AGENTS.md: widest table row is 768 bytes (pin 768; headroom 0).
✓ check-skill-line-ratchet: AGENTS.md is 1075 lines (ceiling 1075; headroom 0).

Derived gate set — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
derived 18 families from the 2-path change set; all 18 run, all exit 0. Reconciled with --ran:

Run reconciliation — 18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED
  (a DERIVED zero — all 18 recorded an exit code and none of them is 3).

Two gates first returned exit 3 = PREREQUISITE NOT MET (not a finding): node_modules was
absent until pnpm install, and check:doc-formula-expressions additionally needed
@objectstack/formula and @objectstack/lint built. Both re-ran at exit 0 after the builds
(os-verify-lock.sh, VERDICT command-exit 0).

Control characters — grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over both files: no
matches. pnpm check:nul-bytes exit 0.

objectui pin check — read from objectui origin/main:scripts/upstream-port-pin.json.
It pins 11 upstream paths (the guard-main-checkout*, guard-shared-stash* and
guard-tree-enum* families plus check-half-states.mjs, invoked-as.mjs,
check-bash32-floor.mjs). guard-process-kill appears zero times. No downstream pin
moves
, and the objectui parity check has nothing to flag.

changeset: skip-changeset — AGENTS.md (repo-root instruction file) and .claude/** are
both on the fast track; nothing in any package's files[] moves.

维护者速读(草稿)

改了什么 —— 两处纯文字改动。AGENTS.md 第 534 行的「只杀你记录过的 PID,绝不按名字杀」规则,
补上同样正向形式的另一半:等待也只等你记录过的 PID;guard-process-kill.sh 头部注释里那句
「pgrep -f 只是读,随便看」保留它关于「信号」的原意,并补上关于「等待」的告诫。⛔ 没有改任何守卫
逻辑,没有新增拦截。

为什么改 —— 卡片记录的是一次实测事故:三个后台等待进程死锁了五个小时。原因是 pgrep -f "$PAT"
在一个自身命令行就包含 $PAT 的 shell 里执行,匹配到了提问者自己,于是 tail --pid 在等自己的
父进程、父进程在等它。这个错误会报成功 —— pgrep 返回 0,还打印出一个看起来很合理的 PID,所以
没有任何信号提示出错。杀错进程毁的是别人的活、动静很大;等错 PID 静悄悄地烧掉的是你自己的时间,所以
它能跑满五个小时都没人发现。

风险与代价(含回滚) —— 风险极低:改的是说明文字,不是可执行逻辑。bash -n 通过,守卫自检
69 项全绿(改动前后一致),钩子仍然挂载且可执行。代价是 AGENTS.md 行数棘轮在 1075/1075 零余量,
为了不抬上限,删掉了「Don't leave orphan servers behind.」这句话——它只是前面加粗那句「任务做完自己
关掉」的反面重复,规则内容没有任何损失。回滚就是 git revert 这一个 commit,无迁移、无依赖。

席位意见 —— (留空,待 skills 席位定稿)

你要做的 —— 这是受管面(AGENTS.md + .claude/**),按 Prime Directive #14 由维护者手动合并;
⛔ 不翻 ready、不进合并队列、不开 auto-merge。请确认一点:用「删掉那句重复话」来换这一行,是否认可
——若您认为那句话应当保留,替代方案是抬 AGENTS.md 的行数上限(需要您的裁决),我没有自行抬。

Acceptance notes

Out of scope, noted and not filed (neither is a reproducible defect, a declared-contract
violation, nor a metadata trap — the three filing classes):

  • The dispatch's reading that the ratchet "pins a widest-table-row byte count, not prose width"
    is incomplete: a flat MAX_LINE_BYTES = 120 prose rule also applies, and it is what caps line
    534. Reported to the PM rather than filed — it is a fact about this dispatch's premise, already
    corrected above. Carrier: the skills seat, on this PR.
  • The dispatch's "AGENTS.md prose lines wrap at ~100 columns in that section" does not match the
    measurement: Prime Directive Convert to monorepo with scoped packages #8 wraps at 84–90 bytes, and only the ⛔ line ran to 113. Same
    carrier, same PR; no separate card.

Generated by Claude Code

The shared-process-table rule is stated for SIGNALS only, and the guard's
ALLOWED list ends by calling `pgrep -f` a harmless READ. That is correct about
killing and is exactly where the trap sits: a name-matched WAIT has the same
root cause (one process table, text matching, no ownership check) and the same
property that makes the kill rule a rule rather than advice — it reports
success. A `pgrep -f "$PAT"` run from a shell whose own command line contains
"$PAT" matches the asker itself, so the wait never returns.

AGENTS.md's kill rule gains the wait clause in its own positive form. The line
ratchet is at 1075/1075 and the flat 120-byte line budget leaves that line 7
bytes of headroom, so the clause is paid by deleting "Don't leave orphan
servers behind." — a restatement of the bolded "shut it down yourself when the
task is done" that precedes it. No ceiling raise; the file stays at 1075.

The spelling detail the ceiling could not hold (`cmd & pid=$!`, a pidfile, a
wrapper that prints its own PID) lands in the guard header, which carries no
ceiling — header comment only, no guard logic and no new refusal: a wait that
never returns spends only the asker's own budget.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK
@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

改了什么 — 两处受管文字。(1) AGENTS.md 首要指令 #8 里的进程表规则由「只杀自己记录的 PID,永不按名字杀」扩为「只杀自己记录的 PID,也只等自己记录的 PID,永不按名字——pgrep -f 会匹配到发问的 shell 自己」;为了不加行、不动上限,删掉了紧挨着的一句否定式复述「Don't leave orphan servers behind.」(前一句加粗的「任务结束自己关掉」已表达同义),1075/1075 行不变。(2) .claude/hooks/guard-process-kill.sh 头注:原来那句「pgrep -f 只是读,随便看」保留其关于信号的含义,加上「读不等于等」以及一节正文:等待只能用自己记录的 PID(cmd & pid=$!; wait "$pid"、pidfile、或自报 PID 的包装器),并写明为什么这一半不做钩子(按名杀伤及邻居、按名等只烧自己的预算)。不加任何拦截逻辑,自检 69/69 不变。

为什么改 — #17104 实测:一个 dev 用 pgrep -f "<模式>" | head -1 取 PID 再 tail --pid 等待,模式恰在发问 shell 自己的命令行里,于是等的是自己,三个后台等待者死锁 5 小时;规则只写了「杀」这一半,「等」这一半没写,守卫头注还说 pgrep -f 无害。

风险与代价(含回滚) — 纯文字;18 条派生门禁全绿、棘轮 1075/1075 绿、受管判定 GOVERNED 是预期;该钩子不在 objectui 的 pin 清单里,下游无联动。回滚 = revert 本 PR。

席位意见 — 席内按契约复审档复核通过,建议合并。dev 纠正了本席派发词里两处关于棘轮/行宽的错读,已记录。

你要做的 — 受管面:请人工合并 PR #17809(本席不翻 ready、不入队、不挂 auto-merge)。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 12, 2026 09:40
@os-zhuang
os-zhuang enabled auto-merge September 12, 2026 09:40
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit d79f249 Sep 12, 2026
36 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17104-wait-only-on-a-recorded-pid branch September 12, 2026 10:04
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
… own subject or the zero is void (objectstack-ai#17837)

The zero-hit rule bound the control to HIT, not to be SCOPED to the
claim: a control can prove the instrument works while pointed at a
different corpus, path shape, module/package boundary or quoting, and
the zero then comes back as a reassuring negative that survives review
(three misses in one hotcrm shift, two more measured by the triage
seat). The rule now says the control shares the claim's own subject and
the target's failure mode (a phrase that wraps across lines needs a
control that wraps the same way), or the zero is VOID rather than
negative. Landed in all three carriers of the discipline in one PR, per
SKILL.md 「一条规则在本文与核心条款一处改动,另一处同 PR 同改」: `pm-dispatch/SKILL.md`
:161-:162, `references/core-rules.md` :45, and the dev-side reading in
`.claude/agents/os-dev.md` :54. Every file stays at its ratchet ceiling;
the payment is density inside the same section, itemised below. Not a
new gate.

Part of objectstack-ai#17569

## Acceptance notes

**Premises (re-taken on `origin/main` `0cd841a16` at 2026-09-12T10:03Z,
after `git fetch origin main`):**

1. SKILL.md :161 read 「- 零命中必须用确定存在的邻近词反查,否则零命中不成立。」 (78 bytes); file
812 lines, CEILINGS row 812, `MAX_LINE_BYTES = 120` — holds. The ratchet
script lives at `scripts/pm/check-skill-line-ratchet.mjs` (the dispatch
word spelled it without the `pm/` segment; same script).
2. core-rules.md :45 read 「- 零命中必须用确定存在的邻近词反查才成立;仓不可达时 ⛔ 不得当成查过且干净。」
(110 bytes); 151 lines at ceiling 151 — holds; it IS the rule's
core-rules mirror and is edited here.
3. os-dev.md :54 read 「 - 空结果要同会话一个已知必中的控制词答了命中才算读数。」 (83 bytes); 403
lines at ceiling 403 — holds.
4. SKILL.md :175 and core-rules.md :51 are the dedupe-control siblings —
left byte-identical (the new clause is general; no cross-reference
needed).
5. Open-PR scan re-taken at 2026-09-12T10:07Z over all 24 open PRs' file
lists (REST `pulls/{n}/files`): zero hits on the three files. Control,
same corpus and same regex family: 5 of those PRs touch `.claude/` paths
(objectstack-ai#17828 and objectstack-ai#17803 on `platform-readings.md`, objectstack-ai#17823 `rest-channel.md`,
objectstack-ai#17809 a hook, objectstack-ai#17515 `decision-analysis.md`) — the zero is scoped.
`origin/main` moved 3 commits (to `51b024a16`) during the run; none
touched the three files.
6. Sibling clause from PR objectstack-ai#17566 (spec-property-retirement SKILL.md :59)
read: 「零编写实例普查要并跑一个同族已知存活的键作对照,两读数都报;同得零即没测出。」 — the new wording keeps
that vocabulary (a control shares the claim's family/subject; a control
that measures nothing voids the reading) rather than introducing a
second idiom.
7. The card quotes a dispatch-template sentence 「pair every zero with a
control word that must hit」 as a second carrier inside SKILL.md. On
`origin/main` no such sentence exists (grep for `must hit`, `pair every
zero`, `control word` over `.claude/**` and `scripts/pm/**` returns
zero; control: `零命中` hits :161, :542, :724) — the template left in the
rules-only rewrite. No second carrier to edit.

**Edited lines and byte counts (each ≤ 120):**

| file | line | bytes | text |
|:--|:--|--:|:--|
| SKILL.md | :161 | 113 | 「- 零命中须用确定存在的邻近词反查;控制词须与主张同主体,否则该零作废,不是阴性。」 |
| SKILL.md | :162 (new) | 114 | 「- 同主体 =
同语料、同路径形、同包界、同引法、同失效形态:跨行短语配跨行控制词。」 |
| SKILL.md | :165 (merged) | 105 | 「- 时间戳形如 `YYYY-MM-DDThh:mmZ`,树读数另带
ref 或 tip;无时间戳的读数按未取处理。」 |
| core-rules.md | :44 | 112 | 「- 核验 main 用 fetch 后的 `origin/main`,⛔
不用共享检出树;仓不可达 ⛔ 不当查过且干净。」 |
| core-rules.md | :45 | 113 | 「-
零命中须用必中词反查,且与主张同语料/路径形/包界/引法/失效形态,否则该零作废。」 |
| os-dev.md | :54 | 113 | 「   - 空结果要同会话已知必中、与主张同主体同失效形态的控制词答了命中才算读数。」 |

**How density was paid (all inside the 平台读数纪律 section of each file):**

- SKILL.md 812/812: the clause takes two lines (:161-:162, +1); the two
timestamp lines (:164 「时间戳形如 …,树读数另带 ref 或 tip。」 and :165
「无时间戳的读数是格式错误不是现值,读者按未取处理。」) merged into one (−1). Dropped words:
「是格式错误不是现值,读者」 — the operative half 「按未取处理」 survives, matching
core-rules' own wording of that rule.
- core-rules.md 151/151: no two neighbouring lines in the section merge
under 120 bytes, so the clause is one line (:45) with the five sames
inline; the 仓不可达 half of the old :45 moved onto the `origin/main` line
(:44). Dropped words: 「确定存在的邻近」 → 「必中」, 「时」, 「不得当成」 → 「不当」, 「的工作树」 →
「树」, 「先」. The wrapped-phrase example is carried by SKILL.md only (terser
register).
- os-dev.md 403/403: one line, byte-for-byte replacement of :54 (83 →
113 bytes); the 范围 list is otherwise untouched.

**Frame block pin:** `sed -n '734,755p'
.claude/skills/pm-dispatch/SKILL.md | md5sum` =
`3327d02c56f8a0eca88569dad2270f32` before and after (net line count
above it is 0, so the block did not move).

**Gates (derived with `node scripts/pm/dispatch-gates.mjs --commands` in
the worktree, 18 commands; reconciliation via `--ran`: 「18 derived, 18
run, 0 NOT-MEASURED, 0 UNRUN」):** all 18 exit 0 at head `9e15e315f`.
`pnpm check:pm-skill-ratchet` verdict lines: 「SKILL.md is 812 lines
(ceiling 812; headroom 0)」 · 「core-rules.md is 151 lines (ceiling 151;
headroom 0)」 · 「os-dev.md is 403 lines (ceiling 403; headroom 0)」. `pnpm
check:skill-frame-sync`: 「the one declared copy of the decision frame is
internally coherent … 4 axes」. `pnpm check:nul-bytes`: 「OK (scanned 8465
text file(s) …; no raw ASCII control bytes)」.
`check:doc-formula-expressions` first answered exit 3 PREREQUISITE NOT
MET (`@objectstack/formula` and `@objectstack/lint` unbuilt) — not a
measurement; built both under the verify lock (VERDICT command-exit 0,
169 s) and re-ran: exit 0.

**Changeset:** `.claude/**` publishes nothing from any released package
(`node scripts/check-changeset-fixed.mjs` exit 0; no `files[]` of any
package covers `.claude/`) — `skip-changeset` applied by additive POST
and read back.

**Deferred rider (⛔ not in this PR):** the card's two
`references/platform-readings.md` rows (`git grep -- 'a/**/*.ext'` skips
files directly under `a/`; `grep -c $'\x00'` is not a NUL probe) wait on
that file's serial behind PR objectstack-ai#17828 and PR objectstack-ai#17803. objectstack-ai#17569 stays open for
the rider PR; hence `Part of`, not a closing keyword.

**Governed surface:** `.claude/**` — this PR stays a draft at the human
terminal; nothing here flips it ready or arms auto-merge.

## 维护者速读(草稿)

**改了什么:** 「零命中必须配控制词反查」这条规则,在三处载体(pm-dispatch SKILL.md、核心条款
core-rules.md、os-dev 开发 agent 定义)各加一句:控制词必须与主张同主体 ——
同语料、同路径形、同包界、同引法、同失效形态 —— 否则这个零作废,不算阴性读数。三个文件行数不变,各自仍顶着 ratchet 上限。

**为什么改:**
一个班次里三次「控制词命中了、零仍是假的」:控制词证明工具能用,却没证明工具对准了主张。旧文只要求控制词「命中」,不要求它「与主张同一件事」,于是错误读数反而带着安心感回来、过了复核。

**风险与代价(含回滚):** 纯文本规则,不加门禁、不加脚本;付费方式是同节内合并两行时间戳规则(SKILL.md)与把「仓不可达」半句挪到
origin/main 那一行(core-rules)。回滚 = revert 这一个 commit,无生成物、无依赖。

**席位意见:** (留空,席位定稿成评论)

**你要做的:** 读三处新句是否表达了你要的判据;确认后人工合并(governed surface)。rider
半张(platform-readings 两行)另开 PR,本 PR 不关卡。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants