@xdxer/dingtalk-agent 0.1.5-beta.8 → 0.1.5-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.en.md +1 -0
  3. package/README.md +1 -0
  4. package/dist/bin/dingtalk-agent.js +115 -14
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/agent-audit.js +108 -9
  7. package/dist/src/agent-audit.js.map +1 -1
  8. package/dist/src/development-workspace.js +37 -1
  9. package/dist/src/development-workspace.js.map +1 -1
  10. package/dist/src/dws.js +9 -0
  11. package/dist/src/dws.js.map +1 -1
  12. package/dist/src/multica-deploy.js +347 -105
  13. package/dist/src/multica-deploy.js.map +1 -1
  14. package/dist/src/multica-provider.js +151 -27
  15. package/dist/src/multica-provider.js.map +1 -1
  16. package/dist/src/multica-runtime-vocabulary.js +110 -0
  17. package/dist/src/multica-runtime-vocabulary.js.map +1 -0
  18. package/dist/src/promotion.js +2 -1
  19. package/dist/src/promotion.js.map +1 -1
  20. package/dist/src/schedule-plan.js +192 -24
  21. package/dist/src/schedule-plan.js.map +1 -1
  22. package/docs/AGENT-IN-PRODUCTION.md +255 -0
  23. package/docs/ARCHITECTURE.md +5 -1
  24. package/docs/PLATFORM-GUARDRAILS.md +188 -0
  25. package/docs/schemas/multica-deployment-plan.schema.json +3 -1
  26. package/docs/schemas/multica-deployment-receipt.schema.json +15 -1
  27. package/docs/schemas/multica-deployment-status.schema.json +6 -2
  28. package/docs/schemas/multica-workspace-inspection.schema.json +16 -0
  29. package/docs/schemas/multica-workspace-status.schema.json +2 -0
  30. package/docs/schemas/workspace-scaffold.schema.json +1 -0
  31. package/lab/project-workspace/fake-multica-provider.mjs +97 -7
  32. package/lab/project-workspace/multica-deploy.fixture.json +2 -2
  33. package/lab/project-workspace/multica-readonly.fixture.json +2 -2
  34. package/lab/project-workspace/project.fixture.json +1 -1
  35. package/lab/robot-eval/suite.json +1 -1
  36. package/package.json +5 -2
  37. package/skills/core/dta-agent-compose/SKILL.md +1 -1
  38. package/skills/core/dta-agent-compose/references/drive-and-schedules.md +22 -2
  39. package/skills/core/dta-basic-behavior/SKILL.md +1 -1
  40. package/skills/core/dta-basic-behavior/references/memory-and-evolution.md +1 -1
  41. package/skills/core/dta-people-group-memory/SKILL.md +4 -4
  42. package/skills/core/dta-people-group-memory/references/model.md +1 -1
  43. package/skills/platforms/multica-dingtalk/PLATFORM.md +2 -2
  44. package/skills/platforms/multica-dingtalk/dta-deploy-multica/SKILL.md +6 -5
  45. package/skills/platforms/multica-dingtalk/dta-deploy-multica/references/multica-deployment-contract.md +14 -4
  46. package/skills/platforms/multica-dingtalk/dta-ops-multica/SKILL.md +33 -7
  47. package/skills/platforms/multica-dingtalk/dta-ops-multica/scripts/multica_ext.py +118 -12
@@ -131,7 +131,7 @@ Host 原生发现目录不是发布源。OpenCode 的 `.agents/skills/`、Claude
131
131
  - **外发类节律的留痕在动作之后,所以它没有锁。** 两个节律落在同一分钟并发唤醒,就会把同一条消息发两遍给第三方。多节律拆分必须错开触发时刻。
132
132
  - **Agent 不自建定时器、不自改节律。** 节律是可审计的部署产物;在线 Run 只能把新节律整理成候选交给 operator,不能自己配上再宣称配好了——这与 §1「在线 Run 不能热改身份、权限或已启用 Skill」是同一条边界的延伸。
133
133
 
134
- **本仓库在这一层拥有什么**:不拥有 cron,拥有三件事——装配时**声明**需要哪些节律与是否允许 Agent 自主排程(`dta-agent-compose`);部署到具体平台时**校验**该平台是否真的提供这些能力、不提供就 fail closed(平台 Skill 包 + `agent-platform` 注册表);以及 Agent 侧被定时驱动时必须满足的**行为合同**(`dta-basic-behavior`)。**平台之间能力不同是常态**——同一个 Agent 的节律声明可以不变,能不能被满足由目标平台回答。
134
+ **本仓库在这一层拥有什么**:不拥有 cron,拥有三件事——装配时**声明**需要哪些节律与是否允许 Agent 自主排程(`dta-agent-compose`);部署到具体平台时**校验**该平台是否真的提供这些能力、不提供就 fail closed(平台 Skill 包 + `agent-platform` 注册表);以及 Agent 侧被定时驱动时必须满足的**行为合同**(`dta-basic-behavior`)。**平台之间能力不同是常态**——同一个 Agent 的节律声明可以不变,能不能被满足由目标平台回答。`dta schedule show` 还会静态检两条与平台无关的不变量:**同刻并发**(按真实 occurrence 枚举判,判不准的当撞车挡住)和**缺完成判据**(声明了 cron 却没写数据派生 `completion` 的记成缺口)。
135
135
 
136
136
  ## 4. 谁拦住什么:Skill 劝,CLI 拦,DWS 做
137
137
 
@@ -292,6 +292,8 @@ dingtalk-doc:<node-or-url> DWS 拉取在线文档的只读快照
292
292
 
293
293
  **不做**:`observe` 产物不进 Git(团队间无法共享);不把线上数据写回 Prompt/Skill/suite;在线 Run 不得自动扩大身份、权限或启用 Skill。
294
294
 
295
+ > 计量这层 dta 不造,但**该怎么自建**有可抄的范式——运行记录账本(每 Run 一行)+ 费率折算 + 体检元节律 + L0–L4 达成度判据,见 [AGENT-IN-PRODUCTION.md](AGENT-IN-PRODUCTION.md)「观测这一层,dta 不替你造——但有可抄的范式」。
296
+
295
297
  ---
296
298
 
297
299
  Agent Project 与 Development Workspace 的基数关系:
@@ -350,6 +352,7 @@ trusted W3 inspection + local Definition/Skill tree
350
352
 
351
353
  | 想做什么 | 去哪 |
352
354
  |---|---|
355
+ | **接手一个已经在跑的 Agent**(运行视角:一天怎么转、坏起来什么样、去哪看) | [Agent in Production](AGENT-IN-PRODUCTION.md) |
353
356
  | 安装、升级与排障 | [Installation](INSTALLATION.md) |
354
357
  | 评测方法、证据分层与晋级门禁 | [Self-test](SELF-TEST.md) |
355
358
  | 创建或审计一个 Agent | [Compose Skill](../skills/core/dta-agent-compose/SKILL.md) |
@@ -357,6 +360,7 @@ trusted W3 inspection + local Definition/Skill tree
357
360
  | 公共行为协议本身 | [Basic Behavior Skill](../skills/core/dta-basic-behavior/SKILL.md) |
358
361
  | DWS 与钉钉 API 的实战缺陷 | [DWS Field Notes](DWS-FIELD-NOTES.md) |
359
362
  | 借鉴了哪些开源项目、各自取了什么 | [Prior Art](PRIOR-ART.md) |
363
+ | 真实事故该由平台哪一层兜(护栏清单) | [Platform Guardrails](PLATFORM-GUARDRAILS.md) |
360
364
  | 尚未完成的工作 | [roadmap](roadmap/) |
361
365
 
362
366
  在本仓库工作的 Coding Agent 请先读 [AGENTS.md](../AGENTS.md),验证命令以那里为准。
@@ -0,0 +1,188 @@
1
+ # dta 平台护栏清单:哪些坑必须由工具层兜住
2
+
3
+ > 2026-07-24 一夜盯一个真实生产 Agent(FDE 教练)跑,踩出六个坑。本文回答一个问题:
4
+ > **每个坑该由谁兜——原语 / CLI 闸门 / Skill 纪律 / 文档?**
5
+ >
6
+ > 一条贯穿全文的判据:**凡是「dta 文档已经写过、业务方仍然逐个踩」的坑,就是该从文档下沉到 CLI 的信号。**
7
+ >
8
+ > ⚠️ **诚实说清本文的定位(经两轮 review 收窄)**:本文是**事故复盘 + 该由哪层兜的分析**,
9
+ > 不是"已封死"的宣告。本次 CR 真正落到**代码**的只有【节律静态检查(同刻并发按真实 occurrence 判、
10
+ > 完成判据存在性、计量标注)】;【AI 表格静默截断】和【完成判据分叉】这两个最核心的坑,
11
+ > 本次只做了**文档 + Skill 纪律 + 存在性检查**,**没有**做到类型层/注册表层的硬封印——那需要
12
+ > `completeness: proven\|unproven` 原语、`predicateRef` 注册表、no-op Receipt 合同,均已列为 follow-up issue。
13
+ > 换句话说:**别把本文当"每个 Agent 都自动免疫了",它是"我们知道坑在哪、部分下沉了、其余明确记账"。**
14
+ >
15
+ > 证据等级:**【实测】**本机只读复跑、**【读码】**对到 file:line、**【下界】**单读表格数可能偏小。
16
+
17
+ ---
18
+
19
+ ## 0. dta 今天的护栏落在哪三层
20
+
21
+ `README.md` / `AGENTS.md` 的口径是 **Skill 劝 · CLI 拦 · DWS 做**。【读码】实际分布:
22
+
23
+ | 层 | 位置 | 今天真拦住了什么 |
24
+ |---|---|---|
25
+ | Skill 劝 | `skills/core/dta-basic-behavior/SKILL.md` | 响应资格、意图、`nothing-to-save` 合法性、没 Receipt 不用完成措辞 |
26
+ | CLI 拦 | `src/dws.ts` 六条纪律、`records()` fail-closed、`src/actions.ts` 发消息后独立回读、`src/memory/operational.ts` 按 recordId 回读 + 命中多行拒绝 | 目标冻结、四原语、写后回读、幂等 |
27
+ | DWS 做 | `src/dws.ts`(全仓唯一 spawn `dws` 处) | — |
28
+ | **文档层**(既不劝也不拦) | `DWS-FIELD-NOTES.md`、`AGENT-IN-PRODUCTION.md` | **什么都拦不住** |
29
+
30
+ **最硬的一个判据**:`AGENT-IN-PRODUCTION.md` 早就写了"写后回读读的是同一个错误对象"、"同一个语义只留一个谓词"、"改 mode 就是在改计量口径"——**文档劝过了,业务方还是踩了**。这三条都该下沉。
31
+
32
+ ---
33
+
34
+ ## 1. 坑一 · AI 表格静默截断 → **必须是原语,不能只写进 Skill**
35
+
36
+ **【实测】** 同一 `--all --page-limit 0` 全表连读 6 次得 `400/300/1300/500/1800/700`,
37
+ pages `5/4/14/6/19/8`,**6 次全 `hasMore=false`、`partial` 键根本不存在**。单次最低 300 是并集下界的 17%。
38
+ 成功信封的全部键只有 `records/hasMore/pages`——**没有 `totalCount`,服务端不提供任何完整性凭据**。
39
+
40
+ **【读码】dta 今天的状态**:
41
+
42
+ - `src/dws.ts records()` 已 fail-closed 检查 `hasMore/partial`——**和 FDE 引擎 `query()` 同一道闸门、同一个漏洞**。平台谎报 `hasMore:false`,两道闸门同时被绕过。
43
+ - **更糟**:`src/dws.ts queryAiTableRecords()`(dta 自己的 Operational Memory 读路径)**不带 `--all`、不带 `--page-limit 0`,只 `--limit 100`**——比 FDE 引擎还弱一档。
44
+ - `DWS-FIELD-NOTES.md` 现有对策「`--all` 必配 `--page-limit 0`」**今晚实测证伪**:加了照样截断。
45
+
46
+ **为什么不能留给业务方**(不是偏好):
47
+ 1. 这是**介质属性**不是岗位知识。FDE 已严格按文档写对参数,仍被骗 → 这题在 Skill/文档层**无解**。
48
+ 2. 判据需要**跨调用状态**(多读、计数账本、水位),Skill 是无状态语义层。`AGENTS.md` 自己就写「幂等、回读、硬拒绝 → CLI/Gate,不得只写在 Prompt」。
49
+ 3. `dta-people-group-memory/references/storage-port.md` 自立原则「模型层不知道『截断』这个词」——既然模型层不该知道,就必须有人替它知道。
50
+
51
+ **该做什么**:
52
+
53
+ | 落层 | 改动 |
54
+ |---|---|
55
+ | **原语** `src/dws.ts` | `records()` 不再把 `hasMore=false` 当完整性证据。返回 `{records, completeness: 'proven'\|'unproven'}`,全量扫一律 `unproven`,调用方**编译期**绕不过去 |
56
+ | **原语** `src/dws.ts` | 把混为一谈的读拆成三个、在类型上分开可靠性:`getByRecordIds()`(≤100 定点,【实测】7/8 正确且失败 fail-loud)/`queryByKey(filters)`(服务端筛,**仍 unproven**)/`scanAll()`(**标记不可信**) |
57
+ | **原语** `src/memory/` | **count ledger**:写侧每次 `+1` 到 meta 表,读侧全量枚举时两数相比——**唯一**能把"读全了"变可证明的机制(服务端无 `totalCount` oracle)。诚实边界:只覆盖 dta 自己写的行,给"不少于";对单写者表等号成立 |
58
+ | **CLI 闸门** | 连读 K 次取 max **只提高发现截断的概率、不能证明完整**(【实测】并集到第 5 次才碰到 1800,仍是下界)。所以 K-读只作 `unproven→suspected-truncated` 降级信号,**不许当完整性证明**——写进代码注释,否则会被误当对策 |
59
+ | **文档** | 更正 `DWS-FIELD-NOTES.md`:`--all --page-limit 0` **不足以**防截断,附今晚数据 |
60
+ | **回归** | `evals/` 加 core 合同:fake dws stub 返回"页对齐前缀 + `hasMore:false`",断言 CLI **拒绝**当完整 |
61
+
62
+ **⚠️ 已推翻的一个说法**(复核纠正):曾以为"`--all` 的空返回有一种 fail-loud 安全形态"。**错**——
63
+ `--all` 路径上的空返回是 `{"hasMore":false,"pages":1,"records":null}`,`records` 键在、值为 `null`,
64
+ 两道闸门(`dws.ts` 的 `'records' in x` 和 `records || []`)都**静默**放过。`--all` 路径上**两种 0 都不安全**。
65
+
66
+ ---
67
+
68
+ ## 2. 坑二 · 写后回读读到同一个错对象 → **Action Gate 该管,今天管不到**
69
+
70
+ **背景事故**:FDE 的 `write-ai` 盲写 `rows[0]`、回读也取 `rows[0]` → 永远自洽 → 写错行永远发现不了。
71
+ 根因是「业务主键不唯一 + 定位靠读」,回读只是发现手段。
72
+
73
+ **【读码】dta 今天**:这条合同**已存在**——`src/memory/operational.ts` 按 recordId 独立回读、key 命中多行直接拒绝、
74
+ 创建后没拿到 recordId 就按 key 回查。`ARCHITECTURE.md` 还拿它当"为什么这必须是 CLI 命令"的范例。
75
+ **但覆盖面几乎为零**:只作用于 `dta memory operational upsert` 一条命令;进 Action Gate 的 typed 副作用
76
+ **只有回复与表情两种**,AI 表格写入根本不进 Gate。FDE 的 write-ai 是业务方自己写的,所以这条事故 dta 一点忙没帮上。
77
+
78
+ **该落哪**:
79
+ - **CLI 闸门(新增窄命令)**:按 `ARCHITECTURE.md` 的六条判据,业务表写入命中第 3(幂等且不能盲重试)+ 第 4(写后必须独立回读)→ **符合进 CLI 的标准**。但不做通用 CRUD(`ARCHITECTURE.md` 明禁复制 DWS 命令面)。折中:
80
+ `dta aitable upsert --key-field <f> --key <v> --required-nonempty <f,…>`,只做四件——按业务键定位 → 键唯一性断言 → 写 → 按 recordId 回读,其余回 DWS。
81
+ - **为什么不留给业务方**:这个错误 **100% 静默且自洽**(写 rows[0]、回读也取 rows[0] → 永远自证成功),数据上看不出来,只有 backlog 三十小时不降才反推得到。"结构性不可观测"的错误定义上就不该由每个业务方各自发现一次。
82
+
83
+ **⚠️ 一个未复现的推测**(复核纠正):曾推测 dta 自己的 `operational.ts` 也有同族"截断到 0 → 判不存在 → create 重复"的洞。
84
+ **在真实代码路径上未复现**:`operational.ts` 走 `--limit 100`(非 `--all`),【实测】连读 30 次里 2 次空返回都是
85
+ 带 `error` 的信封 → 进 `dws.ts` 的 `error.code` 判断 → **fail-loud(die)**,不是静默造重复。
86
+ 所以 dta 内核这条路是安全的——但**新增的 `dta aitable upsert` 若走 `--all` 定位就会引入这个洞**,设计时必须用 `getByRecordIds`/`queryByKey` 而非 `scanAll`。
87
+
88
+ ---
89
+
90
+ ## 3. 坑三 · 两个完成判据分叉 → **注册表 + 唯一入口,不是静态检查**
91
+
92
+ **背景事故**:FDE 的 `duty_runlog()` 完成判据只看答案原文、`_settled()` 还要问题原话,两谓词分叉 →
93
+ 心跳先问 done、done 就不跑补录 → 补救路径 30 小时没被触发、没有任何一层报警。
94
+
95
+ **【读码】现状**:`AGENT-IN-PRODUCTION.md` 已把结论写成「同一个语义只留一个谓词,两个调用方共用」;
96
+ `AGENTS.md` 有「每条不变量只选一个 owner」的表,但管的是**内容写在哪个文件**、不是**代码谓词的唯一性**。**没有机器检查。**
97
+
98
+ **能做**:
99
+ - **装配层**:`dta-agent-compose` 的节律声明里,每条 schedule 带 `doneWhen: predicate:<id>`。
100
+ - **CLI 闸门**:`dta schedule plan` 校验每个 predicate 在 Agent 侧可执行——`src/schedule-plan.ts` 已有
101
+ "平台不提供该能力就 supported:false 记缺口、绝不静默降级"的 fail-closed 骨架,**直接复用到 predicate**。
102
+ - **回归**:同一 duty 的两个调用点必须解析到同一 predicate id,不同就红。
103
+
104
+ **不能做(诚实标注)**:跨语言(FDE 引擎是 Python)**没法静态证明两个函数语义相同**。约束形式只能是「注册 + 唯一入口 + 谁绕过谁没 receipt」,不是分析。
105
+
106
+ ---
107
+
108
+ ## 4. 坑四 · 解析失败被折叠成"内容为空" → **解析失败必须留痕,拦在写入前**
109
+
110
+ **背景事故**:平台改中继模板标题 → FDE 的 `_RE_ASKED` 100% 不命中 → 抠不到问题原话 → **静默写空**。
111
+ 通用形状 = **解析失败与"本来就空"不可区分**,和 `DWS-FIELD-NOTES.md` 的「读错层级 → undefined → 当成空」同族。
112
+
113
+ **该落哪**:
114
+ - **原语/Skill 纪律**:凡"从外部信封解析出字段再写入"的路径,解析失败必须产出一个**可区分于空**的标记(如 `parse-miss`),
115
+ 而不是落一个空字符串。写入前若关键字段是 `parse-miss` → 记欠账、不写、可报警。
116
+ - **对应 `dta-people-group-memory` 的第 3 条不变式「读失败 ≠ 不存在」**——但那条现在只针对**读**,
117
+ 要扩到**解析**:解析失败 ≠ 字段为空。
118
+
119
+ ---
120
+
121
+ ## 5. 坑五 · ephemeral 不产 issue → 不可计量 → **观测缺口,ARCHITECTURE §8 自己承认**
122
+
123
+ **【读码】** 常驻节律多是 `ephemeral`(`run_only`),平台不为它建 issue → `/api/tasks/{id}/usage` 拿不到 →
124
+ 心跳的真实 Token 拿不到。`ARCHITECTURE.md §8` 自己说观测是"最薄的一段"。
125
+ FDE 侧实测:可见 IM 成本只是冰山一角,心跳那半平台不记账。
126
+
127
+ **该落哪**:
128
+ - **文档已对(`AGENT-IN-PRODUCTION.md` §6 写了盲区),但 dta 无计量能力**。这条不该硬造——
129
+ 但 dta 至少该在 `dta schedule plan` 输出里**标注每条节律的 `mode` 与可计量性**,让 operator 在排期时就知道哪块看不见,
130
+ 而不是等业务方各自发现。「改 `mode` 就是改计量口径」这句该从文档变成 `schedule plan` 的一条显式告警。
131
+
132
+ ---
133
+
134
+ ## 6. 坑六 · "无操作"不可观测 → **`nothing-to-save` 也要留痕**
135
+
136
+ **背景事故**:FDE 的单聊沉淀判 `nothing-to-save` 是合法结果,但**判了 nothing-to-save 和压根没跑,在外部完全不可区分**——
137
+ 沉淀这步没跑不留痕、跑了判空也不留痕。结果是"这个能力到底在不在工作"永远回答不了。
138
+
139
+ **该落哪**:
140
+ - **Skill 纪律(`dta-basic-behavior` + `dta-people-group-memory`)**:把「无操作也是一次可观测的操作」立成通用合同——
141
+ 判 `nothing-to-save` / `skip` / `no-op` 时**留一行结构化痕迹**(带原因),而不是静默返回。
142
+ - 这条和 `dta-basic-behavior` 的"没 Receipt 不用完成措辞"是一体两面:完成要有证据,**不完成也要有证据**。
143
+
144
+ ---
145
+
146
+ ## 7. 对 dta-people-group-memory 四条不变式的复核
147
+
148
+ 这个 Skill(v0.3.0)刚进默认套装,四条不变式在**静默截断**下逐条评估:
149
+
150
+ | 不变式 | 扛得住截断吗 | 说明 |
151
+ |---|---|---|
152
+ | ① 原始层只增不删,派生层可重建 | ✅ | 与截断无关 |
153
+ | ② 写后必回读,success 不算数 | ⚠️ **不够** | 回读若用 `scanAll` 会被截断骗;必须限定为**按 recordId 回读**(坑二)。「回读」要在契约里写死是哪种读 |
154
+ | ③ **读失败 ≠ 不存在** | 🔴 **最需要加强** | 现在只针对**报错的读**。而截断读**不报错**——它成功返回一个偏小的前缀。这条不变式的前提"读失败会被识别"在 `--all` 上**不成立**。必须补一句:**"成功但可能不完整"也算读失败**,全量枚举一律 `unproven`,绝不据此判"不存在"或新建重复 |
155
+ | ④ 先写事实、后推水位 | ✅ | 与截断无关,且正确 |
156
+
157
+ **核心修订**:第 ③ 条要从「读失败 ≠ 不存在」升级为「**读不完整 ≠ 不存在**,而全量枚举永远无法自证完整」。
158
+ 这正是坑一 G1 的原语要兜的——把"完整性未证明"变成一个类型层的显式状态,Skill 的第 ③ 条才有东西可依靠。
159
+
160
+ ---
161
+
162
+ ## 附:落地状态(2026-07-24 本次 CR,两轮 review 后收窄)
163
+
164
+ 分三档如实标:**代码闸门**(机器强制)、**文档/Skill**(劝、不强制)、**follow-up**(真封印,本次没做)。
165
+
166
+ **已落代码闸门(机器强制、CI 守)**
167
+ | 坑 | 落点 | 状态 |
168
+ |---|---|---|
169
+ | 坑三 判据缺失 | `schedule-plan.ts` 校验每条节律有 `completion`,缺失 `schedule show/plan` **exit 2**(json+人类都设,不 fail-open) | ✅ 代码 |
170
+ | 坑五 计量盲区 | `schedule-plan.ts` 每条节律标 `metered`(ephemeral 不记账显式提示) | ✅ 代码 |
171
+ | 撞车误报/漏报 | `firesCollide` 改**按真实 occurrence 判**(robfig dom/dow OR、名字字面量、闰年枚举);判不准的(跨时区/扩展语法)进 `unverifiedPairs`,**和确认撞车一样 exit 2 挡住**——`0 9 * * *`@Asia/Shanghai 与 `0 1 * * *`@UTC 是同一时刻,证不出不撞就不许部署 | ✅ 代码 + 回归 |
172
+
173
+ **已落文档/Skill 纪律(劝,不强制——这几条本次【没有】做成硬封印)**
174
+ | 坑 | 落点 | ⚠️ 局限 |
175
+ |---|---|---|
176
+ | 坑一 截断 | `DWS-FIELD-NOTES` 重写 + `dws.ts` 注释 + people-memory 不变式③「读不完整≠不存在」 | `records()` 仍会接受 `hasMore:false` 的部分前缀、把 `records:null` 折成 `[]`——**没有 completeness 类型封印**(见 follow-up) |
177
+ | 坑三 判据分叉 | `completion` 存在性检查 | **只证明"写了一句话"**,不保证运行时两个谓词引用同一个——原事故那种"各有一套判据"仍可能复发(见 follow-up) |
178
+ | 坑四 解析失败折叠成空 | `DWS-FIELD-NOTES`「解析失败≠空」+ `parse-miss` | 是文案,没有强制的 typed action |
179
+ | 坑六 无操作不可观测 | `dta-basic-behavior`:`nothing-to-save` 也要留痕 | 是文案,**没有** host 签发的 no-op Receipt 合同(见 follow-up) |
180
+ | 不变式② | people-memory:写后按 recordId 回读 | ✅ 纪律清晰 |
181
+
182
+ **明确 follow-up(真封印,本次没做,各开 issue)**
183
+ 1. `completeness: proven\|unproven` 读原语 + 页对齐前缀 fixture——把"未证明完整"变类型层显式状态。
184
+ 2. `predicateRef` 注册表 + 运行时引用校验——让"完成判据分叉"在编译期/运行期查得出来。
185
+ 3. host 签发的 no-op / nothing-to-save Receipt 合同——让"跑过判空"可与"没跑"区分。
186
+ 4. cron 同刻判定复用 provider(robfig)occurrence 预览,替掉本地模拟。
187
+
188
+ **明确没做(避免一味增加)**:不新增 `dta aitable upsert` CLI 命令面(`ARCHITECTURE.md` 明禁复制 DWS 命令面,且无现有调用方);`records:null` 不硬判死(dws 版本相关,硬判深耦合);计量不进 core。
@@ -40,9 +40,11 @@
40
40
  }
41
41
  },
42
42
  "definitionHash": { "$ref": "#/$defs/hash" },
43
- "instructionsHash": { "$ref": "#/$defs/hash" }
43
+ "instructionsHash": { "$ref": "#/$defs/hash" },
44
+ "previousRuntimeId": { "type": "string" }
44
45
  }
45
46
  },
47
+ "rebindRuntime": { "type": "boolean" },
46
48
  "operations": { "type": "array", "items": { "type": "string", "minLength": 1 } },
47
49
  "writeBudget": {
48
50
  "type": "object",
@@ -59,7 +59,13 @@
59
59
  "loadedSkills": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/name" } },
60
60
  "toolTracePassed": { "type": "boolean" }, "responsePassed": { "type": "boolean" },
61
61
  "responseHash": { "type": "string" },
62
- "failures": { "type": "array", "items": { "type": "string" } }
62
+ "failures": { "type": "array", "items": { "type": "string" } },
63
+ "runtimeProvider": { "type": "string" },
64
+ "vocabularyIds": { "type": "array", "items": { "type": "string" } },
65
+ "toolNames": { "type": "array", "items": { "type": "string" } },
66
+ "unrecognizedTools": { "type": "array", "items": { "type": "string" } },
67
+ "classification": { "enum": ["", "skills-not-visible"] },
68
+ "failureDetails": { "$ref": "multica-workspace-inspection.schema.json#/$defs/failureDetails" }
63
69
  }
64
70
  },
65
71
  "rollback": {
@@ -72,6 +78,14 @@
72
78
  }
73
79
  },
74
80
  "failures": { "type": "array", "items": { "type": "string" } },
81
+ "failureDetails": { "$ref": "multica-workspace-inspection.schema.json#/$defs/failureDetails" },
82
+ "rebind": {
83
+ "type": "object", "additionalProperties": false,
84
+ "required": ["fromRuntimeId", "toRuntimeId"],
85
+ "properties": {
86
+ "fromRuntimeId": { "type": "string" }, "toRuntimeId": { "type": "string" }
87
+ }
88
+ },
75
89
  "evidencePath": { "type": "string", "minLength": 1 },
76
90
  "remoteRead": { "const": true }, "remoteWrite": { "type": "boolean" },
77
91
  "triggerWrite": { "const": false },
@@ -14,10 +14,14 @@
14
14
  "passed": { "type": "boolean" }, "providerReady": { "type": "boolean" },
15
15
  "operationIntegrity": { "type": "boolean" }, "receiptIntegrity": { "type": "boolean" },
16
16
  "reconciled": { "type": "boolean" },
17
+ "receiptFrozen": { "type": "boolean" },
18
+ "smokeStale": { "type": "boolean" },
19
+ "nextAction": { "type": "string" },
17
20
  "failures": { "type": "array", "items": { "type": "string" } },
18
21
  "receipt": { "oneOf": [{ "type": "null" }, { "$ref": "multica-deployment-receipt.schema.json" }] },
19
- "remoteRead": { "type": "boolean" }, "remoteWrite": { "const": false },
20
- "triggerWrite": { "const": false }, "sideEffect": { "enum": [false, "multica-read"] },
22
+ "remoteRead": { "type": "boolean" }, "remoteWrite": { "type": "boolean" },
23
+ "triggerWrite": { "const": false },
24
+ "sideEffect": { "enum": [false, "multica-read", "multica-read-write"] },
21
25
  "dingtalkSideEffect": { "const": false }
22
26
  }
23
27
  }
@@ -36,6 +36,8 @@
36
36
  "resources": { "$ref": "#/$defs/resources" },
