From ad2e5934e727698d19299361c3f7986cdc6e8111 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 16:26:09 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(pm,agents):=20write=20identity=20follo?= =?UTF-8?q?ws=20the=20channel,=20not=20the=20account=20=E2=80=94=20REST-on?= =?UTF-8?q?ly=20content=20writes,=20the=20ACCEPT=20refuses=20MCP=20writes,?= =?UTF-8?q?=20a=20stale=20shared=20checkout=20re-seats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Equal-line edits under every ratchet (SKILL.md 812, os-dev.md 403, platform-readings 454, rest-channel 82, core-rules 151), every touched line at or under 120 bytes. - facts and invariants: content writes go only through the REST proxy, never an MCP content-write tool; `user.login` on a write names the channel's token (installation ⇒ `claude[bot]`, user-to-server ⇒ the bound user — per session, not the seat's to choose), never the actor; attribution is the session ID in the text carrier. Rewritten in os-dev.md :51/:53, SKILL.md :97–:98, core-rules :25, platform-readings :129, rest-channel :54. - dispatch order and acceptance: every dispatch order carries a `Writes:` line; an os-dev-report whose `mcp_calls` names a write tool (the settings deny list + `update_pull_request`) is refused, on both the seat side (SKILL.md 复核) and the dev side (os-dev.md :369). - propagation: at fire time the seat also reads the latest origin/main touch of the harness-loaded files (.claude/settings.json, .claude/agents/*.md, .claude/hooks/*) against the shared checkout's HEAD; a touch not in HEAD ⇒ close the shift and re-seat in a fresh session, never advance the shared checkout in place. The reading is scripts/pm/check-harness-current.mjs (git only, seat-side, not CI). - fleet: the new-repo registration checklist gains the write-identity locks port (deny + hooks). Density paid inside each file: four SKILL.md bullet pairs merged (终报要求/机器可核字段, mode:cloud/build-heavy, marker spellings/ missing-notification, 报告契约 authority/second-copy); os-dev.md :53 drops the board/wide-search clause already carried by :50 and :54. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 --- .claude/agents/os-dev.md | 6 +- .claude/skills/pm-dispatch/SKILL.md | 22 +++---- .../pm-dispatch/references/core-rules.md | 2 +- .../references/platform-readings.md | 2 +- .../pm-dispatch/references/rest-channel.md | 2 +- scripts/pm/check-harness-current.mjs | 59 +++++++++++++++++++ 6 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 scripts/pm/check-harness-current.mjs diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index a04f9c55f09..171ddc00548 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -48,9 +48,9 @@ model: opus - 它们进 PR `## Acceptance notes`,报告 `out_of_scope_findings` 记 `noted, not filed: …`,席位 ACCEPT 读。 - 写 `noted, not filed` 前先答哪一个 PR 或人会碰到这个文件;答不出就写明「承接者:无」。 - 立卡者不查重,只在卡面附 3–5 个查重词;查重归分诊席,⛔ 不扫 open issues、不拉板。 - - GitHub 写一律走 REST 代理(`curl` 带环境 `GITHUB_TOKEN`),署名恒 App 的 `claude[bot]`。 + - GitHub 写一律走 REST 代理(`curl` 带环境 `GITHUB_TOKEN`);归属 = 文本里的 session ID,非 `user.login`。 - 写预算四笔:`git push`、一次 `POST /pulls`(draft)、`POST /issues/{n}/labels`、`os-dev-report` 评论。 - - ⛔ 不用任何 MCP GitHub 写工具:用户账号署名,封号即隐;⛔ 不枚举板面、不宽词搜。 + - ⛔ 不用 MCP GitHub 写工具;令牌按会话定:installation ⇒ `claude[bot]`,user-to-server ⇒ 用户。 - 卡与线程只走 payload 档(公开仓单卡网页内嵌 JSON,拼写住 platform-readings)或单卡 REST 读。 - 三类发现附查重词进报告交席位代立,dev 不 `POST /issues`;预算外零写,⛔ 不 `PATCH` 正文。 - 通道对照表见 `.claude/skills/pm-dispatch/references/rest-channel.md`,其 ✓ 按座位实测。 @@ -366,7 +366,7 @@ model: opus "premise_still_valid": true, "summary": "what was implemented, 2-4 sentences", "tests": "commands run + pass/fail evidence (real output excerpts); ablation: rebuild + on-disk mutation proof", - "mcp_calls": " — your MCP GitHub call count for the whole run", + "mcp_calls": " — MCP GitHub calls with tool names; a write tool in the list = this report is refused", "api_writes": " — REST proxy writes, each endpoint listed", "open_questions": [ { "question": "…", "options": ["A …", "B …"], "recommendation": "A, because …" } diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index 7f68cb708a3..11b83eb9517 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -88,12 +88,14 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 维护者明示召唤是仲裁:有简报径直坐席;无简报才走保守确认,确认终止即坐席。 - 互斥清 ⇒ fetch 后读三章程文件(本文、core-rules、本席章程)在 `origin/main` 的最新触碰 sha。 - 异于上一开轮标记即先重读;留开轮标记(session ID + fire 时刻 + 该触碰,注明重读)再跑轮。 +- 同读 harness 载入面 `.claude/{settings.json,agents/*.md,hooks/*}` 的最新触碰是否已在共享检出 HEAD。 +- 否 ⇒ 收班、换新会话再派,⛔ 不推进共享检出;读数走 `scripts/pm/check-harness-current.mjs`。 ## 全体座位的不变量 - 状态只经 GitHub 标签、assignee、正文行与 `pm:seat` 座位贴读写;循环须能从全新会话恢复。 -- 用户账号仅三用:assignee、授权批准、维护者亲手;⛔ 席位与 dev 永不以用户账号写内容。 -- 内容恒经 REST 代理(`claude[bot]`);批准账号永不跑席位、不作席位 claude.ai 的关联用户。 +- 用户账号仅三用:assignee、授权批准、维护者亲手;批准账号永不跑席位或作其关联用户。 +- 内容写只走 REST 代理,⛔ 无 MCP 写;`user.login` 记令牌不记席位,归属 = 文本里的 session ID。 - GitHub 上一切新内容用英文;中文只留四通道(维护者速读、轮报、裁决引文、四维分析)。 - 裁决引文照抄不译;四维中文只管新记录,存量英文块 ⛔ 不迁移;存量中文 ⛔ 不追溯改写。 - 一座位一车道双射:域 X 谁管、PM Y 管什么,各恰好一个答案。 @@ -190,7 +192,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 单车道仓 `cloud`、`objectos`、`hotcrm`、`www.objectos.ai`;未来新仓默认此类。 - 单车道仓 `repo:*` 席自理机械三务,⛔ 不产 `domain:*`;决策卡入本仓收件箱,不落 objectstack。 - 机械三务 = 自扫 sweep、自打 `type`、自做 `finding` 首触定级。 -- 新仓登记是一张清单:座位贴、标签、类别归属、门禁盘点。 +- 新仓登记是一张清单:座位贴、标签、类别归属、门禁盘点、写身份锁移植(deny + hooks)。 - 新仓准入判据一句:这个仓真的需要常设席位吗。 - hotcrm 收卡判据与宪章见 `references/lanes/hotcrm.md`。 - objectui 卡按修复落点分流三流,`domain:ui` 是唯一新增标签。 @@ -532,8 +534,8 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 标准非协商条款 ⛔ 不抄进派发词:hook 已机械强制段与 os-dev.md 已载通用段砍掉。 - ⛔ 不砍卡类特有约束:ADR 类 draft-only、tier 推导引用、棘轮实况、释义纪律。 - 无条件条款只住角色文件,冲突时它胜、错了修那里,⛔ 不靠派发词临时覆盖。 -- 终报要求随派发词带一句:只收机器可核字段,⛔ 复述 PR body 叙事。 -- 机器可核字段 = gates / line_budget / deviations / files_changed。 +- 终报要求随派发词带一句:只收机器可核字段(gates / line_budget / deviations / files_changed)。 +- 派发令恒带 `Writes:` 行:只走 REST 代理、写预算(端点清单)、`mcp_calls` 计数,dev 两数都报。 - 清单、路径、行号在派发那一刻从树上取,⛔ 不从卡片/上次派发/记忆抄。 - 门禁清单取 `dispatch-gates.mjs --commands` 逐条跑,退出码先落盘,`--ran` 对账;⛔ 不抓人读输出。 - 点名单是线索不是规格,dev 对实际改动重取补跑;行级断言转述前必须自己重验。 @@ -551,8 +553,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 翻转公开语义的裁决随卡带全仓 pin 清扫,两句缺一不可,原文见 runbook。 - 拒收用例 ⇒ `code`+`status` 最低断言;过滤/谓词语义 ⇒ 编译面清单逐面申报,⛔ 静默略过。 - 资源与后端:S 级机械 + M ⇒ `mode:subagent`;S 级但不机械(判断面在设计不在门禁)按 M 待遇。 -- `mode:cloud` 只保留给 L/XL、必须活过 PM 会话的工作、浏览器/dogfood 验证。 -- build 重的 M 卡逐卡判断是否上云。 +- `mode:cloud` 只保留给 L/XL、活过 PM 会话的工作、浏览器/dogfood 验证;build 重的 M 卡逐卡判。 - 归档义务只落在云卡;OOM 死的单独重派;判定连同档位写进认领评论。 - 一次性云卡用 `create_session`,⛔ 不用 create_trigger+fire;trigger 流只留给定时/重复型。 - 云会话 `SendMessage` not-reachable 是设计非故障,⛔ 不复测。 @@ -563,8 +564,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 报告通道统一:GitHub 是两种模式共用的真相源;dev 终报先落 issue 评论、再作返回消息。 - 收集先扫 GitHub,标记评论在 = 报告完整;两处皆无才进探活/判死。 -- 标记两种拼写等效:HTML 注释形与首行纯文本 `os-dev-report`。 -- ⛔ 仅凭 HTML 注释形式缺失永不读作报告未达;⛔ 永不把没收到失败通知读作还在跑。 +- 标记两种拼写等效(HTML 注释形、首行 `os-dev-report`);⛔ 永不把没收到失败通知读作还在跑。 - 探活是每轮巡检的固定动作;完成通知不可靠,缺席什么都不证明。 - ① 巡检首动作 `list_triggers` 确认 Routine 启用,在飞重挂加速器;唤醒归 Routine,漏挂不断链。 - ② 在飞期间主巡检间隔 ≤45 分钟,待命期 60–70;节奏维护者明示可改。 @@ -599,6 +599,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 - 触 `skills/**` 的 PR 加问整包价值密度:从整包加载的客户 agent 座位读,⛔ 不从作者座位读。 - 超派发预算或小功能大扩写 ⇒ REWORK,⛔ 不因已经写好了放行。 - 判决 ACCEPT:issue 英文短评论,核对清单结论 + 抽查读数 + 偏差,链接 PR,⛔ 不复述其叙事。 +- `mcp_calls` 点名写工具(`settings.json` deny 清单 + `update_pull_request`)⇒ 拒收,⛔ 不带注放行。 - 判决 REWORK:逐项反馈,同认领重派;补丁轮优先 SendMessage 续派原 dev;最多 2 轮,第三次升级。 - 判决 ESCALATE:见 升级与决策 节。 - **ACCEPT 之后的路径分叉**:翻 ready / 挂 auto-merge / 入队前先取一次 PR 的路径面。 @@ -771,8 +772,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报 ## 报告契约 -- 终报 JSON 的权威形状住 `.claude/agents/os-dev.md` 终报消息节,字段与拼写以那里为准。 -- ⛔ 本文不抄终报形状的第二份。 +- 终报 JSON 的权威形状住 `.claude/agents/os-dev.md` 终报消息节,⛔ 本文不抄第二份。 - `premise_still_valid: false` + `pr: null` 是合法终报,当再分诊输入复核,永不当失败派发。 - `status: needs_decision` 时 `open_questions` 必须非空。 - `out_of_scope_findings` 只列三类立卡与 `noted, not filed`;立卡附查重词、归挂、立在修复仓。 diff --git a/.claude/skills/pm-dispatch/references/core-rules.md b/.claude/skills/pm-dispatch/references/core-rules.md index c6ae8d49c67..e7b1cd2e8e8 100644 --- a/.claude/skills/pm-dispatch/references/core-rules.md +++ b/.claude/skills/pm-dispatch/references/core-rules.md @@ -22,7 +22,7 @@ - 状态住 GitHub 标签、assignee、正文行与座位贴;⛔ 本地零状态;循环须能从新会话恢复。 - PM ⛔ 不写文件也不写代码,唯一例外是维护者逐 PR 明示授权的 `.claude/` 内部工具 PR。 - 该例外须把授权原话引在 PR 正文并由另一座位或维护者复核,⛔ 不得自审自合。 -- 用户账号仅三用:assignee、授权批准、维护者亲手;写恒经 REST 代理;批准账号永不跑席位。 +- 用户账号仅三用:assignee、授权批准、维护者亲手;写只走 REST 代理,署名随令牌非席位。 - GitHub 新内容一律英文,中文只留轮次报告、派发令裁决引文与四维分析;引文照抄不译。 - 存量中文 ⛔ 不追溯改写;一座位一车道双射,每域恰一 PM;与 AGENTS.md 冲突以之为准。 ## 状态模型 diff --git a/.claude/skills/pm-dispatch/references/platform-readings.md b/.claude/skills/pm-dispatch/references/platform-readings.md index fddf218aece..23d0f0f298f 100644 --- a/.claude/skills/pm-dispatch/references/platform-readings.md +++ b/.claude/skills/pm-dispatch/references/platform-readings.md @@ -126,7 +126,7 @@ - ⇒ MCP 限流先探 REST 再定退避,⛔ 不据一侧限流把整个平台的写都停掉。 - MCP 的读限流与写限流彼此独立,两向各有实测 ⇒ 一侧被拒 ⛔ 不推另一侧也不可用。 - REST 档以本班 repo-scoped 探针绿为前提;403 会话改按降级梯读。 -- 容器 curl 的 REST 通道 = App installation token,core 15,000/时,与 GraphQL 池独立计。 +- 容器 curl 的 REST 通道令牌按会话定:installation(`claude[bot]`)或 user-to-server(用户),core 15,000/时。 - GraphQL 池 5000/时,只留给没有 REST 对应物的几件。 - 那几件 = draft 翻转、auto-merge 挂载、语义 `/search/*`、Projects field_values、`issue transfer`。 - 逐操作通道归属、写侧配方与队列路由三读法见 `rest-channel.md`,⛔ 不在本表复述。 diff --git a/.claude/skills/pm-dispatch/references/rest-channel.md b/.claude/skills/pm-dispatch/references/rest-channel.md index de93862cfe5..24a4fe58592 100644 --- a/.claude/skills/pm-dispatch/references/rest-channel.md +++ b/.claude/skills/pm-dispatch/references/rest-channel.md @@ -51,7 +51,7 @@ - ✓ auto-merge 挂载 `PUT .../pulls/{n}/ccr/auto_merge` 带 `{"merge_method":"SQUASH"}`,`DELETE` 卸载。 - 入队读 timeline `added_to_merge_queue`,落地读 `git rev-list --parents`;⛔ `auto_merge` 与回显都不作数。 - 两条 2026-09-12 两席实调;备用通道 MCP `update_pull_request` 与 `enable_pr_auto_merge`。 -- 直合仓另有 `PUT .../pulls/{n}/merge`;ccr 的 timeline actor 记 `claude[bot]`,MCP 记席位账号。 +- 直合仓 `PUT .../pulls/{n}/merge`;actor 记通道令牌:REST 按会话为 `claude[bot]` 或用户,MCP 恒用户。 ## 不可迁移 —— 只有这三件,围着它们排计划;红窗守候规则住 `platform-readings.md` 配额段 diff --git a/scripts/pm/check-harness-current.mjs b/scripts/pm/check-harness-current.mjs new file mode 100644 index 00000000000..27b8be7c086 --- /dev/null +++ b/scripts/pm/check-harness-current.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * check-harness-current -- is every harness-loaded file on `origin/main` already in the + * shared (primary) checkout's HEAD? A seat-side fire-time reading; ⛔ not a CI gate. + * + * node scripts/pm/check-harness-current.mjs [--shared ] [--ref origin/main] + * + * The harness reads `.claude/settings.json`, `.claude/agents/*.md` and `.claude/hooks/*` from + * the PRIMARY checkout when a session starts and never reloads them, so a touch that lands on + * `origin/main` after that clone is inert for the running session -- measured: a session whose + * primary checkout predated the MCP-write deny list still carried every denied tool. Worktrees + * do not help (the harness never reads them) and the primary checkout is never advanced in place + * (worktree-first), so the remedy is to close the shift and re-seat in a fresh session. + * + * Reads git only -- fetch first. Exit 0 = current; 1 = stale (each stale path printed with its + * touch); 2 = undecidable: the ancestry test is negative on a SHALLOW clone and the touch is not + * newer than HEAD, so the missing ancestor path may be a truncation, not a fact -- deepen, rerun. + */ + +import { execFileSync } from 'node:child_process'; +import process from 'node:process'; + +const PATHS = ['.claude/settings.json', '.claude/agents/*.md', '.claude/hooks/*']; +const argv = process.argv.slice(2); +const opt = (flag, fallback) => (argv.includes(flag) ? argv[argv.indexOf(flag) + 1] : fallback); +const git = (dir, ...args) => { + try { + return { ok: true, out: execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() }; + } catch (error) { + return { ok: false, out: String(error.stdout ?? '').trim() }; + } +}; + +const common = git(process.cwd(), 'rev-parse', '--path-format=absolute', '--git-common-dir'); +const shared = opt('--shared', common.ok ? common.out.replace(/\/\.git$/, '') : undefined); +const ref = opt('--ref', 'origin/main'); +const head = shared ? git(shared, 'rev-parse', 'HEAD') : { ok: false, out: '' }; +if (!head.ok) { console.error(`check-harness-current: no git checkout at ${shared ?? '(cwd)'} -- pass --shared `); process.exit(2); } +const headTime = Number(git(shared, 'log', '-1', '--format=%ct', 'HEAD').out); +const shallow = git(shared, 'rev-parse', '--is-shallow-repository').out === 'true'; +const short = (sha) => sha.slice(0, 10); +let stale = 0; +let undecided = 0; +for (const path of PATHS) { + const touch = git(shared, 'log', '-1', '--format=%H %ct %cI', ref, '--', path); + if (!touch.ok || !touch.out) { undecided += 1; console.log(`? ${path}: no touch of ${ref} visible from ${shared} (unfetched ref, or a shallow window) -- UNDECIDED`); continue; } + const [sha, ct, ci] = touch.out.split(' '); + if (git(shared, 'merge-base', '--is-ancestor', sha, 'HEAD').ok) { console.log(`✓ ${path}: latest touch ${short(sha)} (${ci}) is in the shared HEAD`); continue; } + if (Number(ct) > headTime || !shallow) { stale += 1; console.log(`✗ ${path}: latest touch ${short(sha)} (${ci}) is NOT in the shared HEAD ${short(head.out)} -- STALE`); continue; } + undecided += 1; + console.log(`? ${path}: touch ${short(sha)} (${ci}) is not under HEAD in a SHALLOW clone and not newer than HEAD -- UNDECIDED, deepen and rerun`); +} +const verdict = stale + ? `STALE -- ${stale} harness-loaded path(s) on ${ref} are not in ${shared} HEAD ${short(head.out)}: close the shift and re-seat in a fresh session; ⛔ never advance the shared checkout in place` + : undecided ? `UNDECIDED -- ${undecided} path(s) could not be placed; fetch/deepen ${shared} and rerun` : `CURRENT -- every harness-loaded path on ${ref} is in ${shared} HEAD ${short(head.out)}`; +console.log(`check-harness-current: ${verdict}`); +process.exit(stale ? 1 : undecided ? 2 : 0); From 7103d0b09fedba5b121bb15c5067bc764067aaaa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 14 Sep 2026 16:27:12 +0000 Subject: [PATCH 2/2] docs(agents): keep the rewritten REST-proxy line under the 120-byte cap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The list indent under 基本规则 3 costs three bytes the draft measurement left out; `带环境` becomes `带`, 122 → 116 bytes, same content. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 --- .claude/agents/os-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index 171ddc00548..e8cdca03fe3 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -48,7 +48,7 @@ model: opus - 它们进 PR `## Acceptance notes`,报告 `out_of_scope_findings` 记 `noted, not filed: …`,席位 ACCEPT 读。 - 写 `noted, not filed` 前先答哪一个 PR 或人会碰到这个文件;答不出就写明「承接者:无」。 - 立卡者不查重,只在卡面附 3–5 个查重词;查重归分诊席,⛔ 不扫 open issues、不拉板。 - - GitHub 写一律走 REST 代理(`curl` 带环境 `GITHUB_TOKEN`);归属 = 文本里的 session ID,非 `user.login`。 + - GitHub 写一律走 REST 代理(`curl` 带 `GITHUB_TOKEN`);归属 = 文本里的 session ID,非 `user.login`。 - 写预算四笔:`git push`、一次 `POST /pulls`(draft)、`POST /issues/{n}/labels`、`os-dev-report` 评论。 - ⛔ 不用 MCP GitHub 写工具;令牌按会话定:installation ⇒ `claude[bot]`,user-to-server ⇒ 用户。 - 卡与线程只走 payload 档(公开仓单卡网页内嵌 JSON,拼写住 platform-readings)或单卡 REST 读。