dsh-punky-swarm 0.3.6

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 (101) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +661 -0
  3. package/README.en.md +281 -0
  4. package/README.md +281 -0
  5. package/cordis.patch.yml +59 -0
  6. package/lib/acps/certs.js +310 -0
  7. package/lib/acps/discovery-client.js +283 -0
  8. package/lib/acps/registry-client.js +527 -0
  9. package/lib/acps/server.js +302 -0
  10. package/lib/aip/agent-descriptor.js +170 -0
  11. package/lib/aip/identity.js +469 -0
  12. package/lib/aip/tool-descriptor.js +93 -0
  13. package/lib/api.js +266 -0
  14. package/lib/artifact-types.js +48 -0
  15. package/lib/assembly/audit-blind-review.js +170 -0
  16. package/lib/assembly/schema.js +253 -0
  17. package/lib/assembly.js +41 -0
  18. package/lib/assets.js +103 -0
  19. package/lib/bridge/trajectory.js +202 -0
  20. package/lib/client.js +704 -0
  21. package/lib/comms/acps-bridge.js +219 -0
  22. package/lib/comms/aip-format.js +141 -0
  23. package/lib/comms/budget.js +124 -0
  24. package/lib/comms/mailbox.js +221 -0
  25. package/lib/comms/topic-runtime.js +76 -0
  26. package/lib/comms/topic.js +115 -0
  27. package/lib/discovery/filter.js +199 -0
  28. package/lib/discovery/schema.js +157 -0
  29. package/lib/discovery/service.js +251 -0
  30. package/lib/hot/config-watch.js +252 -0
  31. package/lib/index.js +433 -0
  32. package/lib/lock.js +69 -0
  33. package/lib/panel/batch-detail.js +163 -0
  34. package/lib/panel/batch-list.js +84 -0
  35. package/lib/panel/locales.js +93 -0
  36. package/lib/panel/main.js +270 -0
  37. package/lib/panel/stream.js +348 -0
  38. package/lib/panel/theme.js +103 -0
  39. package/lib/panel/widgets.js +63 -0
  40. package/lib/schema.d.ts +192 -0
  41. package/lib/schema.js +259 -0
  42. package/lib/schema.ts +398 -0
  43. package/lib/state/archive.js +174 -0
  44. package/lib/state/command-exec.js +148 -0
  45. package/lib/state/constants.js +37 -0
  46. package/lib/state/corrupt-registry.js +103 -0
  47. package/lib/state/event-types.js +65 -0
  48. package/lib/state/gates.d.ts +226 -0
  49. package/lib/state/gates.js +428 -0
  50. package/lib/state/gates.ts +348 -0
  51. package/lib/state/machine-rules.d.ts +14 -0
  52. package/lib/state/machine-rules.js +76 -0
  53. package/lib/state/machine-rules.ts +88 -0
  54. package/lib/state/machine.js +68 -0
  55. package/lib/state/resume.js +201 -0
  56. package/lib/state/schema-v3.d.ts +10 -0
  57. package/lib/state/schema-v3.js +57 -0
  58. package/lib/state/schema-v3.ts +80 -0
  59. package/lib/state/store.js +607 -0
  60. package/lib/state/task-utils.js +40 -0
  61. package/lib/tools/core.js +323 -0
  62. package/lib/tools/git-utils.js +44 -0
  63. package/lib/tools/lane-tools.js +455 -0
  64. package/lib/tools/log-tools.js +173 -0
  65. package/lib/tools/mailbox-tools.js +121 -0
  66. package/lib/tools/merge-agent.js +127 -0
  67. package/lib/tools/register.js +85 -0
  68. package/lib/tools/shared.js +32 -0
  69. package/lib/types/contracts.d.ts +277 -0
  70. package/lib/types/contracts.js +17 -0
  71. package/lib/types/contracts.ts +279 -0
  72. package/lib/verify/evidence.js +249 -0
  73. package/lib/verify/gate.js +161 -0
  74. package/lib/verify/mount.js +48 -0
  75. package/lib/verify/selector.js +101 -0
  76. package/lib/watch/lane-heartbeat.js +333 -0
  77. package/lib/wave-plan.d.ts +48 -0
  78. package/lib/wave-plan.js +473 -0
  79. package/lib/wave-plan.ts +465 -0
  80. package/package.json +71 -0
  81. package/presets/jiufeng/NOTICE +5 -0
  82. package/presets/jiufeng/agent.cordis.yml +275 -0
  83. package/presets/jiufeng/preset.yml +3 -0
  84. package/skills/jiufeng-team/SKILL.md +214 -0
  85. package/skills/jiufeng-team/references/constitution.md +62 -0
  86. package/skills/jiufeng-team/references/roles/coder.md +17 -0
  87. package/skills/jiufeng-team/references/roles/coordinator.md +13 -0
  88. package/skills/jiufeng-team/references/roles/designer.md +14 -0
  89. package/skills/jiufeng-team/references/roles/doc-manager.md +13 -0
  90. package/skills/jiufeng-team/references/roles/manager.md +22 -0
  91. package/skills/jiufeng-team/references/roles/reviewer.md +19 -0
  92. package/skills/jiufeng-team/references/roles/supervisor.md +13 -0
  93. package/skills/jiufeng-team/references/roles/tester.md +13 -0
  94. package/skills/jiufeng-team/references/templates/call-chain-matrix-template.md +121 -0
  95. package/skills/jiufeng-team/references/templates/endpoint-behavior-template.md +60 -0
  96. package/skills/jiufeng-team/references/templates/gap-list.json +33 -0
  97. package/skills/jiufeng-team/references/templates/leader-decision-pack.md +92 -0
  98. package/skills/jiufeng-team/references/templates/plan-template.md +61 -0
  99. package/skills/jiufeng-team/references/templates/success-pattern-seeds.md +126 -0
  100. package/skills/jiufeng-team/references/workflow.md +98 -0
  101. package/skills/jiufeng-team/scripts/check-v3-density.mjs +98 -0
@@ -0,0 +1,253 @@
1
+ /*
2
+ Copyright (C) 2025-2026 Punky
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ // 装配配置注册表统一
19
+ // 纯函数 + 数据,零运行时依赖(仅 import 本包 lib/schema.js 默认常量)。
20
+ // 能力键路径保持既有现状(aip.* / capabilities.* 两套不变)= 消费点零改动 = 向后兼容。
21
+ // 校验语义:validateCapabilities 仅对显式 enabled 的非法组合报错(fail-closed 在测试/装配验收层);
22
+ // 引擎启动侧只 warn 不炸宿主(配置错误不破坏可用性,与「默认关零破坏」一致)。
23
+
24
+ import { WATCH_DEFAULTS, TRAJECTORY_DEFAULTS, VERIFY_DEFAULTS, DISCOVERY_DEFAULTS, ACPS_DEFAULTS } from '../schema.js';
25
+
26
+ // ── 能力注册表(11 键:aip/identity/discovery/verify/watch/worktree/budget/trajectory/acps/logs/topic)──
27
+ // path = config 取值路径;default = 缺省值(7 键默认开——aip/discovery/verify/watch/worktree/budget/trajectory;
28
+ // 显式 enabled:false 可逐键关闭;4 键默认关——identity/acps/logs/topic,需显式开启);
29
+ // consumers = 既有消费点(键路径一致性依据)
30
+ // identity 键:身份体系默认关——config.aip.identity.enabled === true 时
31
+ // 调用方才激活 lib/aip/identity.js 模块 API(AIC 身份码注册 + CAI 证书发行 + 签名 + 信任链验证);
32
+ // 默认关 → 零开销零破坏;不注册新治理工具(20 工具契约不变),身份能力经模块 API 暴露。
33
+ export const CAPABILITY_REGISTRY = [
34
+ { key: 'aip', path: ['aip'], default: { enabled: true }, consumers: ['tools/register.js catalog + api.js /tools 端点'] },
35
+ // acps 键:ACPs 通讯能力——对外 mTLS 服务端点 + 内部桥。
36
+ // 默认关:acps.enabled 与 acps.endpoint.enabled 均 false 时零加载零监听(config 短路);显式开启才
37
+ // index.js 实例化 createAcpsServer(lib/acps/server.js + certs.js,node:https/tls/crypto 原生,零新依赖)。
38
+ { key: 'acps', path: ['acps'], default: ACPS_DEFAULTS, consumers: ['index.js acps endpoint 装配 + lib/acps/server.js'] },
39
+ { key: 'identity', path: ['aip', 'identity'], default: { enabled: false }, consumers: ['lib/aip/identity.js 模块 API(AIC/CAI/sign/verifyTrustChain)'] },
40
+ { key: 'discovery', path: ['capabilities', 'discovery'], default: DISCOVERY_DEFAULTS, consumers: ['index.js discovery 服务装配 + api.js /discover + /.well-known/aip 端点'] },
41
+ { key: 'verify', path: ['capabilities', 'verify'], default: VERIFY_DEFAULTS, consumers: ['index.js mountVerify 捕获 hook', 'verify/gate.js createCompletionGate(DI)'] },
42
+ { key: 'watch', path: ['capabilities', 'watch'], default: WATCH_DEFAULTS, consumers: ['index.js watchdog + watch/lane-heartbeat.js'] },
43
+ { key: 'worktree', path: ['capabilities', 'worktree'], default: { enabled: true, mergeAgent: { enabled: false, model: null, timeoutMs: 600000 } }, consumers: ['tools/lane-tools.js 三工具注册'] },
44
+ { key: 'budget', path: ['capabilities', 'budget'], default: { enabled: true, maxChainHops: 4, maxChainRoundTrips: 2 }, consumers: ['comms/budget.js + mailbox-tools.js 发送检查'] },
45
+ { key: 'trajectory', path: ['capabilities', 'trajectory'], default: TRAJECTORY_DEFAULTS, consumers: ['index.js 桥接订阅'] },
46
+ // logs 键(E3 增强,P1-01):日志导出工具注册开关——默认关(与 cordis.patch.yml 显式 logs.enabled:true 相合);
47
+ // 消费点经 readCapability(config,'logs') 缺省读取,显式 enabled:false 可关。
48
+ { key: 'logs', path: ['capabilities', 'logs'], default: { enabled: false }, consumers: ['tools/log-tools.js log_export 注册'] },
49
+ // topic 键(P1-03):comms/topic.js 预留模块开关——默认关;接线(trajectory 桥广播改经本模块等)归操作面板批次,本批不实施。
50
+ { key: 'topic', path: ['capabilities', 'topic'], default: { enabled: false }, consumers: ['comms/topic.js(预留,接线归操作面板批次)'] },
51
+ ];
52
+
53
+ // ── 互斥表(预留空表:当前能力两两可叠加;未来互斥能力登记于此)──
54
+ // 项形如 { keys: ['x', 'y'], message: '...' }:两键同时 enabled=true → validateCapabilities 报 MUTEX error
55
+ export const EXCLUSIONS = [];
56
+
57
+ // ── 反向断言权威表(对齐 jiufeng-team skill 装配表 8 角色)──
58
+ // manager 显式豁免:Leader 直系拉起、不经 wavePlan lane 角色池,不登记于装配表 roles
59
+ export const REQUIRED_ROLES = ['coordinator', 'designer', 'coder', 'tester', 'reviewer', 'supervisor', 'doc-manager'];
60
+
61
+ // ── 盲审扩展 ──
62
+ // 三角色 + 六模板键名清单;模板内容由 lib/assembly/audit-blind-review.js 提供
63
+ export const BLIND_REVIEW_ROLES = ['audit-panelist', 'audit-aggregate', 'audit-critic'];
64
+ export const BLIND_REVIEW_TEMPLATE_KEYS = ['bundle', 'panelist', 'aggregate', 'critic', 'checklist', 'config'];
65
+
66
+ // ── 内部工具 ──
67
+ // R1 热更新(lib/hot/config-watch.js)导出复用本函数做 runtime 覆盖层 deepMerge——
68
+ // 禁止复制散落实现(复制即漂移源,设计 §5.3 风险点 1);readCapability 缺省合并与本函数同源。
69
+ export function deepMerge(base, override) {
70
+ if (override === undefined || override === null) return base;
71
+ if (typeof base !== 'object' || base === null || typeof override !== 'object' || override === null) return override;
72
+ const out = { ...base };
73
+ for (const k of Object.keys(override)) {
74
+ const b = out[k];
75
+ const o = override[k];
76
+ out[k] = (b !== null && typeof b === 'object' && !Array.isArray(b)
77
+ && o !== null && typeof o === 'object' && !Array.isArray(o))
78
+ ? deepMerge(b, o)
79
+ : o;
80
+ }
81
+ return out;
82
+ }
83
+
84
+ // ── 统一读取:按注册表 path 取值 + 默认合并(消费点语义等价,供测试与工具面统一读取)──
85
+ export function readCapability(config, key) {
86
+ const entry = CAPABILITY_REGISTRY.find((e) => e.key === key);
87
+ if (!entry) return undefined;
88
+ let val = config;
89
+ for (const seg of entry.path) val = val?.[seg];
90
+ return deepMerge(entry.default, val);
91
+ }
92
+
93
+ // ── 校验:仅对显式 enabled 的非法组合报错;禁用能力零校验零开销 ──
94
+ export function validateCapabilities(config = {}) {
95
+ const errors = [];
96
+ const c = config.capabilities ?? {};
97
+
98
+ // DEP-1 verify:mode ∈ {advisory, enforce}(createCompletionGate 静默回退,统一校验兜底)
99
+ if (c.verify?.enabled === true && c.verify.mode !== undefined && !['advisory', 'enforce'].includes(c.verify.mode)) {
100
+ errors.push('DEP-1: capabilities.verify.mode must be advisory or enforce (got ' + JSON.stringify(c.verify.mode) + ')');
101
+ }
102
+
103
+ // DEP-2 trajectory:autoFail === true ⇒ enabled === true(autoFail 无 enabled 无意义)
104
+ if (c.trajectory?.autoFail === true && c.trajectory.enabled !== true) {
105
+ errors.push('DEP-2: capabilities.trajectory.autoFail=true requires enabled=true');
106
+ }
107
+
108
+ // DEP-3 trajectory:poll.enabled === true ⇒ poll.baseUrl 非空字符串(HTTP 轮询需目标地址)
109
+ if (c.trajectory?.poll?.enabled === true) {
110
+ const baseUrl = c.trajectory.poll.baseUrl;
111
+ if (typeof baseUrl !== 'string' || baseUrl.trim().length === 0) {
112
+ errors.push('DEP-3: capabilities.trajectory.poll.enabled=true requires non-empty poll.baseUrl');
113
+ }
114
+ }
115
+
116
+ // DEP-4 budget:maxChainHops / maxChainRoundTrips 为正整数(显式声明时;环防护参数合法性)
117
+ if (c.budget?.enabled === true) {
118
+ for (const field of ['maxChainHops', 'maxChainRoundTrips']) {
119
+ if (c.budget[field] !== undefined && (!Number.isInteger(c.budget[field]) || c.budget[field] < 1)) {
120
+ errors.push('DEP-4: capabilities.budget.' + field + ' must be a positive integer (got ' + JSON.stringify(c.budget[field]) + ')');
121
+ }
122
+ }
123
+ }
124
+
125
+ // DEP-5 watch:maxMissed ≥ 1、scanIntervalMinutes ≥ 0.1(显式声明时;resolveWatchConfig 已钳制,校验兜底)
126
+ if (c.watch?.enabled === true) {
127
+ if (c.watch.maxMissed !== undefined && (!Number.isFinite(c.watch.maxMissed) || c.watch.maxMissed < 1)) {
128
+ errors.push('DEP-5: capabilities.watch.maxMissed must be >= 1 (got ' + JSON.stringify(c.watch.maxMissed) + ')');
129
+ }
130
+ if (c.watch.scanIntervalMinutes !== undefined && (!Number.isFinite(c.watch.scanIntervalMinutes) || c.watch.scanIntervalMinutes < 0.1)) {
131
+ errors.push('DEP-5: capabilities.watch.scanIntervalMinutes must be >= 0.1 (got ' + JSON.stringify(c.watch.scanIntervalMinutes) + ')');
132
+ }
133
+ }
134
+
135
+ // MUTEX:互斥表(当前空表,无天然互斥;心跳/预算/worktree/verify/trajectory/aip 两两可叠加)
136
+ for (const ex of EXCLUSIONS) {
137
+ const on = ex.keys.filter((k) => readCapability(config, k)?.enabled === true);
138
+ if (on.length === ex.keys.length) errors.push('MUTEX: ' + ex.message);
139
+ }
140
+
141
+ return { errors };
142
+ }
143
+
144
+ // ── 结构校验:team/layers/roles/skills 形状;role ∈ roles ⇒ skills[role] 非空字符串数组 ──
145
+ export function validateAssembly(assembly) {
146
+ const errors = [];
147
+ if (assembly === null || typeof assembly !== 'object') {
148
+ return { errors: ['assembly must be an object'] };
149
+ }
150
+ if (typeof assembly.team !== 'string' || assembly.team.length === 0) {
151
+ errors.push('assembly.team must be a non-empty string');
152
+ }
153
+ const layers = assembly.layers;
154
+ if (layers === null || typeof layers !== 'object' || Array.isArray(layers)) {
155
+ errors.push('assembly.layers must be an object');
156
+ return { errors };
157
+ }
158
+ for (const [layerName, layer] of Object.entries(layers)) {
159
+ if (layer === null || typeof layer !== 'object') {
160
+ errors.push('layers.' + layerName + ' must be an object');
161
+ continue;
162
+ }
163
+ const roles = layer.roles;
164
+ if (!Array.isArray(roles) || roles.length === 0) {
165
+ errors.push('layers.' + layerName + '.roles must be a non-empty array');
166
+ continue;
167
+ }
168
+ const skills = layer.skills;
169
+ if (skills === null || typeof skills !== 'object' || Array.isArray(skills)) {
170
+ errors.push('layers.' + layerName + '.skills must be an object');
171
+ continue;
172
+ }
173
+ for (const role of roles) {
174
+ const list = skills[role];
175
+ if (!Array.isArray(list) || list.length === 0) {
176
+ errors.push('layers.' + layerName + '.skills.' + role + ' missing or empty');
177
+ } else {
178
+ for (const s of list) {
179
+ if (typeof s !== 'string' || s.length === 0) {
180
+ errors.push('layers.' + layerName + '.skills.' + role + ' contains non-string skill');
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ return { errors };
187
+ }
188
+
189
+ // ── 完整性断言(三视图,入测试门禁)──
190
+ // 视图 1 正向:装配表自洽(∀ (layer, role):skills[role] 非空字符串数组且每个 skill 可解析)
191
+ // 视图 2 反向:jiufeng-team 装配表 7 角色(manager 显式豁免)全部出现且映射非空
192
+ // 视图 3 扩展:extensions.blindReview.enabled=true 时三角色映射非空 + 六模板键齐备
193
+ // skillCatalog:{ has(name) -> bool };生产传 ~/.agents/skills/<name>/SKILL.md 存在性解析器,测试传 fixture
194
+ export function assertAssemblyCompleteness(assembly, skillCatalog) {
195
+ if (typeof skillCatalog?.has !== 'function') {
196
+ throw new TypeError('assertAssemblyCompleteness requires skillCatalog.has(name) — production: ~/.agents/skills/<name>/SKILL.md resolver; test: fixture');
197
+ }
198
+ const missing = [];
199
+ const layers = assembly?.layers ?? {};
200
+
201
+ // 收集全量角色 → skills(供视图 2/3 复用)
202
+ const roleSkills = new Map();
203
+ for (const layer of Object.values(layers)) {
204
+ for (const role of layer?.roles ?? []) {
205
+ roleSkills.set(role, layer?.skills?.[role]);
206
+ }
207
+ }
208
+
209
+ // 视图 1:正向自洽
210
+ for (const [layerName, layer] of Object.entries(layers)) {
211
+ for (const role of layer?.roles ?? []) {
212
+ const skills = layer?.skills?.[role];
213
+ if (!Array.isArray(skills) || skills.length === 0) {
214
+ missing.push('layers.' + layerName + '.skills.' + role + ' missing or empty');
215
+ continue;
216
+ }
217
+ for (const s of skills) {
218
+ if (!skillCatalog.has(s)) missing.push('layers.' + layerName + '.skills.' + role + ': skill "' + s + '" not resolvable');
219
+ }
220
+ }
221
+ }
222
+
223
+ // 视图 2:反向对齐 jiufeng-team 手册表(manager 豁免)
224
+ for (const role of REQUIRED_ROLES) {
225
+ if (!roleSkills.has(role)) {
226
+ missing.push('required role "' + role + '" not present in assembly roles');
227
+ } else if (!Array.isArray(roleSkills.get(role)) || roleSkills.get(role).length === 0) {
228
+ missing.push('required role "' + role + '" has empty skills');
229
+ }
230
+ }
231
+
232
+ // 视图 3:blindReview 扩展(默认不启用 → 零影响)
233
+ const br = assembly?.extensions?.blindReview;
234
+ if (br?.enabled === true) {
235
+ for (const role of BLIND_REVIEW_ROLES) {
236
+ if (!roleSkills.has(role)) {
237
+ missing.push('blindReview: role "' + role + '" not present in assembly roles');
238
+ } else if (!Array.isArray(roleSkills.get(role)) || roleSkills.get(role).length === 0) {
239
+ missing.push('blindReview: role "' + role + '" has empty skills');
240
+ }
241
+ }
242
+ const templates = br.templates;
243
+ if (templates === null || typeof templates !== 'object') {
244
+ missing.push('blindReview: templates object missing');
245
+ } else {
246
+ for (const k of BLIND_REVIEW_TEMPLATE_KEYS) {
247
+ if (!(k in templates)) missing.push('blindReview: template "' + k + '" missing');
248
+ }
249
+ }
250
+ }
251
+
252
+ return { ok: missing.length === 0, missing };
253
+ }
@@ -0,0 +1,41 @@
1
+ /*
2
+ Copyright (C) 2025-2026 Punky
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ // 可插拔装配数据(设计 §12.1/§14.2/§15.3 N7):team → layer → role → skills
19
+ // 引擎只认 "role 契约 + skill 前缀" 通用格式,不感知 team;非 jiufeng 团队 = 换装配(外部路径 config.assembly 或自定义 resolveAssembly)
20
+ export const DEFAULT_ASSEMBLY = {
21
+ team: 'jiufeng',
22
+ layers: {
23
+ plan: {
24
+ roles: ['coordinator', 'designer'],
25
+ skills: { coordinator: ['dev-planner'], designer: ['dev-designer', 'spec-writing', 'design-an-interface'] },
26
+ },
27
+ exec: {
28
+ roles: ['coder', 'tester'],
29
+ skills: { coder: ['dev-coder', 'efficient-edit', 'codebase-design'], tester: ['dev-tester'] },
30
+ },
31
+ audit: {
32
+ roles: ['reviewer', 'supervisor', 'doc-manager'],
33
+ skills: { reviewer: ['code-review-guideline', 'report-blind-audit'], supervisor: ['report-blind-audit', 'archive'], 'doc-manager': ['doc-generator', 'doc-update'] },
34
+ },
35
+ },
36
+ };
37
+
38
+ export function resolveAssembly(team, configAssembly = null) {
39
+ if (configAssembly) return configAssembly;
40
+ return team === 'jiufeng' ? DEFAULT_ASSEMBLY : null;
41
+ }
package/lib/assets.js ADDED
@@ -0,0 +1,103 @@
1
+ /*
2
+ Copyright (C) 2025-2026 Punky
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ // 资产同步:把包内 presets/ 与 skills/ 同步到用户目录(参照 dsh-liangshen 语义)
19
+ // 幂等:目标目录字节一致则跳过(current),否则整体覆盖(synced);只动插件自有目录,不碰用户其他预设/技能。
20
+ import { existsSync, mkdirSync, cpSync, rmSync, readdirSync, statSync, readFileSync } from 'node:fs'
21
+ import { homedir } from 'node:os'
22
+ import { dirname, join, relative } from 'node:path'
23
+ import { fileURLToPath } from 'node:url'
24
+
25
+ const MTIME_TOLERANCE_MS = 1000
26
+
27
+ /** 包根目录(lib/assets.js -> 包根)。 */
28
+ export function packageRoot() {
29
+ return fileURLToPath(new URL('../', import.meta.url))
30
+ }
31
+
32
+ function filesUnder(root) {
33
+ const out = []
34
+ const walk = (dir) => {
35
+ for (const entry of readdirSync(dir)) {
36
+ const p = join(dir, entry)
37
+ if (statSync(p).isDirectory()) walk(p)
38
+ else out.push(p)
39
+ }
40
+ }
41
+ walk(root)
42
+ return out
43
+ }
44
+
45
+ /** 文件字节一致性(size + mtime 容差快速否定,等价字节比对兜底)。 */
46
+ function sameFile(a, b) {
47
+ const sa = statSync(a)
48
+ const sb = statSync(b)
49
+ if (sa.size !== sb.size) return false
50
+ if (Math.abs(sa.mtimeMs - sb.mtimeMs) > MTIME_TOLERANCE_MS) return false
51
+ return readFileSync(a).equals(readFileSync(b))
52
+ }
53
+
54
+ /** 目录幂等同步:'synced'(已写入/覆盖)| 'current'(已是最新)。 */
55
+ export function syncDir(sourceDir, targetDir) {
56
+ if (existsSync(targetDir) && !statSync(targetDir).isDirectory()) {
57
+ rmSync(targetDir, { recursive: true, force: true })
58
+ }
59
+ if (!existsSync(targetDir)) {
60
+ cpSync(sourceDir, targetDir, { recursive: true, preserveTimestamps: true })
61
+ return 'synced'
62
+ }
63
+ for (const file of filesUnder(sourceDir)) {
64
+ const dest = join(targetDir, relative(sourceDir, file))
65
+ if (!existsSync(dest) || !sameFile(file, dest)) {
66
+ rmSync(targetDir, { recursive: true, force: true })
67
+ cpSync(sourceDir, targetDir, { recursive: true, preserveTimestamps: true })
68
+ return 'synced'
69
+ }
70
+ }
71
+ return 'current'
72
+ }
73
+
74
+ /**
75
+ * 同步预设与技能到用户目录。
76
+ * @param opts.home - 用户主目录(测试可注入);缺省 homedir()。
77
+ * @param opts.packageRoot - 包根(测试可注入);缺省按 import.meta.url 解析。
78
+ * @returns [{asset, status: 'synced'|'current'|'missing-source'|'failed', error?}]
79
+ */
80
+ export function syncAssets(opts = {}) {
81
+ const home = opts.home ?? homedir()
82
+ const root = opts.packageRoot ?? packageRoot()
83
+ const jobs = [
84
+ { rel: 'presets/jiufeng', target: join(home, '.dsh', '.agent-presets', 'jiufeng') },
85
+ { rel: 'skills/jiufeng-team', target: join(home, '.agents', 'skills', 'jiufeng-team') },
86
+ ]
87
+ const results = []
88
+ for (const job of jobs) {
89
+ const src = join(root, job.rel)
90
+ if (!existsSync(src)) {
91
+ results.push({ asset: job.rel, status: 'missing-source' })
92
+ continue
93
+ }
94
+ try {
95
+ mkdirSync(dirname(job.target), { recursive: true })
96
+ const status = syncDir(src, job.target)
97
+ results.push({ asset: job.rel, status })
98
+ } catch (error) {
99
+ results.push({ asset: job.rel, status: 'failed', error: error instanceof Error ? error.message : String(error) })
100
+ }
101
+ }
102
+ return results
103
+ }
@@ -0,0 +1,202 @@
1
+ /*
2
+ Copyright (C) 2025-2026 Punky
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ // bridge/trajectory.js —— C5 诊断桥接(成熟模式:LangGraph 观测层 / dsh-trajectory-governance)
19
+ // 订阅 trajectory 异常(anomaly)→ sessionId→lane 映射 → notify(默认 notify-only,auto-fail 默认关)
20
+ // 原则:只读消费告警载荷、不 mutate 载荷、不碰内核、不调插件任何控制面动作
21
+ // 事件契约:'member.dispatch' 记录派发映射(既有事件通道,零新字段);'lane.anomaly' 记录异常证据;broadcast 提示 Manager
22
+ import { join } from 'node:path';
23
+ import { TRAJECTORY_DEFAULTS } from '../schema.js';
24
+ import { readCapability } from '../assembly/schema.js'; // P1-01:装配开关经注册表 default 缺省合并
25
+
26
+ const ANOMALY_EVENT = 'trajectory/anomaly'; // 首选通道:同运行时实时订阅(Cordis 总线事件)
27
+ const DISPATCH_EVENT = 'member.dispatch'; // 派发时记录 worker 会话 id(桥接器 recordDispatch 写入)
28
+ const ANOMALY_RECORD = 'lane.anomaly'; // 异常留痕事件(batch.events,面板可见)
29
+ const MAILBOX_KIND = 'anomaly'; // broadcast 消息 kind
30
+
31
+ // 装配开关(P1-01:缺省默认开——readCapability 合并注册表 default {enabled:true};
32
+ // 显式 capabilities.trajectory.enabled:false 可关,false 时桥接不挂载,零运行时开销)
33
+ export function isTrajectoryEnabled(config) {
34
+ return readCapability(config, 'trajectory')?.enabled === true;
35
+ }
36
+
37
+ export function createTrajectoryBridge(ctx, deps) {
38
+ const { store, config = {}, mailbox } = deps;
39
+ const trajCfg = config?.capabilities?.trajectory ?? {};
40
+ const failConfidence = typeof trajCfg.failConfidence === 'number'
41
+ ? trajCfg.failConfidence
42
+ : TRAJECTORY_DEFAULTS.failConfidence;
43
+
44
+ // sessionId(worker 会话) → { sessionId, batchId, lane };
45
+ // 派发时经 recordDispatch 记录;进程重启后从批次事件重建(幂等)
46
+ const laneBySession = new Map();
47
+ let pollTimer = null;
48
+ let lastPollSince = 0;
49
+ let subscribed = false;
50
+ let detach = null; // ctx.on 返回的退订函数(若存在)
51
+
52
+ function mailboxRootOf(sessionId, batchId) {
53
+ // 与 tools/mailbox-tools.js boxRoot 同源:<root>/sessions/<sessionId>/mailbox/<batchId>
54
+ return join(store.sessionsDir, sessionId, 'mailbox', batchId);
55
+ }
56
+
57
+ // 重启重建:扫全部批次事件,幂等(重复调用只覆盖不膨胀)
58
+ function rebuildFromEvents() {
59
+ let n = 0;
60
+ for (const { sessionId, batchId } of store.listAllBatches()) {
61
+ const batch = store.readBatch(sessionId, batchId);
62
+ if (!batch?.events) continue;
63
+ for (const ev of batch.events) {
64
+ if (ev.type === DISPATCH_EVENT && ev.workerSessionId && ev.lane) {
65
+ laneBySession.set(ev.workerSessionId, { sessionId, batchId, lane: ev.lane });
66
+ n++;
67
+ }
68
+ }
69
+ }
70
+ return n;
71
+ }
72
+
73
+ // 派发记录(member_status → running 派发时的 worker 会话 id 落事件;零新字段,store.appendEvent 既有通道)
74
+ function recordDispatch({ sessionId, batchId, lane, workerSessionId }) {
75
+ if (!sessionId || !batchId || !lane || !workerSessionId) {
76
+ throw new Error('recordDispatch: sessionId/batchId/lane/workerSessionId 均必填');
77
+ }
78
+ store.appendEvent(sessionId, batchId, DISPATCH_EVENT, { lane, workerSessionId });
79
+ laneBySession.set(workerSessionId, { sessionId, batchId, lane });
80
+ return { ok: true, workerSessionId, batchId, lane };
81
+ }
82
+
83
+ // 只读快照:sessionId → { sessionId, batchId, lane }(gate_status 可选 advisory 展示用,不改门禁语义)
84
+ function mapping() {
85
+ const out = {};
86
+ for (const [k, v] of laneBySession) out[k] = { ...v };
87
+ return out;
88
+ }
89
+
90
+ // notify(默认 notify-only):lane.anomaly 事件 + broadcast 提示 Manager;不自动结算(Manager 按纪律裁决)
91
+ // 注:store.newEvent 展开为 { ts, type, ...fields }——载荷若含顶层 type 会覆盖事件类型(store.js:56-58),
92
+ // 故 AnomalyAlert 载荷嵌套于 alert 字段(载荷原样保留,事件 type 恒为 lane.anomaly)
93
+ function notify(hit, alert) {
94
+ const { sessionId, batchId, lane } = hit;
95
+ const { anomalyId, type, confidence, severity, message } = alert;
96
+ store.appendEvent(sessionId, batchId, ANOMALY_RECORD, { lane, alert: { anomalyId, type, confidence, severity, message } });
97
+ mailbox.send(mailboxRootOf(sessionId, batchId), { type: 'broadcast' }, {
98
+ kind: MAILBOX_KIND,
99
+ anomalyId, type, confidence, severity, message,
100
+ sessionId, batchId, lane,
101
+ ts: new Date().toISOString(),
102
+ }, { from: 'trajectory-bridge' });
103
+ }
104
+
105
+ // auto-fail 开关(默认关):仅 loop_deadlock 且 confidence ≥ 阈值(可配)→ member_settle failed 终态结算
106
+ // 不自动重试、不自动开新批次、不调插件 fork/auto-stop;settle 判定权仍在 Manager/门禁
107
+ function maybeAutoFail(hit, alert) {
108
+ const { sessionId, batchId, lane } = hit;
109
+ const { anomalyId, type, confidence, message } = alert;
110
+ if (trajCfg.autoFail !== true) return false;
111
+ if (type !== 'loop_deadlock') return false;
112
+ if (confidence < failConfidence) return false;
113
+ try {
114
+ store.setMember(sessionId, batchId, lane, 'failed',
115
+ `anomalyId=${anomalyId}, 证据摘要(type=${type}, confidence=${confidence}${message ? ', ' + message : ''})`);
116
+ return true;
117
+ } catch (e) {
118
+ // 状态机不合法(lane 非 running/review)→ 跳过自动结算,notify 证据已留
119
+ ctx.logger?.warn?.('[trajectory] auto-fail skipped: ' + String(e?.message ?? e));
120
+ return false;
121
+ }
122
+ }
123
+
124
+ // 告警载荷归一化(AnomalyAlert 契约:anomalyId/sessionId/type/confidence/severity/message)
125
+ function normalizeAlert(raw) {
126
+ const confidence = Number(raw?.confidence);
127
+ return {
128
+ anomalyId: raw?.anomalyId ?? ('anomaly-' + Date.now()),
129
+ sessionId: raw?.sessionId ?? null,
130
+ type: raw?.type ?? 'unknown',
131
+ confidence: Number.isFinite(confidence) ? confidence : 0,
132
+ severity: raw?.severity ?? 'warning',
133
+ message: raw?.message ?? '',
134
+ };
135
+ }
136
+
137
+ // 异常入口:反查 lane → notify(+ 可选 auto-fail);未映射/无 sessionId → 静默降级(不报错不轮询)
138
+ function handleAnomaly(rawAlert) {
139
+ const alert = normalizeAlert(rawAlert);
140
+ if (!alert.sessionId) {
141
+ ctx.logger?.warn?.('[trajectory] anomaly without sessionId, dropped');
142
+ return { routed: false, reason: 'no-session' };
143
+ }
144
+ const hit = laneBySession.get(alert.sessionId);
145
+ if (!hit) {
146
+ ctx.logger?.warn?.('[trajectory] unmapped session: ' + alert.sessionId);
147
+ return { routed: false, reason: 'unmapped' };
148
+ }
149
+ notify(hit, alert);
150
+ const autoFailed = maybeAutoFail(hit, alert);
151
+ return { routed: true, batchId: hit.batchId, lane: hit.lane, autoFailed };
152
+ }
153
+
154
+ // fallback:HTTP 轮询 /api/alerts?since=(可配,默认关;插件未装/无事件时静默降级)
155
+ async function pollAlerts() {
156
+ const baseUrl = trajCfg.poll?.baseUrl;
157
+ if (!baseUrl) return;
158
+ try {
159
+ const res = await fetch(baseUrl + '/api/alerts?since=' + lastPollSince);
160
+ if (!res.ok) return;
161
+ const alerts = await res.json();
162
+ if (Array.isArray(alerts)) {
163
+ for (const a of alerts) handleAnomaly(a);
164
+ lastPollSince = Date.now();
165
+ }
166
+ } catch (e) {
167
+ ctx.logger?.warn?.('[trajectory] poll failed (silent degrade): ' + String(e?.message ?? e));
168
+ }
169
+ }
170
+
171
+ function start() {
172
+ rebuildFromEvents(); // 幂等:重启后从批次事件重建映射
173
+ if (typeof ctx?.on === 'function') {
174
+ const handler = (a) => {
175
+ try { handleAnomaly(a); } catch (e) { ctx.logger?.warn?.('[trajectory] handler error: ' + String(e?.message ?? e)); }
176
+ };
177
+ detach = ctx.on(ANOMALY_EVENT, handler);
178
+ subscribed = true;
179
+ }
180
+ if (trajCfg.poll?.enabled === true) {
181
+ const intervalMs = trajCfg.poll?.intervalMs ?? TRAJECTORY_DEFAULTS.poll.intervalMs;
182
+ pollTimer = setInterval(() => { pollAlerts().catch(() => {}); }, intervalMs);
183
+ }
184
+ return { subscribed, poll: trajCfg.poll?.enabled === true };
185
+ }
186
+
187
+ function stop() {
188
+ if (subscribed && detach) {
189
+ try { detach(); } catch {}
190
+ }
191
+ if (subscribed && typeof ctx?.off === 'function') {
192
+ // 兼容 ctx.on 未返回退订函数、仅提供 off 的宿主
193
+ try { ctx.off(ANOMALY_EVENT); } catch {}
194
+ }
195
+ subscribed = false;
196
+ detach = null;
197
+ if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
198
+ return { stopped: true };
199
+ }
200
+
201
+ return { start, stop, mapping, recordDispatch, handleAnomaly, rebuildFromEvents };
202
+ }