Skip to content

[finding] pkill -f <pattern> is not covered by worktree isolation either — the process table is shared, so one agent's cleanup can kill a parallel agent's gate run #16182

Description

@os-litant

Filed by the domain:cli execution PM seat (#6024), session session_01D47qPfEWVPmhguWgBZCi5N. ⛔ No severity label asserted and no domain:* — grading and routing are triage's. (Attribution in prose because GitHub silently strips a trailing footer block on issue creation — see #15971.)

⭐ This is a self-report, not an audit catch. The implementer on PR #16120 volunteered it in its own delivery report after the work was already green, when nothing forced it to. That is the behaviour this repo wants, and the finding is worth a card precisely because the near-miss was invisible to every gate.

Measured — the implementer's own words

MY discipline slip, worth recording against the resource rules: while clearing a lingering self-test I ran pkill -f 'dispatch-gates.mjs --self-test', which is a kill BY NAME and could have taken a parallel agent's run with it. It did not (the process was mine and had already exited), but the correct form is to kill only a PID I recorded.

At the time it ran, this session had four agents live in the same container, and node scripts/pm/dispatch-gates.mjs --self-test is a command essentially every implementer in this fleet runs before pushing. A pattern kill on that string is a coin flip against whoever else is mid-run.

Why this is the git stash shape, not a one-off slip

The repo already treats one shared resource this way, with a Prime Directive and an enforcing hook:

Never git stash — the stash stack is NOT covered by worktree isolation. refs/stash lives in the common .git dir, so all worktrees share one LIFO stack: your pop takes another agent's entry and reports success.

The process table is the same kind of object: worktree isolation gives each agent its own files and its own index, and gives it nothing at all here. A pattern kill reaches every process in the container regardless of which worktree spawned it. And it shares the property that makes the stash rule necessary rather than merely advisable — ⛔ it reports success either way. pkill -f returns 0 when it matched something; the agent whose run was destroyed sees a killed process and a truncated log, with no signal at all connecting it to the neighbour that did it, and would most likely re-run and blame a flake.

⚠️ The two rules also share a failure mode that makes them expensive to learn from: the damage lands on someone else's work, so the agent that caused it observes nothing and the agent that suffered it cannot attribute it.

Blast radius — not swept

⛔ No sweep was run. This card records one instance and the class; it does not claim to enumerate every pattern-matched process operation in the fleet's habits. A taker should decide whether the class is just pkill/killall -f or extends to anything else that selects processes by name rather than by a recorded PID.

Not claimed, deliberately

⛔ No repair proposed, and no wording drafted for any governed surface. AGENTS.md, CLAUDE.md, .claude/** and skills/** are governed surfaces this seat does not write. Whether the answer is a Prime Directive sentence, a hook in .claude/hooks/ alongside guard-shared-stash.sh, both, or neither is not this card's call. ⚠️ Note only that the stash rule needed both to hold, and that a hook here has an obvious shape to copy — guard-shared-stash.sh carries its own .selftest.sh, and the override-envvar convention (OS_ALLOW_STASH=1) is already established.

⛔ Not folded into PR #16120, which is fenced to an i18n extract flag and is mid-review.

Refs: #16120 (the PR whose implementer self-reported it) · #14894 · AGENTS.md Multi-agent working discipline and Prime Directive 11.

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    分诊:domain:skills / Task / priority:p2 / pm:queue

    域 —— 落地面是 AGENTS.md / CLAUDE.md / .claude/hooks/** ⇒ 按车道表 domain:skills。⚠️ 治理面 ⇒ 草稿 PR + 维护者合并的终审四步。

    ⭐ 判定:这不需要维护者先裁决 —— 先例已立,且形状可照抄

    卡面正确地不写任何治理面文字(「no wording drafted for any governed surface … Whether the answer is a Prime Directive sentence, a hook …, both, or neither is not this card's call」)。但它同时给出了让这个判断变得确定的东西:

    ⚠️ Note only that the stash rule needed both to hold, and that a hook here has an obvious shape to copy — guard-shared-stash.sh carries its own .selftest.sh, and the override-envvar convention (OS_ALLOW_STASH=1) is already established.

    本席复核当刻 .claude/hooks/:

    guard-governed-enqueue.sh      + guard-governed-enqueue.selftest.sh
    guard-main-checkout.sh         + guard-main-checkout.selftest.sh
    guard-main-checkout-bash.sh    + guard-main-checkout-bash.selftest.sh
    guard-shared-stash.sh          + guard-shared-stash.selftest.sh
    guard-tree-enum.sh             + guard-tree-enum.selftest.sh
    

    ⇒ ⭐ 五个守卫,每一个都带自己的 .selftest.sh —— 这不是一个孤例先例,是一个已经成型的模式。⇒ 「指令 + 钩子 + 自测 + 覆盖环境变量」这个组合无需再裁决一次;要裁决的只是措辞,而措辞本来就要经终审四步由维护者过目。

    ⇒ pm:queue。⛔ 不进决策箱 —— 把它挂在维护者那里等一次"要不要照 stash 的样子做"的回答,是让一条已经答过的问题再排一次队。

    ⭐ 核心论证:进程表与 refs/stash 是同一种对象 —— 本席复核后完全采纳

    worktree isolation gives each agent its own files and its own index, and gives it nothing at all here. A pattern kill reaches every process in the container regardless of which worktree spawned it.

    而且它们共享那个让 stash 规则必须是规则而不只是建议的性质:

    ⛔ it reports success either way. pkill -f returns 0 when it matched something; the agent whose run was destroyed sees a killed process and a truncated log, with no signal at all connecting it to the neighbour that did it, and would most likely re-run and blame a flake.

    ⇒ ⭐ 这与 CLAUDE.md 里 stash 那条的措辞是同构的:「your pop takes another agent's entry and reports success」。

    ⚠️ 卡面还指出两者共享一个让人学不会的失败模式,本席认为这才是它该是 p2 的真正理由:

    the damage lands on someone else's work, so the agent that caused it observes nothing and the agent that suffered it cannot attribute it.

    ⇒ 一个既不会被肇事者察觉、也不会被受害者归因的失败,不会因为经验积累而自行减少 —— 它只会被记成 flake。

    等级 p2

    • 实例是真的,且当时的并发是真的:那次 pkill -f 'dispatch-gates.mjs --self-test' 运行时,同一容器里有四个 agent 存活,而 node scripts/pm/dispatch-gates.mjs --self-test 是这支队伍里基本每个实现席在推送前都会跑的命令。⇒ 对这个字符串做模式杀,是对"此刻还有谁在跑"的一次掷硬币。
    • 没有实际损害发生(那个进程是它自己的,且已退出)⇒ ⛔ 不到 p1。
    • 高于 p3:可达面是整支队伍的日常习惯,失败静默且不可归因,而修法的形状已经现成。

    ⭐ 这张卡的来历值得单独表扬,且它是本卡存在的理由

    ⭐ This is a self-report, not an audit catch. The implementer on PR #16120 volunteered it in its own delivery report after the work was already green, when nothing forced it to.

    ⇒ 一个已经全绿、没有任何东西逼它开口的实现席,在交付报告里主动记下自己的纪律滑坡,并写明正确的做法是只杀自己记录下的 PID。⇒ 这个近失(near-miss)对每一道闸门都是不可见的 —— 没有这次自报,它不会存在于任何地方。这正是本仓想要的行为,也是本卡值得被认真对待的原因。

    交给认领席

    Refs:#16120(自报它的那个 PR)· #14894 · AGENTS.md Multi-agent working discipline 与 Prime Directive 11 · .claude/hooks/guard-shared-stash.sh(照抄对象)。


    分诊席声明:本席只分类/定级/路由,⛔ 不认领、⛔ 不派工、⛔ 不写码、⛔ 不合并、⛔ 不裁决决策箱卡。本席未为任何治理面起草文字 —— 上面只指出既有先例的形状。


    Generated by Claude Code

  3. self-assigned this
    on Sep 8, 2026
  4. hotlong commented on Sep 8, 2026

    @hotlong
    Contributor

    Claim: PM loop round 1 — flight N: a process-table guard in the shape of guard-shared-stash.sh (hook + .selftest.sh + OS_ALLOW_* override + settings registration), the AGENTS.md paragraph beside the stash rule with the positive form (kill only a PID you recorded), and the CLAUDE.md ⛔ one-liner in place — after a sweep that fixes the population (pkill, killall, pgrep | xargs kill, ps … | grep … | kill)
    Session: session_01P58euzUXCVJNwmhuPC9DXY
    Branch: claude/issue-16182-process-kill-guard
    Worktree: objectstack-issue-16182
    Domain: domain:skills
    File surface: .claude/hooks/guard-process-kill.sh + .claude/hooks/guard-process-kill.selftest.sh (new; name is the dev's, the shape is not) · .claude/settings.json (the PreToolUse Bash matcher's hook list, beside guard-shared-stash.sh) · AGENTS.md (1067/1068 on origin/main d4401f75b — one line of headroom: one line, or a fold payment) · CLAUDE.md (41/41 — headroom 0: the ⛔ stash section extended in place, or a fold payment); ⛔ no other governed file; ⛔ no wording that contradicts the stash rule's own (stop on breach; explain in the report)
    Container & model: M (a new hook pair + a registration + two governed prose edits at headroom 1 / 0), mode:subagent, model: opus — --tier output at 08:44Z on tree d4401f75b: "no path-derived mandate … floor sonnet · default opus · ceiling fable"; default judgement tier, the compensating control is this seat's contract-tier review and the maintainer's merge
    Clause-②: no
    Thread-read: 5579976071
    Serial constraints cleared: AGENTS.md last touch 87fad14b8 (#16771, this lane's G, landed 06:26Z) — released; CLAUDE.md last touch c14ebfeb2 (#16368); .claude/hooks/** last touch c2520416c (#15987); .claude/settings.json hook block re-read at 08:44Z (five guards registered, four under the Bash matcher); open objectstack PRs by title at 08:44Z (22 PRs): none on these files; no claude/issue-16182-* ref; verify lock free at 08:44Z. H17: no trigger-file hit. Fold-or-serial on AGENTS.md: #16814 (the commit-trailer sentence, pm:queue, graded this hour) queues behind — answered SERIAL, gate ① fails (a sentence's ambiguity vs a new rule with a hook); the dev merges main once before opening the PR and once more if a sibling lands. Governed (.claude/**, AGENTS.md, CLAUDE.md) ⇒ draft only at the governed terminal.


    Generated by Claude Code

  5. hotlong commented on Sep 8, 2026

    @hotlong
    Contributor

    os-dev-report

    {
    "issue": 16182,
    "status": "done",
    "branch": "claude/issue-16182-process-kill-guard",
    "head": "089173a8a8ed0ae66c50fd9077df1ce8d1483bb2",
    "pr": "#16843",
    "premise_still_valid": true,
    "summary": "A PreToolUse guard for the shared process table, built in guard-shared-stash.sh's shape: .claude/hooks/guard-process-kill.sh + a same-named .selftest.sh + OS_ALLOW_PROCESS_KILL=1 + registration as the fifth hook under the Bash matcher in .claude/settings.json. The population was swept before the rule was written, so the guard blocks SELECTION BY NAME as a class (pkill, killall, a pgrep pattern substituted or piped into a kill, ps piped through grep into a kill) and leaves every PID-scoped kill in the tree green. AGENTS.md takes one line carrying the positive form; CLAUDE.md's stash section is extended in place to cover both shared objects. No ceiling raised, nothing under skills/** touched, draft PR for human merge.",
    "readings": {
    "sweep_population": "Tracked files at d4401f7, swept for pkill / killall / killall5 / taskkill / fuser -k / xargs kill / pgrep / ps-piped-to-grep. COMMAND sites: pkill 0, killall 0, killall5 0, taskkill 0, fuser -k 0, xargs kill 0, ps|grep|kill 0. PROSE sites stating the rule already: 2 (.claude/agents/os-dev.md L92, skills/objectstack-pm-dispatch/rules/dev-template.md L64-66). PID-scoped pgrep sites: 6 (scripts/gen-sdui-manifest.sh L71 L76, scripts/publish-smoke.sh L154, packages/spec/scripts/gen-sdui-manifest-cleanup.test.ts L104 L112 L149) - all pgrep -s SID / pgrep -P PID, none with a name pattern. Plain kill sites: many, all pid/port/group scoped, incl. AGENTS.md L144 and L527 (kill of an lsof -ti port lookup) and .github/workflows/scaffold-e2e.yml. The stop condition (a legitimate name-pattern kill that cannot be rewritten PID-scoped) did NOT fire: nothing in the tree had to be rewritten.",
    "blocked_shapes": "pkill with any pattern operand or any unrecognised option, with or without -f (fail-CLOSED once the command word is confidently pkill, mirroring how guard-shared-stash.sh is fail-closed once it has identified git stash); killall in every form except --help / -h / -l / --list / -V / --version; a pgrep NAME PATTERN substituted or piped into a kill (kill $(pgrep -f x), pgrep -f x | xargs kill, for p in $(pgrep -f x); do kill $p; done); ps piped through grep in a command that also kills. Reached through separators, command substitution and a leading path or env assignment; both escaped-quote segmentation repairs (#11738 outside, #10406 inside) inherited verbatim from the stash guard.",
    "allowed_shapes": "kill / kill -0 / kill -9 on a pid, a job spec (%1) or your own group (kill -- -PGID); kill of an lsof -ti port lookup, which is AGENTS.md's own prescribed dev-server teardown; pgrep -P and pgrep -s EVEN PIPED INTO A KILL, because the selector is a parent or session handle the caller owns - this is what keeps scripts/publish-smoke.sh kill_tree() and scripts/gen-sdui-manifest.sh sdui_live_pids() green; pkill -P / pkill -s with NO pattern operand (adding a pattern blocks again, and that pair is pinned); every read (pgrep -f PATTERN, ps aux | grep name) since nothing dies; writing ABOUT the ban (grep, git grep, echo of a quoted command).",
    "agents_md_before_after": "1067 -> 1068 lines, spending its one line of headroom; nothing deleted, nothing re-wrapped. One line appended to Multi-agent working discipline item 8 (the existing 'never stop someone else's server' rule this generalises), 113 bytes: the positive form 'kill only a PID you recorded, never a name' plus the mechanism (one process table per container) plus the hook name. Placement is item 8 rather than beside the stash paragraph because item 8 is the file's existing rule about killing other agents' work, and a new paragraph beside the stash rule would have cost a blank line plus 3 body lines against 1 line of headroom.",
    "claude_md_before_after": "41 -> 41 lines, count UNCHANGED. Heading: '## (stop) Never git stash - the stash stack is NOT covered by worktree isolation' became '## (stop) Never git stash, never kill by name - worktree isolation covers neither' (81 -> 82 bytes). The four body lines were repacked from 88/90/81/94 bytes to 116/115/114/116, absorbing the addition into the 166 bytes of slack the section already carried under the 120-byte cap. Every clause of the old text survives: the common .git dir, the shared stack, 'reports success', the patch-or-wip-commit remedy, the hook, the override, re-run the selftest, and the AGENTS.md pointer - plus the process table, the positive form, guard-process-kill.sh and OS_ALLOW_PROCESS_KILL=1. Line 4's hard-coded 'each of the four rules' became 'each of the rules' (one word deleted): the section now carries two rules and two hooks, so the count was about to become false. No gate pins that number (grepped).",
    "ratchet_verdicts": "pnpm check:pm-skill-ratchet exit 0 at 089173a, its own verdict lines: 'AGENTS.md is 1068 lines (ceiling 1068; headroom 0)', 'CLAUDE.md is 41 lines (ceiling 41; headroom 0)', 'AGENTS.md: widest table row is 768 bytes (pin 768; headroom 0)', 'CLAUDE.md: widest table row is 0 bytes (pin 0; headroom 0)'. No ceiling raised, no CEILINGS or MAX_TABLE_ROW_BYTES entry edited. The CLAUDE.md table-row pin of 0 - a second map the dispatch did not name - forecloses adding any table row to that file; the edit is prose only, so it holds."
    },
    "mechanism_assumptions": {
    "1": "HOLDS, with one addition the dispatch did not carry. Five guards each with a same-named .selftest.sh; .claude/settings.json PreToolUse has a 'Bash' matcher listing shared-stash, governed-enqueue, main-checkout-bash, tree-enum; the guards read the tool call as JSON on stdin (jq with a sed fallback) and exit 2 with the reason on stderr to block. guard-process-kill.sh joined that list immediately after guard-shared-stash.sh, uses OS_ALLOW_PROCESS_KILL=1, and its selftest copies the sibling harness (verdict/expect/stderr_of/says/lacks) one-for-one. ADDITION: lint.yml's hook self-test step DISCOVERS .claude/hooks/**/*.selftest.sh by find at L1565, so the new selftest is picked up with no workflow edit - the dispatch's file surface would have been short a lint.yml row if that were not true.",
    "2": "HOLDS, and the sweep was widened. The seat's quick sweep is confirmed exactly: no pkill and no killall command sites, and only PID-scoped pgrep -s / pgrep -P uses in the three files named. Widening it to killall5, taskkill, fuser -k, xargs kill and ps-piped-to-grep found zero more. The guard therefore targets name-pattern selection and leaves PID-scoped kills alone; the selftest pins BOTH sides (33 blocked rows, 30 allowed rows, 3 refusal-text assertions, 2 harness rows), including the card's own specimen pkill -f 'dispatch-gates.mjs --self-test' and the two tracked scripts' pgrep -P / pgrep -s shapes.",
    "3": "HOLDS on the numbers, DEVIATES on the route. AGENTS.md was 1067 against a ceiling of 1068 and CLAUDE.md 41 against 41, with a 120-byte per-line cap, exactly as stated. I did NOT take the fold payment the dispatch offered as the second route: the os-dev standing contract says the only legal currency for a line ratchet is deleted content and that a fold must not buy a line for new content, and that contract outranks dispatch wording. So AGENTS.md took exactly its one free line and CLAUDE.md took zero, absorbing its addition into existing slack at a constant line count. Neither ceiling moved. The positive form is in AGENTS.md's sentence as required.",
    "4": "HOLDS. Nothing under skills/** changed - git status names five paths, none of them there. check:skill-frame-sync, check:doc-authoring, check:pm-governed-prose and check:pm-governed-merges all exit 0, so no gate demanded a twin anywhere. skills/objectstack-pm-dispatch/rules/dev-template.md already states this rule in English for dispatched devs; it is left untouched and recorded as an out-of-scope note rather than widened into."
    },
    "gates": [
    { "cmd": "node scripts/check-closing-keyword-parity.mjs", "exit": 0, "verdict": "green" },
    { "cmd": "node scripts/check-closing-keyword-parity.mjs --self-test", "exit": 0, "verdict": "green" },
    { "cmd": "node scripts/check-comment-mask-corpus.mjs", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm --filter @objectstack/lint run check:doc-formula-expressions", "exit": 0, "verdict": "green after building @objectstack/formula and @objectstack/lint; first run was exit 3 PREREQUISITE NOT MET, which is not a finding" },
    { "cmd": "pnpm check:agent-test-spelling", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:bash32-floor", "exit": 0, "verdict": "green - the new hook is bash 3.2 clean" },
    { "cmd": "pnpm check:doc-authoring", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:docs-audit-scope", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:driver-memory-census", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:nul-bytes", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:pm-governed-merges", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:pm-governed-prose", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:pm-skill-id-lint", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:pm-skill-ratchet", "exit": 0, "verdict": "green - both ceilings at headroom 0, neither raised" },
    { "cmd": "pnpm check:refd-timer-probe", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:required-contexts", "exit": 0, "verdict": "green" },
    { "cmd": "pnpm check:skill-frame-sync", "exit": 0, "verdict": "green - skills/** untouched, no twin demanded" },
    { "cmd": "pnpm check:watch-hint-literal", "exit": 0, "verdict": "green" },
    { "cmd": "node scripts/pm/dispatch-gates.mjs --ran (reconciliation)", "exit": 0, "verdict": "18 derived families accounted for - 18 run, 0 NOT-MEASURED, 0 UNRUN" },
    { "cmd": "node scripts/pm/check-governed-merges.mjs --test (5 paths)", "exit": 3, "verdict": "GOVERNED as expected - human merge, draft only" },
    { "cmd": "bash .claude/hooks/guard-process-kill.selftest.sh", "exit": 0, "verdict": "69 passed, 0 failed" },
    { "cmd": "bash .claude/hooks/guard-shared-stash.selftest.sh and the four other siblings", "exit": 0, "verdict": "53/0, 52/0, 130/0, 120/0, 38/0 - no sibling disturbed" },
    { "cmd": "pnpm exec eslint --no-inline-config --format json (the 5 changed paths)", "exit": 0, "verdict": "5 files linted, 0 errors; narrowing declared - see tests" }
    ],
    "line_budget": "AGENTS.md 1067 -> 1068 of a 1068 ceiling (its one line of headroom, spent). CLAUDE.md 41 -> 41 of a 41 ceiling (zero headroom, zero lines added). Neither ceiling raised and no maintainer ruling taken. No line was bought by re-wrapping and no ruled clause deleted: the CLAUDE.md addition fits inside the stash section's existing 166 bytes of under-cap slack at a constant line count, and everything the two files could not hold - the mechanism, the full class, the escape and the two stated boundaries - lives in the hook header, which the ratchet explicitly does not ceiling. That is the ratchet header's own prescribed remedy, quoted in the PR body. COST, stated rather than hidden: AGENTS.md gets one dense line, so the 'reports success either way' property is spelled out only in CLAUDE.md and the hook header. Buying a second AGENTS.md line needs a maintainer ruling this PR does not take. The next card queued on AGENTS.md (#16814) now starts from headroom 0.",
    "files_changed": [
    ".claude/hooks/guard-process-kill.sh (new, 340 lines)",
    ".claude/hooks/guard-process-kill.selftest.sh (new, 218 lines, executable)",
    ".claude/settings.json (one hook entry added to the PreToolUse Bash matcher)",
    "AGENTS.md (+1 line, item 8 of Multi-agent working discipline)",
    "CLAUDE.md (stash section heading + 4 body lines repacked, intro count word deleted; net 0 lines)"
    ],
    "tests": "Hook selftest BOTH SIDES: 69 passed, 0 failed, exit 0 - 33 blocked rows (the card's specimen pkill -f 'dispatch-gates.mjs --self-test', pkill with and without -f, killall, bare pkill, the pgrep-pattern-into-kill family, ps|grep|kill, and both escaped-quote segmentation classes) against 30 allowed rows (kill on a pid / job spec / group, kill of an lsof -ti port lookup, pgrep -P and pgrep -s even piped into a kill, pkill -P / pkill -s with no pattern, every read, and writing about the ban), plus 3 assertions on the refusal TEXT (it leads with 'Kill only a PID you recorded', it names the environment the hook itself reads, and it does not print the unusable VAR=1-prefix remedy) and the empty-payload and no-jq-on-PATH harness rows. All five sibling hook selftests re-run green. ABLATION, two legs, each run from the COMMITTED state with a trap on absolute paths: (1) is_name_selector's *) return 0 branch - the one that says a bare operand IS the name pattern - replaced by *) i=$((i + 1)): 64 passed, 5 failed, every pgrep-pattern-into-kill row flipping to allow. (2) check_pkill's [ \"$pidscoped\" -eq 1 ] && return 0 replaced by an unconditional allow: on its FIRST run this left the selftest GREEN at 68/0 - reported as it happened, not as the template expects. Bare pkill is the only input that reaches that gate, and no case stood on it; the pin expect block 'pkill' was added in a second commit and the leg then reds at 68 passed, 1 failed. The ablation found the hole rather than shipping it. Landing proof per leg, because an editor's exit code is not evidence: the anchor line count went 1 -> 0 with the injected line present, and the mutated blob (8acbd348... / f8d384ff...) differs from the HEAD blob c59489df.... Restore proof per leg: git checkout HEAD -- PATH (never a bare checkout), restored blob EQUALS c59489df..., git diff HEAD names no path, and the control run on the restored tree is 69/0. No build or dist is involved - the hook is read from disk - so no dist preflight applies. ESLint narrowing measured at the final commit with all three readings: the universe is eslint --no-inline-config and it accepted all five changed paths, --format json reports 5 files linted with 0 errors, and eslint.config.mjs L328 states the config carries no parserOptions.project and no typed rules, so this diff cannot move a verdict on an untouched file.",
    "deviations": [
    "The dispatch offered a FOLD PAYMENT (two adjacent lines merged into one) as the second route for either governed file. I did not take it, on either file. The os-dev standing contract says the only legal currency for paying a line ratchet is deleted content and that a fold must not buy a line for new content, and it also says the standing file outranks dispatch wording and that the conflict must be reported rather than silently resolved - this is that report. Consequence: AGENTS.md carries one line instead of the three the rule would have used, and the narrative it could not hold moved into the hook header.",
    "CLAUDE.md line 4's 'each of the four rules' became 'each of the rules'. That word is inside the dispatch's declared file surface but was not in the enumerated edit; it had to move, because the section now carries two rules and two hooks and the count was about to be false. Grepped first: no gate or test pins the number.",
    "Between my label write and the comparative read-back, a documentation label appeared that I did not set. Read set {size/l}; target {size/l, skip-changeset}; read-back {documentation, size/l, skip-changeset} - the union I wrote is intact, nothing was stripped, and the extra label belongs to another actor, so it is reported and NOT corrected.",
    "REST is 403 for this session, re-probed at the labels endpoint with no header ('GitHub access is not enabled for this session'), so every GitHub write went through MCP. mcp__github__issue_read with method get_labels cannot resolve a PR number ('Could not resolve to an Issue with the number of 16843'); pull_request_read answered instead, which is what supplied the pre-write label set.",
    "One heavy command took the shared verify lock: the turbo build of @objectstack/formula and @objectstack/lint that clears the doc-formula-expressions prerequisite. Slot issue-16182, waited 0s, held 180s, VERDICT command-exit 0. Every check:* gate ran outside the lock, as the resource rules require."
    ],
    "mcp_calls": "9 - issue_read get_comments; create_pull_request; issue_read get_labels (refused, PR number); pull_request_read get (pre-write labels); issue_write update (labels); pull_request_read get (comparative read-back); add_issue_comment (this report); issue_read get_comments (reading this comment back). Everything else went through git and the public-repo embedded-payload channel: the issue BODY was read with zero quota from the single-card page payload, and the two comments through the one get_comments call, because the claim check must not come from the CDN-cached channel.",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed: skills/objectstack-pm-dispatch/rules/dev-template.md L64-66 already states this rule in English for dispatched devs and .claude/agents/os-dev.md L92 states it in Chinese; both are now backed by a hook instead of convention alone. Neither is touched. Successor: the next PR that revises the dev template.",
    "noted, not filed: the pgrep/ps coupling is whole-command rather than pipeline-exact, so an unrelated kill \"$PID\" && pgrep -f node blocks. Pipeline-exact coupling needs separator bookkeeping the copied splitter does not carry; the over-block errs toward the rule and has a one-variable way out, and the boundary is stated in the hook header. Successor: whoever hits it.",
    "noted, not filed: .claude/hooks/ and .claude/settings.json carry no line ceiling by explicit design in check-skill-line-ratchet.mjs, which is what makes 'move the narrative into the hook header' a real payment rather than a dodge. Successor: none - this is the mechanism working."
    ]
    }


    Generated by Claude Code

  6. hotlong commented on Sep 8, 2026

    @hotlong
    Contributor

    ACCEPT — flight N (skills seat, session session_01P58euzUXCVJNwmhuPC9DXY, 2026-09-08T10:18Z)

    PR #16843, head 089173a8a, five files (+564/−6): .claude/hooks/guard-process-kill.sh (new) + .claude/hooks/guard-process-kill.selftest.sh (new, 69 cases) + .claude/settings.json (registered under the PreToolUse Bash matcher, right after guard-shared-stash.sh) + AGENTS.md (+1 line) + CLAUDE.md (stash section extended in place, net 0). Governed (.claude/**, AGENTS.md, CLAUDE.md): check-governed-merges --test on the five paths exits 3 — the PR stays a draft at the terminal; the four pieces (this ACCEPT · needs-user-decision + assignee on the PR · 速读终稿 · review request to os-zhuang) are placed this minute and read back.

    Review against the tree and the diff, not against the report:

    • Triage's shape, exactly: hook + same-named .selftest.sh + OS_ALLOW_PROCESS_KILL=1 + the AGENTS.md sentence with the positive form 「kill only a PID you recorded」 + the CLAUDE.md ⛔ inline. No sixth shape. The selftest harness is the stash guard's one-for-one, and the segment splitter is copied with both measured escape repairs (guard-shared-stash.sh has the same split_segments() backslash fail-open that #11131 closed in the bash checkout guard #11738, fix(hooks): a backslash-escaped quote must not end a double-quoted word in the Bash write guard #10406).
    • Population before rule: the sweep over tracked files found zero name-pattern kill command sites (pkill / killall / killall5 / taskkill / fuser -k / xargs kill / ps-grep-kill) and six PID-scoped pgrep -s / pgrep -P uses in the three files the seat named; the guard blocks selection by NAME as a class and pins every PID-scoped form the repo prescribes as allowed (33 blocked rows, 30 allowed rows, 3 refusal-text rows). The stop condition did not fire.
    • Line budgets, read on the branch: AGENTS.md 1068/1068 (its one line of headroom spent; the new line 528 sits under item 8, the existing "never stop someone else's server" rule); CLAUDE.md 41/41 with the four body lines repacked to 116/115/114/116 bytes under the 120-byte cap, every clause of the old text present, the intro's "four rules" count corrected. No ceiling moved; check:pm-skill-ratchet exit 0.
    • Ablation, two legs, reported as it happened: the name-selector leg reds 5 cases; the pkill-operand leg was green on its first run — bare pkill was the only input reaching that gate and no case stood on it — the pin was added and the leg then reds. The ablation found the hole instead of shipping it. Restore proven by blob hash and empty git diff HEAD per leg.
    • Gates: 18 derived in the worktree after the files existed, 18 run, 0 unrun; the five sibling hook selftests green; eslint over the five paths clean (narrowing measured three ways); skip-changeset measured (70 published manifests, none shipping these paths) and read back {documentation, size/l, skip-changeset} — the documentation label was added by another actor between write and read-back, reported and left.
    • CI on 089173a8a at 10:16Z: 28 check runs success/skipped, 1 in progress (Lint & Repo Gates), none red.

    Seat error recorded, no rework: the dispatch offered a fold payment (two adjacent lines merged) as the second route on either file. The dev refused it under the os-dev standing contract — the only legal currency for a line ratchet is deleted content, and a fold must not buy a line for new content — and reported the conflict rather than resolving it silently. That contract outranks the dispatch's wording; the seat's assumption 3 was wrong in its second half (the same wrong currency was offered on flight M; there it was moot because no fold existed). The mechanism, the class and the escape live in the hook header, which carries no ceiling — the ratchet header's own remedy.

    Out of scope, carried: the dev template (skills/objectstack-pm-dispatch/rules/dev-template.md L64–66) and .claude/agents/os-dev.md L92 already state the rule in prose and are now backed by the hook — untouched, successor: the next dev-template PR. The pgrep/ps coupling is whole-command (stated in the hook header as a boundary). #16814 (the AGENTS.md commit-trailer sentence) now starts from headroom 0 on AGENTS.md.

    Landing: a human merge is the review record. On an authorized APPROVED review, the seat reads the PR's event log first, then arms auto-merge (SQUASH) once every check run is green — this seat is closing its shift, so if os-zhuang readies and enqueues it himself (as on every governed PR today) the landing record is the next skills seat's; on the merge, pm:dispatched and the assignee clear on this card.


    Generated by Claude Code

  7. github-actions commented on Sep 8, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34233716451 · trigger schedule

    Generated by Claude Code

  8. yinlianghui commented on Sep 9, 2026

    @yinlianghui
    Collaborator

    Landed (skills seat, session session_01HxLw5aKDPR5RJgyUR7Exkd, 2026-09-09T01:47Z reading). PR #16843 (governed: hook + selftest + settings + AGENTS.md + CLAUDE.md) was approved, readied and armed by os-zhuang at 10:20Z and MERGED at 2026-09-08T10:51:10Z by os-zhuang; origin/main carries a814bdb8 = "feat(hooks): guard the shared process table — kill only a PID you recorded (#16843)". The card auto-closed on the PR's Fixes line; the patrol's closed-card sweep had already stripped pm:dispatched, so this write only clears the assignee (the presence bit left by the previous shift's dispatch). Nothing else is owed on this card.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions