补入本地今日改动:session-mechanism + product-planning(按用户令改推本仓库)

背景:旧远端 work.alotbuy.com 今天一直连不上(222/22 端口都不通)⇒ 今天本地累积多条提交推不上去;
用户令改推本仓库 [email protected]:admin/workbuddy_skills.git(只推这 5 个技能:
browser-harness / humanizer / humanizer-zh / product-planning / session-mechanism)。

本次改动(对照本机已安装的技能源逐文件 md5 比对得出,⛔ 是「补差异」不是「整体覆盖」):
· session-mechanism:含今天两条钩子改动(派活收工闸、点名加载收工闸)、
  rules.md §9「只写正向范围」、检查程序静默阈值 10→6、检查排期补 workspace_scope(修未分组)、
  以及今天修的三处陈旧指针(旧技能名 dsh-decision / agent-operating-rules 与旧路径编号)。
· product-planning:第③段「随段样式风格库」并入 design-system-tiaoyue、竞品分析方法论重写等。
· humanizer / humanizer-zh:已一致,零改动。
· browser-harness:比对出的 7 个「本地独有」全是**它自己 .gitignore 里就排除的**构建产物
  (src/*.egg-info/ 与 uv.lock)⇒ 按该技能自己的规矩不推(用户口径「不要环境」)。

仓库级配置(与旧仓一致):core.autocrlf=false;身份 maogeigei <[email protected]>。
⚠️ 克隆时仓库默认 autocrlf=true(会把行尾转成 CRLF)⇒ 已改回 false 并重新暂存,
   核对索引 blob 均为 LF、真实差异 23 个(⛔ 不是把几百个文件一起改掉)。
This commit is contained in:
admin committed 2026-10-07 20:33:49 +08:00
1 parent 0abd33b3a0
commit 61962f1726
23 files changed
+741 -250

No files matched your search

@@ -44,6 +44,9 @@
3. **历史记录按时间倒排** —— **新的在前面,旧的在后面**(⛔ 追加只能往前插,⛔ 不许接在末尾)。
4. **简明扼要有效** —— **单条 ≤6 KB**;⛔ 论证过程/对比表格/逐条展开全删;
⚠️ 但**判据要点一个不许丢**(长度达标而判据被删 = **更坏**,那是假绿)。
5. 🔴 **只写正向范围,⛔ 不写「不用于 XXXX」**(2026-10-07 用户令)
—— 规则正文在 **`rules.md §9`**(⛔ 只一处,本行只作指针)。一句话:技能/文档的"用途/范围"段
一律写**用于什么**;列"不用于 A/B/C"会**把 A/B/C 喂进自动匹配面** ⇒ 反而更容易被误命中。
## 三、🔴 为什么「存档」不等于「读得到」(10-04 实证)
+10 -10
View File
@@ -1,6 +1,6 @@
# manifest · 包内文件清单
> 生成方式:逐文件 `compile()` / `json.loads` + md5 | **最近一次全量重算:2026-10-07 08:15 (加载闸门:派活族不受冷却 + 冷却跳过留痕(含新判据))**
> 生成方式:逐文件 `compile()` / `json.loads` + md5 | **最近一次全量重算:2026-10-07 20:30 (修三处陈旧指针(旧技能名 dsh-decision / agent-operating-rules 与旧路径编号))**
> ⚠️ **2026-10-05 局部增量**:`references/pitfalls.md`(P0-77 拆条 + P0-73/P0-77 压缩)与
> `scripts/goalctl.py`(`--switch-goal` 确认闸 + 旧目标归档)两行的 md5/大小已按当天实测值更新;**其余行仍是 10-04 基线**。
> ⛔ 本表**不含** `install.log`(运行日志)与 `references/manifest.md`(自引用,写完即失真)。
@@ -120,7 +120,7 @@
| `assets/design-tokens.css` | 6364 | `0668feb3a4729f1022e579ac0c5d2569` | — |
| `install.py` | 37297 | `55603c2e739e7b531998f5563f15f789` | ok |
| `references/00-动手前必过.md` | 7181 | `33bc86e442ccd4fcbb930c79c4ad0641` | — |
| `references/01-文档索引.md` | 4159 | `ea4185d568b94d7aa915f84f259b9773` | — |
| `references/01-文档索引.md` | 4518 | `f401dd36b2d30cbc89ac91f9d633b72c` | — |
| `references/02-功能优先协作协议.md` | 35529 | `9122a6715bde02ab549805d69866d1cf` | — |
| `references/03-回复排版-核心块.md` | 7766 | `01277367d560383669ad493538e45587` | — |
| `references/04-决策方法论.md` | 35566 | `eb5bdf8f100beb8eacb9f20b042df86c` | — |
@@ -138,8 +138,8 @@
| `references/karpathy-output-ladder/references/ladder-workflow.md` | 2684 | `d49840707d19302d8f61f4e1d7f4ea2e` | — |
| `references/karpathy-output-ladder/references/ste100.md` | 3022 | `fdbe3271c21a3c17f326d1e71de33b0d` | — |
| `references/pitfalls.md` | 311254 | `d321a71e38dd246b8014b4c9100b12c7` | — |
| `references/rules.md` | 8106 | `95af3566066d27c3c224ce681a82efc1` | — |
| `references/supervise-persistence.md` | 34783 | `3c68c146984133449b8bd6a1240909f1` | — |
| `references/rules.md` | 9218 | `a8fd1e616600defb724fa64af67a7348` | — |
| `references/supervise-persistence.md` | 34799 | `d18d44d36fd939924f7d5752b7c2be40` | — |
| `references/taskgraph.md` | 3521 | `be6c6540be86475bb3430688c2987afc` | — |
| `references/作业规矩/00-作业总规矩(原 agent-operating-rules).md` | 70243 | `80fc5471015b237ee7aacd6976eda6fe` | — |
| `references/作业规矩/02-工作区纪律.md` | 21351 | `1a679fdeb9cca0883a47f619d1aec85d` | — |
@@ -150,9 +150,9 @@
| `scripts/board-launch.py` | 2794 | `998a66b9d335ab1863b5b871a527bf4f` | ok |
| `scripts/board.py` | 146111 | `d90395d1f1a7c3f94419a33cdf444384` | ok |
| `scripts/board_ext.py` | 45655 | `2319fb9d2d21bf961e03a42da9a1ad4b` | ok |
| `scripts/collabctl.py` | 28124 | `827fe07da25b86b773a3f701e86db351` | ok |
| `scripts/collabctl.py` | 32007 | `1c597185be099821c7b1d6dc66fadb05` | ok |
| `scripts/collabd.config.example.json` | 1158 | `6655c15c411a84e3ca12fd136dbe2c60` | ok |
| `scripts/collabd.py` | 434710 | `00bded98f6c49fa30881434df168642e` | ok |
| `scripts/collabd.py` | 434806 | `d4350022bbd75f37a35db63fa0472f88` | ok |
| `scripts/deliver-gateway-token.py` | 5685 | `cbba9648d404ecd2683061e19dbdb24a` | ok |
| `scripts/deploy_code.py` | 5325 | `a79cf5e5ffb3e8c60a31b4d0d8845f40` | ok |
| `scripts/forensics/proc-parent.py` | 2347 | `8cdfff2dbb88303298752c3777da7b20` | ok |
@@ -163,10 +163,10 @@
| `scripts/hooks/decision-rules-hook.py` | 12399 | `c521237bada7487aecf5e54ad4d19ace` | ok |
| `scripts/hooks/lock-guard-hook.py` | 20363 | `641457de0297ee7a812ccde57f67dc2b` | ok |
| `scripts/hooks/prompt-guards.py` | 9332 | `0aa1d1fdd2bf5a40cda2e60d7a785534` | ok |
| `scripts/hooks/reply-style-guard.py` | 11040 | `7e271b342f91e2e98cbc03631b7e68f3` | ok |
| `scripts/hooks/reply-style-guard.py` | 11283 | `1e52b65f279c0b4df70bb53bc613a774` | ok |
| `scripts/hooks/session-log-guard.py` | 24636 | `4dd13b24d541d1afc7b4874dba6bb1eb` | ok |
| `scripts/hooks/skill-load-guard.py` | 27515 | `d544387152d9140380484d046051c023` | ok |
| `scripts/hooks/stop-dialog-guard.py` | 46083 | `fab654dfacfeb6253fc751707bebcac6` | ok |
| `scripts/hooks/skill-load-guard.py` | 29994 | `2450b93b4bac87cf9e99697703efa78f` | ok |
| `scripts/hooks/stop-dialog-guard.py` | 51240 | `3e0791e42f41c37438601a9d2314dd8e` | ok |
| `scripts/hooks/supervise-ensure-hook.py` | 14308 | `dc6f62584a8384baba0a1f2460828013` | ok |
| `scripts/hooks/wb-result-hook.py` | 37199 | `2d4f694acd031222bf03b98a859b6e90` | ok |
| `scripts/init_workspace.py` | 24042 | `dda53acd24bcc66d509bbdab3ebc0488` | ok |
@@ -177,7 +177,7 @@
| `scripts/lock/op-lock.sh` | 4690 | `1a31eda3642e23693a65e1519860c744` | — |
| `scripts/lock/preflight-lock.sh` | 8673 | `e985ec1cb853fae4c351cd03b171355d` | — |
| `scripts/mut_run.py` | 13323 | `3bffc12e1e57d5efc743bac55e584b62` | ok |
| `scripts/selftest.py` | 488379 | `9f5d2f8d1888866c8a7a590e2471d85c` | ok |
| `scripts/selftest.py` | 490245 | `8cb5187bd927ecdecb216a6df4d855c4` | ok |
| `scripts/session-rules-check.py` | 44412 | `b2b468992d80421106316eaf451e2477` | ok |
| `scripts/stop-collab.py` | 10462 | `d8043bedd23b52b6642aaf7ec8b8c980` | ok |
| `scripts/supervise-launch.py` | 2503 | `f416a2f197e5524a8859c1d227ba1d05` | ok |
+13
View File
@@ -76,3 +76,16 @@
- ⛔ **反例(10-05,同一件事栽到第四次)**:常驻的起法只写在当天日志里 ⇒ 之后每一轮都重新折腾一遍、每一轮都得出同样的结论。
✅ 正解:结论**同时**落三处 —— `SKILL.md` 第一屏(会被读)+ `pitfalls.md` 一条(有来龙去脉)+ 本档一条规矩(约束以后怎么写)。
- 🔴 **配套**:凡"换会话容易忘、忘了就会重来"的结论,**优先放第一屏**,⛔ 不要塞进长文档。
## 9 🔴 只写正向范围,⛔ 不写「不用于 XXXX」(2026-10-07 用户令)
**技能与文档的「用途/范围」段一律写"用于什么"**,⛔ 不列"不用于 A/B/C"。
- **为什么(机制性,不只"占地方")**:技能的 `description` 与首屏是**自动匹配面**(技能靠描述匹配被加载)
⇒ 在里面列"不用于 X",等于**把 X 这些词喂进匹配面** ⇒ **反而更容易被不该命中的请求选中**
(实测形态:`draw-ui` 描述里列了「不用于普通插画/海报/故事板」⇒ 用户说"做个海报"时它更可能被选上)。
- **例外(用户 2026-10-07 明确给出)**:**经验沉淀可以写"如何避免踩坑"** ——
禁令**不删**,但写成「**正向目标 + 依据(曾栽过什么)**」的形态
(例:「本方法只出产品视角」+「曾因照英文商业框架产出而跑偏」)。
- ⛔ **别与"事实陈述"混**:「配置里不含本工作区」「该目录不含 `board.py`」是在**描述事实**,
不在本条管辖内 —— 一刀切会把它们误删。
@@ -374,7 +374,7 @@ $p = Get-Process -Id <pid> -ErrorAction SilentlyContinue # $null = 已死
## 六、⚠️ 还没有它就不算长期(两条,别自欺)
1. **静默基准**:检查会话要 `静默 ≥ 10 分钟` 才建(🔴 **2026-10-07 由 20 改 10**,用户原话
1. **静默基准**:检查会话要 `静默 ≥ 6 分钟` 才建(🔴 **2026-10-07 两度调整:20 → 10 → 6**,用户原话
「把检查程序的等待时间由 20分钟 改为 10分钟」;阈值=`collabd.py::CHECK_IDLE_MIN`,闸门与 `--dry-run` 试算同源)。
基准=**本工作区排期的 `updated_at` 最大值**(`_last_progress_ts()`);
⚠️ 本行原先写「基准取台账 `tasks.json` 的 mtime」——**那句与代码不符**,已按代码订正。
+5 -4
View File
@@ -2067,7 +2067,8 @@ TASK_EVENTS = INBOX / "tasks-events.jsonl" # 只作审计(append-only,⛔
TO_MAIN = INBOX / "TO-MAIN.md" # 投递给主会话的通知(投影 · 供人/AI 直接读)
TASK_STATES = ("pending", "running", "done", "blocked") # 需求台账四态:待执行/执行中/已完成/有阻碍
QUEUE_IDLE_MIN = 5 # 唤醒**限流桶**(分钟):同一状态下最多每 **5** 分钟发一次(2026-09-30 用户拍板:30→10→**5**);⛔ 触发条件是 supervise() 的**四条件**(不是计时器)
CHECK_IDLE_MIN = 10 # 🔴🔴 2026-10-07 用户拍板:**20 → 10 分钟**(原话「把检查程序的等待时间由 20分钟 改为 10分钟」)
CHECK_IDLE_MIN = 6 # 🔴🔴 2026-10-07 用户两度拍板:**20 → 10 → 6 分钟**
# 原话一:「把检查程序的等待时间由 20分钟 改为 10分钟」;原话二:「检查程序的等待时间改为6分钟」(当日 18:25)。
# 「检查程序」判"已经静默够久、可以建检查会话"的阈值(`queue-empty` 那条闸)。
# ⚠️ 改这个数必须**一起改三处**,否则文案与判据打架:
# ① 本常量(闸门 `maybe_spawn_check_agent()` + `--dry-run` 试算都读它);
@@ -4316,14 +4317,14 @@ CHECK_PROMPT_RESULT = (
+ _CHECK_TAIL_A + _CHECK_TAIL_B
)
# 【目标检查】—— 队列**空**且静默 >10 分钟时:核对目标完成状态(阈值=`CHECK_IDLE_MIN`,2026-10-07 由 20 改 10)
# 【目标检查】—— 队列**空**且静默 >6 分钟时:核对目标完成状态(阈值=`CHECK_IDLE_MIN`,2026-10-07 由 20 改 10)
CHECK_PROMPT_GOAL = (
"**【目标检查会话 · 第 %d 棒】**本轮只做这一件事:**核对目标完成状态**,**做完即停**。\n"
"\n"
"目标:%s\n"
"\n"
"## 一、为什么是你(🔴 触发条件已满足,不必重判)\n"
"**所有会话都已结束** + **队列为空** + **已静默 ≥10 分钟** + **本项目没有别的待执行排期**。\n"
"**所有会话都已结束** + **队列为空** + **已静默 ≥6 分钟** + **本项目没有别的待执行排期**。\n"
"\n"
"## 二、🔴 先读这五样,⛔ **只读这五个文件**,读完直接判断(⛔ 别到处找信息)\n"
"> ⚠️ **不要在本工作区里到处翻文件**:2026-10-03 实测,检查会话因无边界地找资料\n"
@@ -4569,7 +4570,7 @@ def artifacts_unverifiable() -> list:
def _last_progress_ts() -> float:
"""**最近一次有实质进展的时刻**(epoch 秒)—— 判「静默够久没」(阈值 `CHECK_IDLE_MIN`,现 10 分钟)的基准。
"""**最近一次有实质进展的时刻**(epoch 秒)—— 判「静默够久没」(阈值 `CHECK_IDLE_MIN`,现 6 分钟)的基准。
🔴 取「本工作区排期的 `updated_at` 最大值」;读不到 ⇒ 判**刚有过活动**(0)
⇒ ⛔ 不建(fail-safe:宁可少建,不误判成"已静默够久")。
@@ -20,7 +20,9 @@
🔴 三条设计判据(本需求的关键,⛔ 都别改)
──────────────────────────────────────────
1. **真相源在技能里**(不在本文件、也不在某个工作区):
`<技能库>/agent-operating-rules/references/回复排版-核心块.md`
`<技能库>/session-mechanism/references/03-回复排版-核心块.md`
⚠️ 2026-10-07 订正:原写 `<技能库>/agent-operating-rules/references/回复排版-核心块.md` ——
该技能**已于 2026-10-06 整包并入 `session-mechanism`**,旧路径已不存在(属「指针陈旧」族)。
⇒ 技能是**跨工作区**的 ⇒ 一次改动,**所有会话**都跟着变。
⛔ **绝不在本文件里写死规则文本** —— 写死就变成第二真相源,两边必然漂。
2. **每轮都注入,⛔ 不设冷却**:这条规矩的价值就在"紧贴用户消息、每轮重述"。
@@ -116,6 +116,7 @@ COOLDOWN = 300 # 同会话注入冷却(秒
# 起因(用户令逐字):「**重点问题是 会话接收到 调用执行会话完成目标 没有去调用对应技能去执行**」——
# 本钩子原来**只注入提醒**,AI 照不照做没人管;实测会话 `b232218f` 里用户连说三遍,一次都没拉起执行会话。
DISPATCH_REL = os.path.join('.workbuddy', '.dispatch-pending')
LOAD_REL = os.path.join('.workbuddy', '.load-pending')
_DISPATCH_WORDS = ('使用执行会话', '使用任务会话', '执行会话完成', '任务会话完成',
'调用执行会话', '调用任务会话', '派任务会话', '建任务会话')
@@ -156,13 +157,15 @@ TRIGGERS = (
# 命中词 → 该加载哪个技能(2026-09-22 加:从"只会推决策技能"扩为按命中词分流)
# 🔴 2026-10-02 修:本行原写 `dsh-decision-method`,而它**已于 2026-09-28 合并退役**
# ⇒ 注入文本让人去加载一个**不存在的技能**(找得到才怪 ⇒ 规则仍不进上下文;属静默失效族)。
# 现名 `dsh-decision`,两档全文:`references/00-决策方法论.md`(原 decision-method)
# / `references/01-功能优先协作协议.md`(原 feature-first,含 §5.1 结论骨架 + §5.4 排版硬约束)。
# 🔴 2026-10-07 再修:`dsh-decision` **已于 2026-10-06 整份删除**(实体早已搬入本包),
# 且上面那句写的路径编号也错了 ⇒ 两档实体**现在就在本包**:
# `references/04-决策方法论.md`(原 decision-method)
# / `references/02-功能优先协作协议.md`(原 feature-first,含 §5.1 结论骨架 + §5.4 排版硬约束)。
_LOAD_RULES = ('按规则来', '按规则做', '按作业规则', '遵守规则', '按规矩来',
'回复排版', '执行结果排版', '回复格式', '怎么回复', '排版')
# ── 分组 3 的词 → 加载 `session-mechanism`(2026-10-05 加)─────────────────────
# 与 `_LOAD_RULES`(→ agent-operating-rules)并列的第三条路由。
# 与 `_LOAD_RULES`(该技能已于 2026-10-06 整包并入本包)并列的第三条路由。
# 🔴 为什么单列一族:这三族**目标技能不同**,⛔ 不能混在一个 if 里 ——
# 命中"使用任务会话完成目标"的人,要的是**会话机制本体**(怎么建排期/怎么派活/
# 自决策白名单在 `references/02-功能优先协作协议.md`),⛔ 不是决策方法论。
@@ -171,6 +174,17 @@ _LOAD_SESSION = ('使用任务会话', '使用执行会话', '使用协作会话
'会话技能', '会话机制', '创建任务会话', '建任务会话',
'派任务会话', '任务会话', '执行会话', '协作会话', '多会话')
# 🔴🔴 2026-10-07 「点名族」= TRIGGERS 里**除派活族外的全部**(决策方法族 + 作业规则/排版族)。
# 用途:命中任意一个 ⇒ 写 `<ws>/.workbuddy/.load-pending` 待办,交给 `stop-dialog-guard.py`
# 做**收工前自检**(本会话到底有没有真的 `Skill` 加载过本技能)。
# 起因(用户令逐字):「**问题是 会话没有加载决策方法,之前的优化改动无效**」——
# 实测(`content_marketing_agent` 那条会话的闸门日志):
# `20:09:45 HIT … hits=决策方法` **注入确实发出了**,可会话**仍未去加载**;
# ⇒ 根因=**本钩子只"注入提醒",没有任何东西核对 Skill 到底调没调**(与"派活触发句"同族洞)。
# ⛔ **不许再抄一份词表** —— 从 `TRIGGERS` 里**减**出来,否则加词时两处必然漂移。
_LOAD_WORDS = tuple(w for w in TRIGGERS
if w not in _LOAD_SESSION and w not in _DISPATCH_WORDS)
def _read_stdin():
try:
@@ -270,6 +284,33 @@ def _mark_dispatch(workdir, sid, prompt, hits):
pass
def _is_load(prompt):
"""本轮是不是**点名族**(点名方法/规则/排版)。与 `_is_dispatch` 并列,⛔ 别各写一遍。"""
try:
seg = re.sub(r'\s+', '', prompt or '')
return any(w in seg for w in _LOAD_WORDS)
except Exception:
return False
def _mark_load(workdir, sid, prompt, hits):
"""命中「点名族」⇒ 写一条待办(TSV:会话 id / 时间戳 / 命中词),交给收工闸核。
判据(收工闸会核):**本会话有没有真的 `Skill` 加载过 `session-mechanism`**。
⚠️ 与 `_mark_dispatch` 同规矩:**写在冷却判定之前**(重复点名常被冷却吞掉,写完才有痕迹)。
⛔ 全程 fail-open。
"""
try:
if not _is_load(prompt):
return
with io.open(os.path.join(_norm_path(workdir), LOAD_REL),
'w', encoding='utf-8', newline='\n') as f:
f.write('%s\t%.3f\t%s\n' % (sid, time.time(), ','.join(hits)))
_log(workdir, 'LOAD_PENDING sid=%s hits=%s' % (sid[:8], ','.join(hits)))
except Exception:
pass
def _cooling(workdir, sid):
"""同一会话 COOLDOWN 秒内不重复注入。"""
try:
@@ -326,14 +367,15 @@ def main():
# 🔴 先留待办(⛔ 必须在冷却之前,见 `_mark_dispatch` 注释)
_mark_dispatch(workdir, sid, prompt, hits)
_mark_load(workdir, sid, prompt, hits)
# 🔴🔴 2026-10-07 两处改(用户问「有什么影响 该如何处理」时落的):
# ① **派活族显式触发句不受冷却** —— 那是最明确的一类"要动作",被 300 s 吞掉最亏。
# ① **派活族 + 点名族都不受冷却** —— 它们都是"用户明确点名"的一类,被 300 s 吞掉最亏。
# 实测:会话 `b232218f` 里 01:32:00 命中后,01:33:46(105 s,最明确的那句)
# 被冷却吞掉、**连日志都没有**。
# ② **被冷却跳过也要留一行**(`COOL_SKIP`)—— 否则"没命中"与"被吞"在日志上分不出来,
# 排查时只能靠状态文件时间戳旁证反推(上一次就是这么反推的)。
if _cooling(workdir, sid) and not _is_dispatch(prompt):
# 被冷却吞掉、**连日志都没有**;同型复现在 `content_marketing_agent` 的「决策方法」上
# (20:09:45 注入 → 20:09:54 重复点名被 COOL_SKIP)。
# ② **被冷却跳过也要留一行**(`COOL_SKIP`)—— 否则"没命中"与"被吞"在日志上分不出来。
if _cooling(workdir, sid) and not (_is_dispatch(prompt) or _is_load(prompt)):
_log(workdir, 'COOL_SKIP sid=%s hits=%s(冷却中,本轮不注入)'
% (sid[:8], ','.join(hits)))
return
@@ -710,6 +710,95 @@ def _dispatch_gate(payload):
return True
# ── 🔴🔴 2026-10-07 「点名了机制却没加载」闸 ────────────────────────────────────
# 起因(用户令逐字):「**问题是 会话没有加载决策方法,之前的优化改动无效**」。
# 实测(`content_marketing_agent` 那条会话的闸门日志):
# `20:09:45 HIT … hits=决策方法` ⇒ **注入确实发出了**;可会话**仍未去加载**
# ⇒ 根因=`skill-load-guard.py` **只"注入提醒",没有任何东西核对 `Skill` 到底调没调**。
# 形态:命中「点名族」时它写 `<ws>/.workbuddy/.load-pending`(会话/时间/命中词),
# 本闸在**收工前**核一次:**同会话 + 未过期 + 转录尾部里没有"真的 `Skill` 加载过本技能"** ⇒ 拦一次。
# ⛔ 三条安全底线:① 全程 fail-open(读不到转录 = 判"不确定" ⇒ **放行**);② 只拦一次(复用 10 分钟限流);
# ③ 待办 15 分钟过期。⛔ 只读转录**尾部**(1.5 MB),绝不整读(有 200 MB 级的转录)。
LOAD_REL = os.path.join('.workbuddy', '.load-pending')
LOAD_GRACE = 60 # 命中后给 60 秒去加载(⛔ 不拦"刚说完、还没轮到")
LOAD_EXPIRE = 900 # 待办最长存活 15 分钟
LOAD_TAIL_BYTES = 1500000 # 只看转录最后 1.5 MB(足够覆盖"刚被点名"的那几轮)
LOAD_REASON = (
'⛔ **收工前自检:用户点名了机制问题(决策方法/作业规则/排版),但这条会话没有真的加载过它。**\n'
'你点名的那些判据**不在你的记忆里**,就在技能包里 —— 先调用 Skill 工具加载 `session-mechanism`,再收工。\n'
'⚠️ **提醒 ≠ 加载**:本机制的钩子只会"注入一句提醒",⛔ 它拦不住你直接干活;\n'
' 实测栽过(`content_marketing_agent`):20:09:45 已注入「决策方法」提醒,会话却始终没去加载\n'
' ⇒ 用户原话「**会话没有加载决策方法,之前的优化改动无效**」。'
)
def _has_skill_load(tp):
"""转录尾部里有没有『**真的** Skill 加载过本技能』。
返回 True/False/**None**(读不到 ⇒ 不确定 ⇒ 调用方放行,fail-open)。
⚠️ 转录里技能名被**转义引号**包着:`"arguments":"{\\"skill\\": \\"session-mechanism\\"}"`
(⛔ 按普通引号匹配必然全不命中 —— 本项目 2026-10-07 实测踩过这个假阴性)。
"""
if not tp or not os.path.isfile(tp):
return None
try:
with open(tp, "rb") as f:
try:
f.seek(0, 2)
size = f.tell()
f.seek(max(0, size - LOAD_TAIL_BYTES))
except OSError:
f.seek(0)
raw = f.read().decode("utf-8", "replace")
except Exception:
return None
for pat in (r'\\"skill\\":\s*\\"session-mechanism\\"',
r'\\"command\\":\s*\\"session-mechanism\\"',
r'"skill":\s*"session-mechanism"',
r'"command":\s*"session-mechanism"'):
if re.search(pat, raw):
return True
return False
def _load_gate(payload):
"""见上面那段注释。返回 True = 已发出阻拦 ⇒ 调用方**直接 return**。"""
try:
ws = str(payload.get('cwd') or '') or \
(os.environ.get('CODEBUDDY_PROJECT_DIR') or os.environ.get('DSH_WORKSPACE') or '')
if not ws:
return False
p = os.path.join(_norm_path(ws), LOAD_REL)
if not os.path.isfile(p):
return False
parts = (io.open(p, encoding='utf-8').read().strip().split('\t') + ['', '', ''])[:3]
sid_m, ts_s, hits = parts
sid_now = str(payload.get('session_id') or '')
if not sid_now or sid_m != sid_now: # ⛔ 不是本会话的待办 ⇒ 不碰
return False
ts = float(ts_s or 0)
age = time.time() - ts
if age > LOAD_EXPIRE: # 过期 ⇒ 作废
os.remove(p)
return False
if age < LOAD_GRACE: # 刚说完、还没轮到 ⇒ 放行
return False
got = _has_skill_load(str(payload.get('transcript_path') or ''))
if got is None: # 读不到转录 ⇒ 不确定 ⇒ 放行
return False
if got: # 已经加载过 ⇒ 待办销账
os.remove(p)
return False
except Exception:
return False # ⛔ fail-open
root = _norm_path(ws)
if _rate_limited(root, sid_now): # 同一会话 10 分钟最多拦 1 次
return False
log(root, 'load-gate 命中:点名了机制但本会话未加载(hits=%s|待办 %.0fs)' % (hits, age))
_emit({'continue': False, 'reason': LOAD_REASON})
return True
def main():
raw = _read_stdin_text() # ⚠️ 必须走 buffer:`-E` 下 sys.stdin 是 cp936(见 _read_stdin_text 注释)
payload = None
@@ -729,6 +818,9 @@ def main():
# ⛔ 它只在"本工作区确实有本会话的派活待办"时才可能生效,其余情形一次都不触发。
if _dispatch_gate(payload):
return
# 🔴🔴 2026-10-07 「点名了机制却没加载」闸 —— 同样放在自作用域判定之前(同上理由)。
if _load_gate(payload):
return
if not _in_scope(tp): # 作用域外 → 放行
return
if os.environ.get('DSH_STOP_GUARD_OFF'): # 急停(环境变量)→ 放行
+29 -3
View File
@@ -797,7 +797,7 @@ def t_two_kinds_of_check_agent():
用户原话两条:
1、会话全结束(**还要加上 定时任务中没有本项目待执行的任务**)且**队列不为空** ⇒
**结果检查会话**去跟进任务会话执行结果,判断是否创建对应任务会话继续完成目标;
2、会话全结束(同上)且**队列为空 >10 分钟** ⇒ **目标检查会话**去核对目标完成状态,
2、会话全结束(同上)且**队列为空 >6 分钟** ⇒ **目标检查会话**去核对目标完成状态,
判断是建任务会话还是**修改目标状态**,并执行。
⇒ 改前**两套 prompt 完全共用一个模板** ⇒ 结果检查会话拿到了"改目标状态"的出口(越权),
@@ -7358,12 +7358,38 @@ def t_skillguard_cooldown():
src = (HERE / "hooks" / "skill-load-guard.py").read_text(encoding="utf-8", errors="replace")
return [
("有共用的 `_is_dispatch()`(⛔ 别在两处各写一遍判定)", "def _is_dispatch(" in src),
("冷却旁路在:`_cooling(...) and not _is_dispatch(...)`",
"_cooling(workdir, sid) and not _is_dispatch(prompt)" in src),
("冷却旁路在:`_cooling(...) and not (_is_dispatch(...) or _is_load(...))`",
"_cooling(workdir, sid) and not (_is_dispatch(prompt) or _is_load(prompt))" in src),
("被跳过写 `COOL_SKIP`(⛔ 否则「没命中」与「被吞」分不出来)",
"COOL_SKIP" in src),
]
@case("⛔ 点名了机制却没加载:注入之外还要有「收工前自检」抓手")
def t_load_gate():
"""🔴 2026-10-07 立(用户令逐字:「**问题是 会话没有加载决策方法,之前的优化改动无效**」)。
实测(`content_marketing_agent` 的闸门日志):`20:09:45 HIT … hits=决策方法` ⇒ **注入确实发出**,
可会话**仍未去加载** ⇒ 根因=注入只是"劝",**没有任何东西核对 `Skill` 到底调没调**。
两处处置:① 点名族命中即写 `<ws>/.workbuddy/.load-pending`;
② `stop-dialog-guard.py` 收工前核「转录尾部里有没有**真的** Skill 加载过本技能」。
⚠️ 源码级判据(跑真钩子会动到真工作区的状态,⛔ 不适合进回归套)。
"""
a = (HERE / "hooks" / "skill-load-guard.py").read_text(encoding="utf-8", errors="replace")
b = (HERE / "hooks" / "stop-dialog-guard.py").read_text(encoding="utf-8", errors="replace")
return [
("点名族词表从 `TRIGGERS` **减**出来(⛔ 不另抄一份:`_LOAD_WORDS = tuple(...)`)",
"_LOAD_WORDS = tuple(w for w in TRIGGERS" in a),
("命中即写待办 `LOAD_PENDING`(⛔ 写在冷却之前)",
"LOAD_PENDING" in a and "_mark_load(" in a),
("收工闸在:`_load_gate(payload)` 已接进 main,且在自作用域判定之前",
"def _load_gate(" in b and "if _load_gate(payload):" in b),
("判据认**转义引号**那种编码(⛔ 普通引号匹配 = 恒假阴性)",
'\\\\"skill\\\\":\\s*\\\\"session-mechanism' in b),
("只读转录**尾部**(⛔ 不整读,有 200 MB 级转录)", "LOAD_TAIL_BYTES" in b),
("读不到转录 ⇒ 判「不确定」放行(fail-open)", "got is None" in b),
]
if __name__ == "__main__":
sys.exit(main())