@xdxer/dingtalk-agent 0.1.5-beta.7 → 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 (51) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.en.md +1 -0
  3. package/README.md +1 -0
  4. package/dist/bin/dingtalk-agent.js +118 -15
  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/dist/src/skill-manager.js +76 -9
  23. package/dist/src/skill-manager.js.map +1 -1
  24. package/docs/AGENT-IN-PRODUCTION.md +255 -0
  25. package/docs/ARCHITECTURE.md +5 -1
  26. package/docs/PLATFORM-GUARDRAILS.md +188 -0
  27. package/docs/schemas/multica-deployment-plan.schema.json +3 -1
  28. package/docs/schemas/multica-deployment-receipt.schema.json +15 -1
  29. package/docs/schemas/multica-deployment-status.schema.json +6 -2
  30. package/docs/schemas/multica-workspace-inspection.schema.json +16 -0
  31. package/docs/schemas/multica-workspace-status.schema.json +2 -0
  32. package/docs/schemas/workspace-scaffold.schema.json +1 -0
  33. package/lab/project-workspace/fake-multica-provider.mjs +97 -7
  34. package/lab/project-workspace/multica-deploy.fixture.json +2 -2
  35. package/lab/project-workspace/multica-readonly.fixture.json +2 -2
  36. package/lab/project-workspace/project.fixture.json +1 -1
  37. package/lab/robot-eval/suite.json +1 -1
  38. package/package.json +5 -2
  39. package/skills/core/dta-agent-compose/SKILL.md +1 -1
  40. package/skills/core/dta-agent-compose/references/drive-and-schedules.md +22 -2
  41. package/skills/core/dta-basic-behavior/SKILL.md +1 -1
  42. package/skills/core/dta-basic-behavior/references/memory-and-evolution.md +1 -1
  43. package/skills/core/dta-people-group-memory/SKILL.md +4 -4
  44. package/skills/core/dta-people-group-memory/references/model.md +1 -1
  45. package/skills/platforms/multica-dingtalk/PLATFORM.md +2 -2
  46. package/skills/platforms/multica-dingtalk/dta-deploy-multica/SKILL.md +6 -5
  47. package/skills/platforms/multica-dingtalk/dta-deploy-multica/references/multica-deployment-contract.md +14 -4
  48. package/skills/platforms/multica-dingtalk/dta-ops-multica/SKILL.md +33 -7
  49. package/skills/platforms/multica-dingtalk/dta-ops-multica/scripts/multica_ext.py +118 -12
  50. package/dist/src/map.js +0 -157
  51. package/dist/src/map.js.map +0 -1
@@ -9,12 +9,162 @@
9
9
  *
10
10
  * Pure: no network, no side effects — safe to run in CI and fully testable.
11
11
  */
12
- import { inspectAgentProject } from './development-workspace.js';
12
+ import { inspectAgentProject, multicaAgentIdSource, } from './development-workspace.js';
13
+ // manifest 级的三项静态体检,show 和 plan 共用一份,避免两处逻辑漂移。
14
+ export function analyzeSchedules(declared) {
15
+ const collidingPairs = [];
16
+ const unverifiedPairs = [];
17
+ for (let i = 0; i < declared.length; i++) {
18
+ for (let j = i + 1; j < declared.length; j++) {
19
+ const pair = `${declared[i].name}×${declared[j].name}`;
20
+ // 跨时区:本地无 tz 换算 → 判不准是否同刻 → unverified(不静默当 distinct)。
21
+ if (declared[i].timezone !== declared[j].timezone) {
22
+ unverifiedPairs.push(pair);
23
+ continue;
24
+ }
25
+ const r = firesCollide(declared[i].cron, declared[j].cron);
26
+ if (r === 'collide')
27
+ collidingPairs.push(pair);
28
+ else if (r === 'unverified')
29
+ unverifiedPairs.push(pair);
30
+ }
31
+ }
32
+ const missingCompletion = declared.filter((s) => !s.completion?.trim()).map((s) => s.name);
33
+ return { collidingPairs, unverifiedPairs, missingCompletion };
34
+ }
35
+ /**
36
+ * 阻断部署的问题清单(空 = 可部署)。三类都算阻断,**包括 unverified**。
37
+ *
38
+ * unverified 曾经只告警,理由是"本地证明不了 ≠ 有问题"。这条理由错在赌的方向:
39
+ * `0 9 * * *`@Asia/Shanghai 与 `0 1 * * *`@UTC 是**同一时刻**,静态比对看不出来,
40
+ * 于是两条各建一个 autopilot、每天把同一条消息重复发给第三方——正是本守卫要防的那个伤害。
41
+ * 证不出不撞车就当撞车(fail closed):挡住是一次响亮且可恢复的配置改动,放过是一次静默且发出去收不回的重复外发。
42
+ *
43
+ * 出路有两条,都不需要改这里:把两条节律写成同一个 timezone(对齐后走真实 occurrence 比对,
44
+ * 判得出不撞就自动放行),或换掉本地读不懂的 cron 扩展语法。
45
+ *
46
+ * 判据只此一份:CLI 的 show 和 plan 都调它。此前两处各自手写 exit 条件,结果都漏了 unverified——
47
+ * 抽出 analyzeSchedules 是为了防判据分叉,退出判据自己却分叉了。
48
+ */
49
+ export function scheduleBlockers(a) {
50
+ return [...a.collidingPairs, ...a.unverifiedPairs, ...a.missingCompletion];
51
+ }
13
52
  // ephemeral = run-only (no work item); work-item = one work item per fire.
14
53
  const MULTICA_MODE = {
15
54
  ephemeral: 'run_only',
16
55
  'work-item': 'create_issue',
17
56
  };
57
+ // 只有 work-item 每次 fire 建 issue、平台才有 token 用量可查。
58
+ const isMetered = (mode) => mode === 'work-item';
59
+ // 五个字段的上界(分 时 日 月 周)。
60
+ const CRON_FIELD_MAX = [59, 23, 31, 12, 7];
61
+ // 名字字面量 → 数字(robfig/cron v3 支持,Multica 实测用 MON-FRI;不支持就会漏掉真双发)。
62
+ const DOW_NAMES = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 };
63
+ const MON_NAMES = {
64
+ JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12,
65
+ };
66
+ /**
67
+ * 把一个 cron 字段展开成它命中的整数集合。支持 `*`、列表、范围、步长,以及 dow/月的名字字面量。
68
+ * 解析不出(`L`/`W`/`#`/`@` 等扩展语法)时返回 null —— 调用方据此判 unverified,不假装读懂了。
69
+ */
70
+ function expandCronField(field, max, names) {
71
+ const out = new Set();
72
+ const num = (t) => {
73
+ const up = t.toUpperCase();
74
+ if (names && up in names)
75
+ return names[up];
76
+ return /^\d+$/.test(t) ? parseInt(t, 10) : NaN;
77
+ };
78
+ for (const part of field.split(',')) {
79
+ const [range, stepRaw] = part.split('/');
80
+ const step = stepRaw ? parseInt(stepRaw, 10) : 1;
81
+ let lo = 0;
82
+ let hi = max;
83
+ if (range !== '*') {
84
+ const [a, b] = range.split('-');
85
+ lo = num(a);
86
+ // robfig getRange 的特判:**单个数字 + 步长时上界拉到 max**——`5/15` 等价 `5-59/15` = {5,20,35,50},
87
+ // 不是只有 {5}。当成 {5} 会漏掉 20/35/50,把真撞车判成 distinct → 正是本守卫要防的重复外发。
88
+ hi = b === undefined ? (stepRaw ? max : lo) : num(b);
89
+ }
90
+ if (Number.isNaN(lo) || Number.isNaN(hi) || Number.isNaN(step) || step < 1)
91
+ return null;
92
+ for (let v = lo; v <= hi && v <= max; v += step)
93
+ out.add(v === 7 && max === 7 ? 0 : v);
94
+ }
95
+ // 空集合 = 越界(`70`)或倒序区间(`5-2`),是【没读懂】不是【永不 fire】。
96
+ // 返回空集会让 setsIntersect 判 false → distinct(fail-open);返回 null → unverified,交给人核。
97
+ return out.size === 0 ? null : out;
98
+ }
99
+ function setsIntersect(a, b) {
100
+ for (const v of a)
101
+ if (b.has(v))
102
+ return true;
103
+ return false;
104
+ }
105
+ function parseCron(cron) {
106
+ const f = cron.trim().split(/\s+/);
107
+ if (f.length !== 5)
108
+ return null;
109
+ const min = expandCronField(f[0], 59);
110
+ const hour = expandCronField(f[1], 23);
111
+ const dom = expandCronField(f[2], 31);
112
+ const mon = expandCronField(f[3], 12, MON_NAMES);
113
+ const dow = expandCronField(f[4], 7, DOW_NAMES);
114
+ if (!min || !hour || !dom || !mon || !dow)
115
+ return null;
116
+ return { min, hour, dom, mon, dow, domStar: f[2] === '*', dowStar: f[4] === '*' };
117
+ }
118
+ // robfig/cron v3 的日匹配:dom 和 dow 都受限时用 OR,否则各自 AND(* 视为全通过)。
119
+ function dayMatch(p, dom, dowV) {
120
+ if (!p.domStar && !p.dowStar)
121
+ return p.dom.has(dom) || p.dow.has(dowV);
122
+ return (p.domStar || p.dom.has(dom)) && (p.dowStar || p.dow.has(dowV));
123
+ }
124
+ const isLeap = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
125
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
126
+ function daysInMonth(y, m) {
127
+ return m === 2 && isLeap(y) ? 29 : DAYS_IN_MONTH[m - 1];
128
+ }
129
+ /**
130
+ * 两条节律会不会在【同一时刻】fire。返回 collide / distinct / unverified。
131
+ * 判据【按真实 occurrence】算,不做简化字段交集——robfig 的 dom/dow 是 OR 语义、支持名字字面量,
132
+ * 简化交集会误判(Multica 用 robfig/cron v3,`0 9 * * MON-FRI` 必须能识别)。
133
+ * - 分/时/月任一集合不相交 → 一定不同刻 → distinct(便宜的早退)。
134
+ * - 否则枚举【完整的 400 年格里高利周期】找同一天两条都 active。为什么必须整 400 年而不是几年窗口:
135
+ * 像 `0 9 29 2 1` 与 `0 9 30 2 3` 的首个共同 fire 落在 2040-02-29,短窗口会漏判成 distinct(假阴性→漏真双发)。
136
+ * 公历 (月,日,星期) 组合每 400 年完整重复,所以枚举一整轮 = 完备,"整轮无碰撞"才敢判 distinct。
137
+ * - 任一字段解析不出(扩展语法)→ unverified:**绝不当 distinct**,交给人核。
138
+ * export:给 scripts/check-schedule.mjs 做回归断言。
139
+ */
140
+ export function firesCollide(cronA, cronB) {
141
+ const a = parseCron(cronA);
142
+ const b = parseCron(cronB);
143
+ if (!a || !b)
144
+ return 'unverified';
145
+ if (!setsIntersect(a.min, b.min) || !setsIntersect(a.hour, b.hour) || !setsIntersect(a.mon, b.mon)) {
146
+ return 'distinct';
147
+ }
148
+ // 整 400 年周期手工走(不 new Date,避免 14 万次分配)。2000-01-01 是周六(dow=6)。
149
+ let dow = 6;
150
+ for (let y = 2000; y < 2400; y++) {
151
+ for (let m = 1; m <= 12; m++) {
152
+ const monA = a.mon.has(m);
153
+ const monB = b.mon.has(m);
154
+ const dim = daysInMonth(y, m);
155
+ if (!(monA && monB)) {
156
+ dow = (dow + dim) % 7;
157
+ continue;
158
+ } // 该月两条不同时活跃,跳过但推进 dow
159
+ for (let dom = 1; dom <= dim; dom++) {
160
+ if (dayMatch(a, dom, dow) && dayMatch(b, dom, dow))
161
+ return 'collide';
162
+ dow = (dow + 1) % 7;
163
+ }
164
+ }
165
+ }
166
+ return 'distinct';
167
+ }
18
168
  function shellQuote(value) {
19
169
  return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
20
170
  }
@@ -22,11 +172,11 @@ function envVar(ref) {
22
172
  // "env:MULTICA_FDE_AGENT_ID" -> "$MULTICA_FDE_AGENT_ID"
23
173
  return ref.startsWith('env:') ? `$${ref.slice(4)}` : ref;
24
174
  }
25
- function multicaApplyCommand(schedule, profile, agentIdFrom) {
175
+ function multicaApplyCommand(schedule, profile, agentRef) {
26
176
  const parts = [
27
177
  'python3 "$MULTICA_EXT" autopilot-create',
28
178
  `--profile ${profile}`,
29
- `--agent ${envVar(agentIdFrom)}`,
179
+ `--agent ${agentRef}`,
30
180
  `--title ${shellQuote(schedule.name)}`,
31
181
  `--mode ${MULTICA_MODE[schedule.mode]}`,
32
182
  `--cron ${shellQuote(schedule.cron)}`,
@@ -46,21 +196,39 @@ export function buildSchedulePlan(info, workspaceName) {
46
196
  throw new Error(`Project 未声明 Workspace: ${workspaceName}`);
47
197
  const declared = info.project.schedules;
48
198
  const provider = workspace.provider.kind;
199
+ const analysis = analyzeSchedules(declared); // 撞车/未验证/缺判据——manifest 级,与 provider 无关
49
200
  if (provider === 'multica') {
50
201
  const profile = workspace.provider.profile || '';
51
- const agentIdFrom = workspace.provider.agentIdFrom || '';
52
- if (!profile || !agentIdFrom) {
53
- throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile/agentIdFrom`);
202
+ if (!profile) {
203
+ throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile`);
204
+ }
205
+ // Agent 绑定来源与 deploy 同一份回退链(multicaAgentIdSource):黄金路径下
206
+ // scaffold 故意省略 agentIdFrom、首次 deploy 把真实 ID 写进 state——schedule
207
+ // 此前不读 state,导致部署完的 Agent 声明节律时还被要求手工接线(E-03)。
208
+ const source = multicaAgentIdSource(workspace);
209
+ if (source.kind === 'none') {
210
+ throw new Error(`Workspace ${workspaceName} 缺 Agent 绑定:manifest 未声明 provider.agentIdFrom,` +
211
+ `且尚无部署 state(.dingtalk-agent/state/workspaces/${workspaceName}.json)。` +
212
+ `先 dta deploy --workspace ${workspaceName} 创建 Agent(ID 会写入 state 自动复用),` +
213
+ '或在 manifest 显式声明 provider.agentIdFrom');
54
214
  }
215
+ const agentRef = source.kind === 'declared' ? envVar(source.ref) : source.agentId;
216
+ const agentIdSource = source.kind === 'declared' ? 'declared-env' : 'deploy-state';
55
217
  const schedules = declared.map((schedule) => ({
56
218
  ...schedule,
57
- applyCommand: multicaApplyCommand(schedule, profile, agentIdFrom),
219
+ applyCommand: multicaApplyCommand(schedule, profile, agentRef),
220
+ metered: isMetered(schedule.mode),
58
221
  }));
59
222
  return {
60
- $schema: 'dingtalk-agent/schedule-plan@1',
61
- workspace: workspaceName, provider, supported: true, count: schedules.length, schedules,
223
+ $schema: 'dingtalk-agent/schedule-plan@2',
224
+ workspace: workspaceName, provider, supported: true, count: schedules.length,
225
+ agentIdSource, schedules,
226
+ ...analysis,
62
227
  reconcile: '由 dta-ops-multica 技能执行:先 autopilot-list 读线上,按 title 匹配,' +
63
- '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py,$…_AGENT_ID 由受信环境注入。' +
228
+ '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py' +
229
+ (agentIdSource === 'declared-env'
230
+ ? ',$…_AGENT_ID 由受信环境注入。'
231
+ : ';Agent ID 来自本项目部署 state(首次 deploy 写入)。') +
64
232
  'autopilot-create 非幂等,务必先 list 再建,别盲建(会造重复→重复外发)。',
65
233
  sideEffect: false, dingtalkSideEffect: false,
66
234
  };
@@ -70,35 +238,35 @@ export function buildSchedulePlan(info, workspaceName) {
70
238
  ? 'opencode 是本地开发运行时,不承载常驻节律;节律需部署到托管平台'
71
239
  : `provider ${provider} 尚未提供 schedule 能力`;
72
240
  return {
73
- $schema: 'dingtalk-agent/schedule-plan@1',
241
+ $schema: 'dingtalk-agent/schedule-plan@2',
74
242
  workspace: workspaceName, provider, supported: false, reason,
75
243
  count: declared.length, schedules: [],
244
+ // provider 不支持,也照样报 manifest 自身的问题(撞车/未验证/缺判据),和平台支不支持无关。
245
+ ...analysis,
76
246
  reconcile: `记为缺口:声明了 ${declared.length} 条节律,但目标 provider 不支持——` +
77
247
  '别静默降级成"那就不定时了"。',
78
248
  sideEffect: false, dingtalkSideEffect: false,
79
249
  };
80
250
  }
81
251
  /**
82
- * Declared schedules + the one platform-independent invariant dta can check
83
- * statically: ticks must not share a minute. Outward-effect ticks have no lock on
84
- * their delivery receipt, so two firing in the same minute double-send to third
85
- * parties. Reports any minute field claimed by more than one schedule.
252
+ * Declared schedules + 两条与平台无关、能静态检的不变量:
253
+ * 1) 两拍不得在【同一时刻】并发 fire。按【真实 occurrence】判(见 firesCollide:robfig dom/dow OR、
254
+ * 名字字面量、闰年窗口枚举),不是简化字段交集。判不准的(跨时区/扩展语法)进 unverifiedPairs,不当 distinct。
255
+ * 2) 每条节律都要有数据派生的完成判据(completion),否则只能靠"那一拍跑没跑"判完成,漏拍就永久丢一期。
256
+ * ⚠️ 这两条都只看【本地声明的 manifest】,不查平台实际生效状态——线上真相要走 provider 回读。
86
257
  */
87
258
  export function buildScheduleShow(start, cliVersion = '') {
88
259
  const info = inspectAgentProject(start, cliVersion);
89
260
  const declared = info.project.schedules;
90
- const seen = new Map();
91
- for (const schedule of declared) {
92
- const minute = schedule.cron.trim().split(/\s+/)[0];
93
- seen.set(minute, (seen.get(minute) || 0) + 1);
94
- }
95
- const collidingMinutes = [...seen.entries()].filter(([, n]) => n > 1).map(([m]) => m);
261
+ const { collidingPairs, unverifiedPairs, missingCompletion } = analyzeSchedules(declared);
96
262
  return {
97
- $schema: 'dingtalk-agent/schedule-show@1',
263
+ $schema: 'dingtalk-agent/schedule-show@2',
98
264
  count: declared.length,
99
265
  schedules: declared,
100
- minutesDistinct: collidingMinutes.length === 0,
101
- collidingMinutes,
266
+ firesDistinct: collidingPairs.length === 0 && unverifiedPairs.length === 0,
267
+ collidingPairs,
268
+ unverifiedPairs,
269
+ missingCompletion,
102
270
  sideEffect: false, dingtalkSideEffect: false,
103
271
  };
104
272
  }
@@ -1 +1 @@
1
- {"version":3,"file":"schedule-plan.js","sourceRoot":"","sources":["../../src/schedule-plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schedule reconcile plan — dta's half of the driving contract.\n *\n * dta owns: read the declared schedules (dingtalk-agent.json#schedules), validate\n * them, and map them to a provider-specific reconcile plan. dta does NOT fire or\n * live-write triggers — that stays with the platform ops path (see ARCHITECTURE\n * §3.1, \"触发器不属于 dingtalk-agent 核心执行\"). The plan is the authoritative,\n * provider-mapped desired state the ops skill reconciles against live.\n *\n * Pure: no network, no side effects — safe to run in CI and fully testable.\n */\nimport { inspectAgentProject, type AgentProjectInfo, type DeclaredSchedule } from './development-workspace.js'\n\nexport interface SchedulePlanEntry extends DeclaredSchedule {\n applyCommand: string\n}\n\nexport interface SchedulePlan {\n $schema: 'dingtalk-agent/schedule-plan@1'\n workspace: string\n provider: string\n supported: boolean\n reason?: string\n count: number\n schedules: SchedulePlanEntry[]\n reconcile: string\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n// ephemeral = run-only (no work item); work-item = one work item per fire.\nconst MULTICA_MODE: Record<DeclaredSchedule['mode'], string> = {\n ephemeral: 'run_only',\n 'work-item': 'create_issue',\n}\n\nfunction shellQuote(value: string): string {\n return `\"${value.replace(/([\"\\\\$`])/g, '\\\\$1')}\"`\n}\n\nfunction envVar(ref: string): string {\n // \"env:MULTICA_FDE_AGENT_ID\" -> \"$MULTICA_FDE_AGENT_ID\"\n return ref.startsWith('env:') ? `$${ref.slice(4)}` : ref\n}\n\nfunction multicaApplyCommand(\n schedule: DeclaredSchedule, profile: string, agentIdFrom: string,\n): string {\n const parts = [\n 'python3 \"$MULTICA_EXT\" autopilot-create',\n `--profile ${profile}`,\n `--agent ${envVar(agentIdFrom)}`,\n `--title ${shellQuote(schedule.name)}`,\n `--mode ${MULTICA_MODE[schedule.mode]}`,\n `--cron ${shellQuote(schedule.cron)}`,\n `--timezone ${schedule.timezone}`,\n `--description ${shellQuote(schedule.wake || schedule.name)}`,\n ]\n return parts.join(' ')\n}\n\n/**\n * Build the reconcile plan for one workspace's provider. Returns supported:false\n * (with a reason) when the target platform does not offer schedules yet — the\n * declared rhythms are then a recorded gap, never a silent downgrade.\n */\nexport function buildSchedulePlan(info: AgentProjectInfo, workspaceName: string): SchedulePlan {\n const workspace = info.workspaces.find((item) => item.name === workspaceName)\n if (!workspace) throw new Error(`Project 未声明 Workspace: ${workspaceName}`)\n const declared = info.project.schedules\n const provider = workspace.provider.kind\n\n if (provider === 'multica') {\n const profile = workspace.provider.profile || ''\n const agentIdFrom = workspace.provider.agentIdFrom || ''\n if (!profile || !agentIdFrom) {\n throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile/agentIdFrom`)\n }\n const schedules = declared.map((schedule) => ({\n ...schedule,\n applyCommand: multicaApplyCommand(schedule, profile, agentIdFrom),\n }))\n return {\n $schema: 'dingtalk-agent/schedule-plan@1',\n workspace: workspaceName, provider, supported: true, count: schedules.length, schedules,\n reconcile: '由 dta-ops-multica 技能执行:先 autopilot-list 读线上,按 title 匹配,' +\n '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py,$…_AGENT_ID 由受信环境注入。' +\n 'autopilot-create 非幂等,务必先 list 再建,别盲建(会造重复→重复外发)。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n }\n\n // opencode / local dev / not-yet-supported platforms: no schedule capability.\n const reason = provider === 'opencode'\n ? 'opencode 是本地开发运行时,不承载常驻节律;节律需部署到托管平台'\n : `provider ${provider} 尚未提供 schedule 能力`\n return {\n $schema: 'dingtalk-agent/schedule-plan@1',\n workspace: workspaceName, provider, supported: false, reason,\n count: declared.length, schedules: [],\n reconcile: `记为缺口:声明了 ${declared.length} 条节律,但目标 provider 不支持——` +\n '别静默降级成\"那就不定时了\"。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n\nexport interface ScheduleShow {\n $schema: 'dingtalk-agent/schedule-show@1'\n count: number\n schedules: DeclaredSchedule[]\n minutesDistinct: boolean\n collidingMinutes: string[]\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n/**\n * Declared schedules + the one platform-independent invariant dta can check\n * statically: ticks must not share a minute. Outward-effect ticks have no lock on\n * their delivery receipt, so two firing in the same minute double-send to third\n * parties. Reports any minute field claimed by more than one schedule.\n */\nexport function buildScheduleShow(start: string, cliVersion = ''): ScheduleShow {\n const info = inspectAgentProject(start, cliVersion)\n const declared = info.project.schedules\n const seen = new Map<string, number>()\n for (const schedule of declared) {\n const minute = schedule.cron.trim().split(/\\s+/)[0]\n seen.set(minute, (seen.get(minute) || 0) + 1)\n }\n const collidingMinutes = [...seen.entries()].filter(([, n]) => n > 1).map(([m]) => m)\n return {\n $schema: 'dingtalk-agent/schedule-show@1',\n count: declared.length,\n schedules: declared,\n minutesDistinct: collidingMinutes.length === 0,\n collidingMinutes,\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n"]}
1
+ {"version":3,"file":"schedule-plan.js","sourceRoot":"","sources":["../../src/schedule-plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schedule reconcile plan — dta's half of the driving contract.\n *\n * dta owns: read the declared schedules (dingtalk-agent.json#schedules), validate\n * them, and map them to a provider-specific reconcile plan. dta does NOT fire or\n * live-write triggers — that stays with the platform ops path (see ARCHITECTURE\n * §3.1, \"触发器不属于 dingtalk-agent 核心执行\"). The plan is the authoritative,\n * provider-mapped desired state the ops skill reconciles against live.\n *\n * Pure: no network, no side effects — safe to run in CI and fully testable.\n */\nimport {\n inspectAgentProject, multicaAgentIdSource,\n type AgentProjectInfo, type DeclaredSchedule,\n} from './development-workspace.js'\n\nexport interface SchedulePlanEntry extends DeclaredSchedule {\n applyCommand: string\n // 平台是否为这一拍记账。work-item 每次 fire 建 issue → 有 token 用量可查;\n // ephemeral(run_only) 不建 issue → 平台不记账,这一拍的成本【看不见】。\n // 改 mode 就是在改计量口径,这里显式标出来,别让它变成隐形盲区。\n metered: boolean\n}\n\nexport interface SchedulePlan {\n $schema: 'dingtalk-agent/schedule-plan@2'\n workspace: string\n provider: string\n supported: boolean\n reason?: string\n count: number\n /**\n * Agent 绑定来源:manifest 声明的 env 引用(declared-env),或首次 deploy 写入\n * workspace state 的真实 ID(deploy-state)。黄金路径(scaffold 故意省略\n * agentIdFrom、由 deploy 创建 Agent 存 ID)走的是后者。\n */\n agentIdSource?: 'declared-env' | 'deploy-state'\n schedules: SchedulePlanEntry[]\n // 声明了 cron 却没写 completion(数据派生的完成判据)的节律名。\n // 没有完成判据 = 只能靠\"那一拍跑了没有\"来判完成,而平台会漏拍/跳过迟到的 fire,\n // 一次抖动就永久丢一期。这些拍记成缺口,不静默放过。\n missingCompletion: string[]\n // 会同一时刻并发 fire 的节律对 / 无法静态确认是否同刻的对。\n // ⚠️ plan 是 operator 真正照着部署的路径,所以撞车必须在这里也拦——\n // 否则 show 报了撞车、plan 照样吐出 apply 命令,两条都建成 autopilot → 重复外发。\n collidingPairs: string[]\n unverifiedPairs: string[]\n reconcile: string\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n// manifest 级的三项静态体检,show 和 plan 共用一份,避免两处逻辑漂移。\nexport function analyzeSchedules(declared: DeclaredSchedule[]): {\n collidingPairs: string[]; unverifiedPairs: string[]; missingCompletion: string[]\n} {\n const collidingPairs: string[] = []\n const unverifiedPairs: string[] = []\n for (let i = 0; i < declared.length; i++) {\n for (let j = i + 1; j < declared.length; j++) {\n const pair = `${declared[i].name}×${declared[j].name}`\n // 跨时区:本地无 tz 换算 → 判不准是否同刻 → unverified(不静默当 distinct)。\n if (declared[i].timezone !== declared[j].timezone) { unverifiedPairs.push(pair); continue }\n const r = firesCollide(declared[i].cron, declared[j].cron)\n if (r === 'collide') collidingPairs.push(pair)\n else if (r === 'unverified') unverifiedPairs.push(pair)\n }\n }\n const missingCompletion = declared.filter((s) => !s.completion?.trim()).map((s) => s.name)\n return { collidingPairs, unverifiedPairs, missingCompletion }\n}\n\n/**\n * 阻断部署的问题清单(空 = 可部署)。三类都算阻断,**包括 unverified**。\n *\n * unverified 曾经只告警,理由是\"本地证明不了 ≠ 有问题\"。这条理由错在赌的方向:\n * `0 9 * * *`@Asia/Shanghai 与 `0 1 * * *`@UTC 是**同一时刻**,静态比对看不出来,\n * 于是两条各建一个 autopilot、每天把同一条消息重复发给第三方——正是本守卫要防的那个伤害。\n * 证不出不撞车就当撞车(fail closed):挡住是一次响亮且可恢复的配置改动,放过是一次静默且发出去收不回的重复外发。\n *\n * 出路有两条,都不需要改这里:把两条节律写成同一个 timezone(对齐后走真实 occurrence 比对,\n * 判得出不撞就自动放行),或换掉本地读不懂的 cron 扩展语法。\n *\n * 判据只此一份:CLI 的 show 和 plan 都调它。此前两处各自手写 exit 条件,结果都漏了 unverified——\n * 抽出 analyzeSchedules 是为了防判据分叉,退出判据自己却分叉了。\n */\nexport function scheduleBlockers(a: {\n collidingPairs: string[]; unverifiedPairs: string[]; missingCompletion: string[]\n}): string[] {\n return [...a.collidingPairs, ...a.unverifiedPairs, ...a.missingCompletion]\n}\n\n// ephemeral = run-only (no work item); work-item = one work item per fire.\nconst MULTICA_MODE: Record<DeclaredSchedule['mode'], string> = {\n ephemeral: 'run_only',\n 'work-item': 'create_issue',\n}\n\n// 只有 work-item 每次 fire 建 issue、平台才有 token 用量可查。\nconst isMetered = (mode: DeclaredSchedule['mode']): boolean => mode === 'work-item'\n\n// 五个字段的上界(分 时 日 月 周)。\nconst CRON_FIELD_MAX = [59, 23, 31, 12, 7]\n// 名字字面量 → 数字(robfig/cron v3 支持,Multica 实测用 MON-FRI;不支持就会漏掉真双发)。\nconst DOW_NAMES: Record<string, number> = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 }\nconst MON_NAMES: Record<string, number> = {\n JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12,\n}\n\n/**\n * 把一个 cron 字段展开成它命中的整数集合。支持 `*`、列表、范围、步长,以及 dow/月的名字字面量。\n * 解析不出(`L`/`W`/`#`/`@` 等扩展语法)时返回 null —— 调用方据此判 unverified,不假装读懂了。\n */\nfunction expandCronField(field: string, max: number, names?: Record<string, number>): Set<number> | null {\n const out = new Set<number>()\n const num = (t: string): number => {\n const up = t.toUpperCase()\n if (names && up in names) return names[up]\n return /^\\d+$/.test(t) ? parseInt(t, 10) : NaN\n }\n for (const part of field.split(',')) {\n const [range, stepRaw] = part.split('/')\n const step = stepRaw ? parseInt(stepRaw, 10) : 1\n let lo = 0\n let hi = max\n if (range !== '*') {\n const [a, b] = range.split('-')\n lo = num(a)\n // robfig getRange 的特判:**单个数字 + 步长时上界拉到 max**——`5/15` 等价 `5-59/15` = {5,20,35,50},\n // 不是只有 {5}。当成 {5} 会漏掉 20/35/50,把真撞车判成 distinct → 正是本守卫要防的重复外发。\n hi = b === undefined ? (stepRaw ? max : lo) : num(b)\n }\n if (Number.isNaN(lo) || Number.isNaN(hi) || Number.isNaN(step) || step < 1) return null\n for (let v = lo; v <= hi && v <= max; v += step) out.add(v === 7 && max === 7 ? 0 : v)\n }\n // 空集合 = 越界(`70`)或倒序区间(`5-2`),是【没读懂】不是【永不 fire】。\n // 返回空集会让 setsIntersect 判 false → distinct(fail-open);返回 null → unverified,交给人核。\n return out.size === 0 ? null : out\n}\n\nfunction setsIntersect(a: Set<number>, b: Set<number>): boolean {\n for (const v of a) if (b.has(v)) return true\n return false\n}\n\ninterface ParsedCron { min: Set<number>; hour: Set<number>; dom: Set<number>; mon: Set<number>; dow: Set<number>; domStar: boolean; dowStar: boolean }\n\nfunction parseCron(cron: string): ParsedCron | null {\n const f = cron.trim().split(/\\s+/)\n if (f.length !== 5) return null\n const min = expandCronField(f[0], 59)\n const hour = expandCronField(f[1], 23)\n const dom = expandCronField(f[2], 31)\n const mon = expandCronField(f[3], 12, MON_NAMES)\n const dow = expandCronField(f[4], 7, DOW_NAMES)\n if (!min || !hour || !dom || !mon || !dow) return null\n return { min, hour, dom, mon, dow, domStar: f[2] === '*', dowStar: f[4] === '*' }\n}\n\n// robfig/cron v3 的日匹配:dom 和 dow 都受限时用 OR,否则各自 AND(* 视为全通过)。\nfunction dayMatch(p: ParsedCron, dom: number, dowV: number): boolean {\n if (!p.domStar && !p.dowStar) return p.dom.has(dom) || p.dow.has(dowV)\n return (p.domStar || p.dom.has(dom)) && (p.dowStar || p.dow.has(dowV))\n}\n\nconst isLeap = (y: number): boolean => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0\nconst DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\nfunction daysInMonth(y: number, m: number): number {\n return m === 2 && isLeap(y) ? 29 : DAYS_IN_MONTH[m - 1]\n}\n\n/**\n * 两条节律会不会在【同一时刻】fire。返回 collide / distinct / unverified。\n * 判据【按真实 occurrence】算,不做简化字段交集——robfig 的 dom/dow 是 OR 语义、支持名字字面量,\n * 简化交集会误判(Multica 用 robfig/cron v3,`0 9 * * MON-FRI` 必须能识别)。\n * - 分/时/月任一集合不相交 → 一定不同刻 → distinct(便宜的早退)。\n * - 否则枚举【完整的 400 年格里高利周期】找同一天两条都 active。为什么必须整 400 年而不是几年窗口:\n * 像 `0 9 29 2 1` 与 `0 9 30 2 3` 的首个共同 fire 落在 2040-02-29,短窗口会漏判成 distinct(假阴性→漏真双发)。\n * 公历 (月,日,星期) 组合每 400 年完整重复,所以枚举一整轮 = 完备,\"整轮无碰撞\"才敢判 distinct。\n * - 任一字段解析不出(扩展语法)→ unverified:**绝不当 distinct**,交给人核。\n * export:给 scripts/check-schedule.mjs 做回归断言。\n */\nexport function firesCollide(cronA: string, cronB: string): 'collide' | 'distinct' | 'unverified' {\n const a = parseCron(cronA)\n const b = parseCron(cronB)\n if (!a || !b) return 'unverified'\n if (!setsIntersect(a.min, b.min) || !setsIntersect(a.hour, b.hour) || !setsIntersect(a.mon, b.mon)) {\n return 'distinct'\n }\n // 整 400 年周期手工走(不 new Date,避免 14 万次分配)。2000-01-01 是周六(dow=6)。\n let dow = 6\n for (let y = 2000; y < 2400; y++) {\n for (let m = 1; m <= 12; m++) {\n const monA = a.mon.has(m)\n const monB = b.mon.has(m)\n const dim = daysInMonth(y, m)\n if (!(monA && monB)) { dow = (dow + dim) % 7; continue } // 该月两条不同时活跃,跳过但推进 dow\n for (let dom = 1; dom <= dim; dom++) {\n if (dayMatch(a, dom, dow) && dayMatch(b, dom, dow)) return 'collide'\n dow = (dow + 1) % 7\n }\n }\n }\n return 'distinct'\n}\n\nfunction shellQuote(value: string): string {\n return `\"${value.replace(/([\"\\\\$`])/g, '\\\\$1')}\"`\n}\n\nfunction envVar(ref: string): string {\n // \"env:MULTICA_FDE_AGENT_ID\" -> \"$MULTICA_FDE_AGENT_ID\"\n return ref.startsWith('env:') ? `$${ref.slice(4)}` : ref\n}\n\nfunction multicaApplyCommand(\n schedule: DeclaredSchedule, profile: string, agentRef: string,\n): string {\n const parts = [\n 'python3 \"$MULTICA_EXT\" autopilot-create',\n `--profile ${profile}`,\n `--agent ${agentRef}`,\n `--title ${shellQuote(schedule.name)}`,\n `--mode ${MULTICA_MODE[schedule.mode]}`,\n `--cron ${shellQuote(schedule.cron)}`,\n `--timezone ${schedule.timezone}`,\n `--description ${shellQuote(schedule.wake || schedule.name)}`,\n ]\n return parts.join(' ')\n}\n\n/**\n * Build the reconcile plan for one workspace's provider. Returns supported:false\n * (with a reason) when the target platform does not offer schedules yet — the\n * declared rhythms are then a recorded gap, never a silent downgrade.\n */\nexport function buildSchedulePlan(info: AgentProjectInfo, workspaceName: string): SchedulePlan {\n const workspace = info.workspaces.find((item) => item.name === workspaceName)\n if (!workspace) throw new Error(`Project 未声明 Workspace: ${workspaceName}`)\n const declared = info.project.schedules\n const provider = workspace.provider.kind\n const analysis = analyzeSchedules(declared) // 撞车/未验证/缺判据——manifest 级,与 provider 无关\n\n if (provider === 'multica') {\n const profile = workspace.provider.profile || ''\n if (!profile) {\n throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile`)\n }\n // Agent 绑定来源与 deploy 同一份回退链(multicaAgentIdSource):黄金路径下\n // scaffold 故意省略 agentIdFrom、首次 deploy 把真实 ID 写进 state——schedule\n // 此前不读 state,导致部署完的 Agent 声明节律时还被要求手工接线(E-03)。\n const source = multicaAgentIdSource(workspace)\n if (source.kind === 'none') {\n throw new Error(\n `Workspace ${workspaceName} 缺 Agent 绑定:manifest 未声明 provider.agentIdFrom,` +\n `且尚无部署 state(.dingtalk-agent/state/workspaces/${workspaceName}.json)。` +\n `先 dta deploy --workspace ${workspaceName} 创建 Agent(ID 会写入 state 自动复用),` +\n '或在 manifest 显式声明 provider.agentIdFrom')\n }\n const agentRef = source.kind === 'declared' ? envVar(source.ref) : source.agentId\n const agentIdSource = source.kind === 'declared' ? 'declared-env' as const : 'deploy-state' as const\n const schedules = declared.map((schedule) => ({\n ...schedule,\n applyCommand: multicaApplyCommand(schedule, profile, agentRef),\n metered: isMetered(schedule.mode),\n }))\n return {\n $schema: 'dingtalk-agent/schedule-plan@2',\n workspace: workspaceName, provider, supported: true, count: schedules.length,\n agentIdSource, schedules,\n ...analysis,\n reconcile: '由 dta-ops-multica 技能执行:先 autopilot-list 读线上,按 title 匹配,' +\n '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py' +\n (agentIdSource === 'declared-env'\n ? ',$…_AGENT_ID 由受信环境注入。'\n : ';Agent ID 来自本项目部署 state(首次 deploy 写入)。') +\n 'autopilot-create 非幂等,务必先 list 再建,别盲建(会造重复→重复外发)。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n }\n\n // opencode / local dev / not-yet-supported platforms: no schedule capability.\n const reason = provider === 'opencode'\n ? 'opencode 是本地开发运行时,不承载常驻节律;节律需部署到托管平台'\n : `provider ${provider} 尚未提供 schedule 能力`\n return {\n $schema: 'dingtalk-agent/schedule-plan@2',\n workspace: workspaceName, provider, supported: false, reason,\n count: declared.length, schedules: [],\n // provider 不支持,也照样报 manifest 自身的问题(撞车/未验证/缺判据),和平台支不支持无关。\n ...analysis,\n reconcile: `记为缺口:声明了 ${declared.length} 条节律,但目标 provider 不支持——` +\n '别静默降级成\"那就不定时了\"。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n\nexport interface ScheduleShow {\n $schema: 'dingtalk-agent/schedule-show@2'\n count: number\n schedules: DeclaredSchedule[]\n // 会同一时刻并发 fire 的节律对,如 \"轻拍×补拍\"。外发拍的送达回执是发完才写、没有锁,\n // 同刻并发会重复送第三方,所以这是必须静态拦下的一条。\n firesDistinct: boolean\n collidingPairs: string[]\n // 无法静态确认是否同刻的对(跨时区、或 cron 有本解析器读不懂的扩展语法)。\n // 【绝不当成 distinct】——只是本地静态检查证明不了,要人核或走 provider occurrence 预览。\n unverifiedPairs: string[]\n // 声明了 cron 却没写数据派生完成判据(completion)的节律名——见 SchedulePlan.missingCompletion。\n missingCompletion: string[]\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n/**\n * Declared schedules + 两条与平台无关、能静态检的不变量:\n * 1) 两拍不得在【同一时刻】并发 fire。按【真实 occurrence】判(见 firesCollide:robfig dom/dow OR、\n * 名字字面量、闰年窗口枚举),不是简化字段交集。判不准的(跨时区/扩展语法)进 unverifiedPairs,不当 distinct。\n * 2) 每条节律都要有数据派生的完成判据(completion),否则只能靠\"那一拍跑没跑\"判完成,漏拍就永久丢一期。\n * ⚠️ 这两条都只看【本地声明的 manifest】,不查平台实际生效状态——线上真相要走 provider 回读。\n */\nexport function buildScheduleShow(start: string, cliVersion = ''): ScheduleShow {\n const info = inspectAgentProject(start, cliVersion)\n const declared = info.project.schedules\n const { collidingPairs, unverifiedPairs, missingCompletion } = analyzeSchedules(declared)\n return {\n $schema: 'dingtalk-agent/schedule-show@2',\n count: declared.length,\n schedules: declared,\n firesDistinct: collidingPairs.length === 0 && unverifiedPairs.length === 0,\n collidingPairs,\n unverifiedPairs,\n missingCompletion,\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  // Skill 安装生命周期由开放生态的 `npx skills` 负责。
2
2
  // dingtalk-agent 只做两件事:把内置 Skill 目录交给上游 CLI;只读验证客户端能否发现。
3
3
  // 默认(不指定 name)对全部内置 Skill 生效:基础行为 + Agent 装配 + 交付后评测;指定 name 时只操作该 Skill。
4
- import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync, rmSync } from 'node:fs';
4
+ import { existsSync, lstatSync, readdirSync, readFileSync, readlinkSync, rmSync, unlinkSync, } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { spawnSync } from 'node:child_process';
@@ -60,6 +60,10 @@ const LEGACY_BASIC_SKILL_NAMES = Object.entries(LEGACY_SKILL_RENAMES)
60
60
  export function isBasicSkillName(name) {
61
61
  return name === GLOBAL_SKILL_NAME || LEGACY_BASIC_SKILL_NAMES.includes(name);
62
62
  }
63
+ // 已经从包里删除、且没有替代品的 Skill。它们不会被任何 install 装回来,也就永远
64
+ // 不会自己消失:`dingtalk-agent-boot-multica` 在 0.1.5-beta.2 就已移除,却仍留在
65
+ // 机器上并通过 ~/.claude/skills 的符号链继续作为可用 Skill 暴露给 Host。
66
+ export const REMOVED_SKILL_NAMES = ['dingtalk-agent-boot-multica'];
63
67
  /**
64
68
  * 删除改名前留在本机的旧名副本。install/upgrade 每次都调用,幂等。
65
69
  *
@@ -76,15 +80,67 @@ export function pruneLegacySkills(home = homedir()) {
76
80
  for (const [name, replacedBy] of Object.entries(LEGACY_SKILL_RENAMES)) {
77
81
  if (!isDirectory(join(home, '.agents', 'skills', replacedBy)))
78
82
  continue;
79
- const removed = [join(home, '.agents', 'skills', name), join(home, '.claude', 'skills', name)]
80
- .filter(pathExists);
81
- if (!removed.length)
83
+ const removed = removeSkillCopies(home, name);
84
+ if (removed.length)
85
+ pruned.push({ name, replacedBy, removed });
86
+ }
87
+ // 停用的 Skill 没有「现名已落地」这道闸门可用——它本来就不该再存在于任何机器上。
88
+ for (const name of REMOVED_SKILL_NAMES) {
89
+ const removed = removeSkillCopies(home, name);
90
+ if (removed.length)
91
+ pruned.push({ name, replacedBy: '', removed });
92
+ }
93
+ return pruned;
94
+ }
95
+ function removeSkillCopies(home, name) {
96
+ const removed = [];
97
+ for (const path of [join(home, '.agents', 'skills', name), join(home, '.claude', 'skills', name)]) {
98
+ let stat;
99
+ try {
100
+ stat = lstatSync(path);
101
+ }
102
+ catch {
82
103
  continue;
83
- for (const path of removed)
104
+ }
105
+ // 符号链接必须 unlink。canonical 先删、exposure 后删,到这一步 Claude 那条链接
106
+ // 已经悬空,而 rmSync 是按**目标**去 stat 的:ENOENT 被 force 当成「本来就没有」
107
+ // 吞掉,链接原地不动。于是 Host 继续看得见这个 Skill,每轮清理还会重复上报一次。
108
+ if (stat.isSymbolicLink())
109
+ unlinkSync(path);
110
+ else
84
111
  rmSync(path, { recursive: true, force: true });
85
- pruned.push({ name, replacedBy, removed });
112
+ removed.push(path);
86
113
  }
87
- return pruned;
114
+ return removed;
115
+ }
116
+ /** 旧名还装着、现名却没装上的 Skill。它们卡在半路,不会自己往前走。 */
117
+ export function strandedLegacySkills(home = homedir()) {
118
+ return Object.entries(LEGACY_SKILL_RENAMES)
119
+ .filter(([name, replacedBy]) => pathExists(join(home, '.agents', 'skills', name)) &&
120
+ !isDirectory(join(home, '.agents', 'skills', replacedBy)))
121
+ .map(([name, replacedBy]) => ({ name, replacedBy }));
122
+ }
123
+ /**
124
+ * 把卡在旧名上的 Skill 真正迁移过去:先装现名,再删旧名。
125
+ *
126
+ * 只对**本机已经装了旧名**的 Skill 动作,所以它保留用户既有的能力,而不是凭空多装
127
+ * 东西。平台 Skill(deploy/ops)不在默认套装里,光靠 `skill install` 永远等不到自己
128
+ * 的替代品——没有这一步,升级完它们就永久停在改名前的陈旧副本上,且无人告知。
129
+ */
130
+ export function migrateStrandedLegacySkills(packageRoot, options = {}) {
131
+ const home = options.home || options.env?.HOME || process.env.HOME || homedir();
132
+ if (options.dryRun)
133
+ return [];
134
+ const migrated = [];
135
+ for (const { replacedBy } of strandedLegacySkills(home)) {
136
+ if (!INSTALLABLE_SKILL_NAMES.includes(replacedBy))
137
+ continue;
138
+ // 装现名的那一步内部就会清掉对应旧名,报告在它的 status 上——必须收下来,
139
+ // 否则迁移做了却没人说,正是「静默删目录」要避免的那种沉默。
140
+ migrated.push(...(installGlobalSkill(packageRoot, { ...options, name: replacedBy })
141
+ .prunedLegacy || []));
142
+ }
143
+ return [...migrated, ...pruneLegacySkills(home)];
88
144
  }
89
145
  const SKILLS_PACKAGE = 'skills@latest';
90
146
  const REGISTRY = 'https://registry.npmjs.org';
@@ -93,12 +149,23 @@ export function skillSuiteStatus(packageRoot, options = {}) {
93
149
  return suiteOf(BUNDLED_SKILL_NAMES.map((name) => skillStatus(packageRoot, { ...options, name })));
94
150
  }
95
151
  export function installBundledSkills(packageRoot, options = {}) {
96
- return suiteOf(BUNDLED_SKILL_NAMES
152
+ const suite = suiteOf(BUNDLED_SKILL_NAMES
97
153
  .map((name) => installGlobalSkill(packageRoot, { ...options, name })));
154
+ return withStrandedMigration(suite, packageRoot, options);
98
155
  }
99
156
  export function upgradeBundledSkills(packageRoot, options = {}) {
100
- return suiteOf(BUNDLED_SKILL_NAMES
157
+ const suite = suiteOf(BUNDLED_SKILL_NAMES
101
158
  .map((name) => upgradeGlobalSkill(packageRoot, { ...options, name })));
159
+ return withStrandedMigration(suite, packageRoot, options);
160
+ }
161
+ /**
162
+ * 套装装完之后再补一轮:默认套装只覆盖 core,卡在旧名上的平台 Skill 只能在这里
163
+ * 被带过去,否则升级完就永久停在改名前的副本上。
164
+ */
165
+ function withStrandedMigration(suite, packageRoot, options) {
166
+ const migrated = migrateStrandedLegacySkills(packageRoot, options);
167
+ const prunedLegacy = [...(suite.prunedLegacy || []), ...migrated];
168
+ return { ...suite, ...(prunedLegacy.length ? { prunedLegacy } : {}) };
102
169
  }
103
170
  export function uninstallBundledSkills(packageRoot, options = {}) {
104
171
  return suiteOf(BUNDLED_SKILL_NAMES.map((name) => uninstallGlobalSkill(packageRoot, { ...options, name })));