37
37
  "diff": { "type": "array", "items": { "type": "string" } },
38
38
  "failures": { "type": "array", "items": { "type": "string" } },
39
+ "failureDetails": { "$ref": "#/$defs/failureDetails" },
40
+ "envSanitized": { "type": "array", "items": { "type": "string" } },
39
41
  "calls": {
40
42
  "type": "array",
41
43
  "items": {
@@ -60,6 +62,20 @@
60
62
  },
61
63
  "$defs": {
62
64
  "hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
65
+ "failureDetails": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": ["code"],
71
+ "properties": {
72
+ "code": { "type": "string", "minLength": 1 },
73
+ "expected": { "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] },
74
+ "actual": { "oneOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] },
75
+ "hint": { "type": "string" }
76
+ }
77
+ }
78
+ },
63
79
  "resources": {
64
80
  "type": "object",
65
81
  "additionalProperties": false,
@@ -26,6 +26,8 @@
26
26
  "observedHash": { "type": "string" },
27
27
  "diff": { "type": "array", "items": { "type": "string" } },
28
28
  "failures": { "type": "array", "items": { "type": "string" } },
29
+ "failureDetails": { "$ref": "multica-workspace-inspection.schema.json#/$defs/failureDetails" },
30
+ "observedAgentRuntimeId": { "type": "string" },
29
31
  "remoteRead": { "const": false },
30
32
  "remoteWrite": { "const": false },
31
33
  "sideEffect": { "const": false },
@@ -19,6 +19,7 @@
19
19
  "alreadyDeclared": { "type": "boolean" },
20
20
  "manifest": { "const": "dingtalk-agent.json" },
21
21
  "block": { "type": "object" },
22
+ "createdPaths": { "type": "array", "items": { "type": "string", "minLength": 1 } },
22
23
  "envRefs": {
23
24
  "type": "object",
24
25
  "additionalProperties": false,
@@ -82,7 +82,9 @@ if (commandKey === 'config show') {
82
82
  process.exit(0)
83
83
  }
84
84
  if (commandKey === 'auth status') {
85
- if (!state.authenticated) {
85
+ // 复刻 multica Go CLI 的保留语义:环境里存在 MULTICA_AGENT_ID 即切 Agent 身份鉴权,
86
+ // 用户 token 判无效(B11)。dta 剔除该变量后本分支不应再命中。
87
+ if (!state.authenticated || process.env.MULTICA_AGENT_ID) {
86
88
  console.error("Not authenticated. Run 'multica login' to authenticate.")
87
89
  process.exit(0)
88
90
  }
@@ -93,7 +95,11 @@ if (commandKey === 'auth status') {
93
95
  }
94
96
  if (commandKey === 'workspace list') finish([state.workspace])
95
97
  if (commandKey === 'workspace get') finish(state.workspace)
96
- if (commandKey === 'runtime list') { requireScope(); finish([state.runtime]) }
98
+ if (commandKey === 'runtime list') {
99
+ requireScope()
100
+ const provider = process.env.DTA_FAKE_MULTICA_RUNTIME_PROVIDER || state.runtime.provider
101
+ finish([{ ...state.runtime, provider }, ...(state.extraRuntimes || [])])
102
+ }
97
103
  if (commandKey === 'agent list') { requireScope(); finish(state.agents) }
98
104
  if (commandKey === 'skill list') { requireScope(); finish(state.skills) }
99
105
 
@@ -216,10 +222,34 @@ const makeIssueRun = (issueId, agentId, description) => {
216
222
  const agent = state.agents.find((item) => item.id === agentId)
217
223
  if (!agent) process.exit(94)
218
224
  const marker = description.match(/^marker=(.+)$/m)?.[1] || ''
225
+ // 递增的确定性 created_at:readSmoke 取最新 run 判定,重试产生的新 run 必须排在旧
226
+ // 失败 run 之后(真实平台的时间戳天然递增)。
227
+ const sequence = state.sequence.task
219
228
  const taskId = id('task')
229
+ const createdAt = new Date(sequence * 60_000).toISOString()
220
230
  const loaded = (agent.skill_ids || []).map((skillId) => state.skills.find((item) => item.id === skillId)?.name)
221
231
  .filter(Boolean).sort()
232
+ // 冷沙箱竞态模拟:前 N 个 smoke run 看不到刚推送的 Skill(B7)。
233
+ const notFoundBudget = Number(process.env.DTA_FAKE_MULTICA_SKILL_NOT_FOUND_RUNS || 0)
234
+ const notFoundUsed = state.fired.filter((item) => item === 'skill-not-found').length
235
+ if (marker && notFoundBudget > notFoundUsed) {
236
+ state.fired.push('skill-not-found')
237
+ const missing = loaded[0] || 'dta-basic-behavior'
238
+ const messages = [
239
+ { type: 'tool_use', tool: 'read_file', input: { text: `Loading skill '${missing}' (SKILL.md)` } },
240
+ { type: 'tool_result', tool: 'read_file', content: `Skill view failed: Skill '${missing}' not found` },
241
+ { type: 'text', content: 'Only the dws skill is available in this sandbox; cannot execute the verification.' },
242
+ ].map((item, index) => ({ task_id: taskId, issue_id: issueId, seq: index + 1, ...item }))
243
+ return {
244
+ id: taskId, issue_id: issueId, agent_id: agentId, status: 'failed',
245
+ created_at: createdAt, messages,
246
+ }
247
+ }
222
248
  const reply = JSON.stringify({ schema: 'dta-multica-load-smoke@1', marker, loaded })
249
+ if (marker && process.env.DTA_FAKE_MULTICA_SMOKE_NO_COMMENT !== '1') {
250
+ state.comments ||= {}
251
+ ;(state.comments[issueId] ||= []).push({ id: id('file'), issue_id: issueId, content: reply })
252
+ }
223
253
  const resultMode = process.env.DTA_FAKE_MULTICA_SMOKE_RESULT_FILE || ''
224
254
  const resultName = resultMode === 'json-force' ? 'result.json' : 'result.md'
225
255
  const resultPath = `/home/user/multica_workspaces/ws-contract-deploy/${taskId}/workdir/${resultName}`
@@ -256,7 +286,25 @@ const makeIssueRun = (issueId, agentId, description) => {
256
286
  ].join('\n') } },
257
287
  { type: 'tool_use', tool: 'bash', input: { command: `multica issue status ${issueId} in_review` } },
258
288
  ]
259
- const smokeMessages = [
289
+ // runtime 词汇(B8 实测轨迹):skill 装载走 read_file + tool_result 的
290
+ // **Skill loaded**,命令走 terminal 的 `$ ` 前缀文本;穿插无害噪声命令。
291
+ const cloudSmokeMessages = [
292
+ { type: 'tool_use', tool: 'terminal', input: { text: `$ multica issue get ${issueId} --output json` } },
293
+ ...loaded.flatMap((name) => [
294
+ { type: 'tool_use', tool: 'read_file', input: { text: `Loading skill '${name}' (SKILL.md)` } },
295
+ { type: 'tool_result', tool: 'read_file', content: `**Skill loaded** - Name: ${name}` },
296
+ ]),
297
+ { type: 'tool_use', tool: 'terminal', input: { text: '$ ls -la' } },
298
+ ...(process.env.DTA_FAKE_MULTICA_SMOKE_UNSAFE === '1'
299
+ ? [{ type: 'tool_use', tool: 'dws', input: { command: 'contact current-user' } }] : []),
300
+ { type: 'text', content: 'Required skills loaded; posting the structured result.' },
301
+ { type: 'tool_use', tool: 'write', input: { filePath: resultPath, content: reply } },
302
+ { type: 'tool_use', tool: 'terminal', input: { text: `$ multica issue comment add ${issueId} --content-file result.json` } },
303
+ { type: 'tool_result', tool: 'terminal', content: `Comment added to issue ${issueId}.` },
304
+ { type: 'tool_use', tool: 'terminal', input: { text: `$ multica issue status ${issueId} in_review` } },
305
+ { type: 'text', content: 'Load smoke result posted to the current issue.' },
306
+ ]
307
+ const hostSmokeMessages = [
260
308
  { type: 'tool_use', tool: 'bash', input: { command: `multica issue get ${issueId} --output json` } },
261
309
  { type: 'tool_use', tool: 'bash', input: { command: `multica issue metadata list ${issueId} --output json` } },
262
310
  { type: 'tool_use', tool: 'bash', input: { command: `multica issue comment list ${issueId} --recent 10 --output json` } },
@@ -268,6 +316,20 @@ const makeIssueRun = (issueId, agentId, description) => {
268
316
  ...responseTools,
269
317
  { type: 'text', content: 'Load smoke result posted to the current issue.' },
270
318
  ]
319
+ // 伪造轨迹:banner 只出现在 terminal echo 的 tool_result 里(模型可指定的内容),
320
+ // 没有任何 read_file 装载结果——判定必须拒绝把它当装载证据。
321
+ const cloudSpoofMessages = [
322
+ ...loaded.flatMap((name) => [
323
+ { type: 'tool_use', tool: 'terminal', input: { text: `$ echo "**Skill loaded** - Name: ${name}"` } },
324
+ { type: 'tool_result', tool: 'terminal', content: `**Skill loaded** - Name: ${name}` },
325
+ ]),
326
+ { type: 'text', content: 'Required skills loaded; posting the structured result.' },
327
+ { type: 'tool_use', tool: 'write', input: { filePath: resultPath, content: reply } },
328
+ { type: 'tool_use', tool: 'terminal', input: { text: `$ multica issue comment add ${issueId} --content-file result.json` } },
329
+ ]
330
+ const vocabMode = process.env.DTA_FAKE_MULTICA_TRACE_VOCAB || ''
331
+ const smokeMessages = vocabMode === 'cloud' ? cloudSmokeMessages
332
+ : vocabMode === 'cloud-spoof' ? cloudSpoofMessages : hostSmokeMessages
271
333
  const conversationMessages = [
272
334
  { type: 'tool_use', tool: 'skill', input: { name: 'release-manager' } },
273
335
  { type: 'text', content: 'Multica contract answer from the assigned Agent.' },
@@ -277,7 +339,7 @@ const makeIssueRun = (issueId, agentId, description) => {
277
339
  }))
278
340
  return {
279
341
  id: taskId, issue_id: issueId, agent_id: agentId, status: 'completed',
280
- created_at: new Date(0).toISOString(), messages,
342
+ created_at: createdAt, messages,
281
343
  }
282
344
  }
283
345
 
@@ -303,19 +365,47 @@ if (commandKey === 'issue comment add') {
303
365
  if (!issue) process.exit(94)
304
366
  const content = flag('--content')
305
367
  if (!content.includes(`mention://agent/${issue.assignee_id}`)) process.exit(95)
306
- issue.runs.push(makeIssueRun(issue.id, issue.assignee_id, issue.description || ''))
368
+ const newRun = makeIssueRun(issue.id, issue.assignee_id, issue.description || '')
369
+ // 复刻真实平台:mention 触发的 run 是异步创建的,前 N 次 issue runs 读还看不到它。
370
+ const delayReads = Number(process.env.DTA_FAKE_MULTICA_RUN_DELAY_READS || 0)
371
+ if (delayReads > 0) {
372
+ state.delayedRuns ||= {}
373
+ state.delayedRuns[issue.id] = { remaining: delayReads, run: newRun }
374
+ } else {
375
+ issue.runs.push(newRun)
376
+ }
307
377
  issue.status = 'done'
308
- finish({ id: id('file'), issue_id: issue.id, content }, true)
378
+ state.comments ||= {}
379
+ const comment = { id: id('file'), issue_id: issue.id, content }
380
+ ;(state.comments[issue.id] ||= []).push(comment)
381
+ finish(comment, true)
382
+ }
383
+ if (commandKey === 'issue comment list') {
384
+ requireScope()
385
+ const issue = state.issues.find((item) => item.id === core[3])
386
+ if (!issue) process.exit(94)
387
+ finish((state.comments || {})[issue.id] || [])
309
388
  }
310
389
  if (commandKey === 'issue runs') {
311
390
  requireScope()
312
391
  const issue = state.issues.find((item) => item.id === core[2])
313
392
  if (!issue) process.exit(94)
393
+ // 延迟中的 run 每被读一次倒计数一次,归零才可见(模拟异步 run 创建)。
394
+ const delayed = (state.delayedRuns || {})[issue.id]
395
+ let materialized = false
396
+ if (delayed) {
397
+ delayed.remaining -= 1
398
+ if (delayed.remaining <= 0) {
399
+ issue.runs.push(delayed.run)
400
+ delete state.delayedRuns[issue.id]
401
+ }
402
+ materialized = true
403
+ }
314
404
  // Simulate an overrunning run: report the task as still running so the poll loop times out.
315
405
  // The persisted run stays terminal, so a later `--status --run-id` resume sees it completed.
316
406
  const forceRunning = process.env.DTA_FAKE_MULTICA_RUN_FORCE_RUNNING === '1'
317
407
  finish(issue.runs.map(({ messages, ...run }) =>
318
- forceRunning ? { ...run, status: 'running' } : run))
408
+ forceRunning ? { ...run, status: 'running' } : run), materialized)
319
409
  }
320
410
  if (commandKey === 'issue run-messages') {
321
411
  requireScope()
@@ -9,8 +9,8 @@
9
9
  "runtime": {
10
10
  "id": "runtime-contract-deploy",
11
11
  "workspace_id": "ws-contract-deploy",
12
- "name": "Contract Codex Runtime",
13
- "provider": "codex",
12
+ "name": "Contract OpenCode Runtime",
13
+ "provider": "opencode",
14
14
  "status": "online"
15
15
  },
16
16
  "authenticated": true,
@@ -9,8 +9,8 @@
9
9
  "runtime": {
10
10
  "id": "runtime-contract-codex",
11
11
  "workspace_id": "ws-contract-release",
12
- "name": "Contract Codex Runtime",
13
- "provider": "codex",
12
+ "name": "Contract OpenCode Runtime",
13
+ "provider": "opencode",
14
14
  "status": "online"
15
15
  },
16
16
  "agent": {
@@ -29,7 +29,7 @@
29
29
  "profile": "dev",
30
30
  "workspaceIdFrom": "env:MULTICA_WORKSPACE_ID",
31
31
  "runtimeIdFrom": "env:MULTICA_RUNTIME_ID",
32
- "agentIdFrom": "env:MULTICA_AGENT_ID"
32
+ "agentIdFrom": "env:DTA_MULTICA_DEV_AGENT_ID"
33
33
  },
34
34
  "authority": {
35
35
  "dwsProfileFrom": "env:DTA_DWS_PROFILE",
@@ -12,7 +12,7 @@
12
12
  "capability": "basic-skill",
13
13
  "prompt": "这是基础行为加载探针。只依据已经进入当前上下文的正文,严格输出 skill=<name>@<metadata.version>;如果正文不可见,只输出 skill=not-loaded。禁止猜测,不要补充解释。",
14
14
  "expectations": [
15
- { "id": "exact-basic-version", "type": "equals", "value": "skill=dta-basic-behavior@0.13.0" }
15
+ { "id": "exact-basic-version", "type": "equals", "value": "skill=dta-basic-behavior@0.13.1" }
16
16
  ],
17
17
  "manualChecks": []
18
18
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xdxer/dingtalk-agent",
3
- "version": "0.1.5-beta.8",
3
+ "version": "0.1.5-beta.9",
4
4
  "description": "把 Agent 装配成钉钉数字员工:Skill 决策,CLI 固定事务边界,Agent 本体运行在哪个 Managed Agent Platform 上是可替换的。",
5
5
  "keywords": [
6
6
  "dingtalk",
@@ -39,6 +39,8 @@
39
39
  "evals/baselines/2026-07-17/personal-event-live-readiness-summary.json",
40
40
  "evals/baselines/2026-07-17/agent-enhance-opencode-dogfood-summary.json",
41
41
  "lab",
42
+ "docs/AGENT-IN-PRODUCTION.md",
43
+ "docs/PLATFORM-GUARDRAILS.md",
42
44
  "docs/ARCHITECTURE.md",
43
45
  "docs/INSTALLATION.md",
44
46
  "docs/PRIOR-ART.md",
@@ -59,9 +61,10 @@
59
61
  "eval:behavior": "npm run build && node evals/run-shadow-evals.mjs",
60
62
  "eval:opencode": "npm run build && node dist/bin/dingtalk-agent.js lab eval --engine opencode --workspace lab/robot-eval/workspace --suite lab/robot-eval/suite.json --lanes stateless --execute --yes --json",
61
63
  "prepack": "npm run build",
62
- "release:check": "npm run build && node scripts/check-skill-versions.mjs && node scripts/check-docs.mjs && node evals/run-contract-evals.mjs --full && node scripts/release-readiness.mjs --json",
64
+ "release:check": "npm run build && node scripts/check-skill-versions.mjs && node scripts/check-docs.mjs && node scripts/check-schedule.mjs && node evals/run-contract-evals.mjs --full && node scripts/release-readiness.mjs --json",
63
65
  "check:skill-versions": "node scripts/check-skill-versions.mjs",
64
66
  "check:docs": "node scripts/check-docs.mjs",
67
+ "check:schedule": "node scripts/check-schedule.mjs",
65
68
  "eval:contract:full": "npm run build && node evals/run-contract-evals.mjs --full"
66
69
  },
67
70
  "devDependencies": {
@@ -3,7 +3,7 @@ name: dta-agent-compose
3
3
  description: 当用户要创建、新建、装配一个 Agent 或钉钉数字员工——包括把 GitHub 仓库、本地文件夹或钉钉文档定义成 Agent,或要审计、补齐、优化 Agent 的 AGENTS.md、本体职责、岗位 Skills、记忆/知识/产物存储、DWS 权限绑定与「谁叫醒它」的驱动节律时使用。即使尚未 init Workspace,也按 dingtalk-agent 的 AgentDefinition 范式给出可运行的最小装配方案;声明驱动节律但不实现触发器本身。
4
4
  compatibility: Requires dingtalk-agent on PATH; remote DingTalk documents require authenticated dws.
5
5
  metadata:
6
- version: "0.15.0"
6
+ version: "0.15.2"
7
7
  ---
8
8
 
9
9
  # 装配一个可工作的钉钉数字员工 Agent