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,157 @@
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
+ // 发现协议(ADP,Agent Discovery Protocol)常量与请求校验 — 独立模块,核心无感知。
19
+ // 基准:ACPs-community v2.1.0(acps-sdk/adp + discovery-server/app/discovery,06-ACPs-spec-ADP)。
20
+ // 字段名以参考实现逐字为准(lowerCamelCase),不臆造。纯函数 + 数据,零运行时依赖。
21
+
22
+ // ── 查询类型(QUERY_TYPES,与 acps_sdk/adp/constants.py 一致)──
23
+ export const QUERY_TYPES = ['explicit', 'exploratory', 'trending', 'filtered'];
24
+ export const QUERY_TYPE_EXPLICIT = 'explicit';
25
+
26
+ // ── 转发限制(FORWARD_*,与 acps_sdk/adp/constants.py 一致)──
27
+ export const FORWARD_DEPTH_LIMIT_DEFAULT = 3; // 服务器默认
28
+ export const FORWARD_DEPTH_LIMIT_MIN = 1;
29
+ export const FORWARD_DEPTH_LIMIT_MAX = 5; // 绝对上限
30
+ export const FORWARD_FANOUT_LIMIT_DEFAULT = 1; // 未提供时默认不允许并发转发
31
+ export const FORWARD_FANOUT_LIMIT_MIN = 1;
32
+ export const FORWARD_FANOUT_LIMIT_MAX = 5;
33
+ export const FORWARD_EACH_TIMEOUT_MS_DEFAULT = 10_000;
34
+ export const FORWARD_TOTAL_TIMEOUT_MS_DEFAULT = 60_000;
35
+
36
+ // ── 查询结果限制(服务器默认 5,上限 50,同 discovery-server/schema.py)──
37
+ export const DISCOVERY_LIMIT_DEFAULT = 5;
38
+ export const DISCOVERY_LIMIT_MAX = 50;
39
+
40
+ // ── 过滤运算符全集(FilterOperator,06-ACPs-spec-ADP §4.2.1 逐字 34 个)──
41
+ export const FILTER_OPERATORS = [
42
+ // 通用:等值与存在性
43
+ 'eq', 'ne', 'exists',
44
+ // 比较:数值、日期、字符串字典序
45
+ 'gt', 'gte', 'lt', 'lte', 'between',
46
+ // 集合:值列表匹配
47
+ 'in', 'nin',
48
+ // 字符串:模式匹配(默认大小写不敏感)
49
+ 'contains', 'notContains', 'startsWith', 'endsWith',
50
+ // 字符串:大小写敏感变体(Cs = Case Sensitive)
51
+ 'eqCs', 'neCs', 'inCs', 'ninCs', 'containsCs', 'notContainsCs', 'startsWithCs', 'endsWithCs',
52
+ // 数组:集合运算
53
+ 'anyOf', 'allOf', 'noneOf', 'size', 'sizeGt', 'sizeGte', 'sizeLt', 'sizeLte',
54
+ // Map/对象:键检查
55
+ 'hasKey', 'hasNoKey', 'hasAnyKey', 'hasAllKeys',
56
+ ];
57
+
58
+ // ── ADP 错误码(06-ACPs-spec-ADP §4.1.2 逐字)──
59
+ export const ADP_ERROR = {
60
+ MISSING_QUERY: { code: 40001, message: 'MissingQuery' },
61
+ FORWARD_DEPTH_LIMIT_INVALID: { code: 40002, message: 'ForwardDepthLimitInvalid' },
62
+ FORWARD_CHAIN_INVALID: { code: 40003, message: 'ForwardChainInvalid' },
63
+ FILTER_INVALID: { code: 40004, message: 'FilterInvalid' },
64
+ FORWARD_FANOUT_LIMIT_INVALID: { code: 40005, message: 'ForwardFanoutLimitInvalid' },
65
+ BAD_REQUEST: { code: 40000, message: 'BadRequest' },
66
+ INTERNAL_ERROR: { code: 50001, message: 'InternalError' },
67
+ };
68
+
69
+ // ── 请求校验(对齐 discovery-server validator.py + schema.py 语义)──
70
+ // 返回 { error } 或 null。error = { code, message, data? }(CommonResponse.error 形态)
71
+ export function validateDiscoveryRequest(raw = {}) {
72
+ const error = (e, data) => ({ error: { code: e.code, message: e.message, data } });
73
+
74
+ // type 合法性
75
+ const type = raw.type ?? QUERY_TYPE_EXPLICIT;
76
+ if (!QUERY_TYPES.includes(type)) {
77
+ return error(ADP_ERROR.BAD_REQUEST, '无效的查询类型: ' + JSON.stringify(type) + ',允许: ' + QUERY_TYPES.join('/'));
78
+ }
79
+
80
+ // explicit 必填 query(SDK DiscoveryRequest.validate_explicit_query)
81
+ if (type === QUERY_TYPE_EXPLICIT && !(typeof raw.query === 'string' && raw.query.trim().length > 0)) {
82
+ return error(ADP_ERROR.MISSING_QUERY, "type='explicit' 时 query 字段必填且不能为空字符串");
83
+ }
84
+
85
+ // filtered 必填 filter(reference _handle_filtered_discovery 语义:40000 BadRequest)
86
+ if (type === 'filtered' && !raw.filter) {
87
+ return error(ADP_ERROR.BAD_REQUEST, 'filtered 查询必须提供 filter 参数');
88
+ }
89
+
90
+ // forwardDepthLimit 1..5(validator.py:不在 1-5 区间 → 40002)
91
+ if (raw.forwardDepthLimit !== undefined && raw.forwardDepthLimit !== null
92
+ && (raw.forwardDepthLimit < FORWARD_DEPTH_LIMIT_MIN || raw.forwardDepthLimit > FORWARD_DEPTH_LIMIT_MAX)) {
93
+ return error(ADP_ERROR.FORWARD_DEPTH_LIMIT_INVALID, 'forwardDepthLimit 不在 ' + FORWARD_DEPTH_LIMIT_MIN + '-' + FORWARD_DEPTH_LIMIT_MAX + ' 区间');
94
+ }
95
+
96
+ // forwardFanoutLimit 1..5
97
+ if (raw.forwardFanoutLimit !== undefined && raw.forwardFanoutLimit !== null
98
+ && (raw.forwardFanoutLimit < FORWARD_FANOUT_LIMIT_MIN || raw.forwardFanoutLimit > FORWARD_FANOUT_LIMIT_MAX)) {
99
+ return error(ADP_ERROR.FORWARD_FANOUT_LIMIT_INVALID, 'forwardFanoutLimit 不在 ' + FORWARD_FANOUT_LIMIT_MIN + '-' + FORWARD_FANOUT_LIMIT_MAX + ' 区间');
100
+ }
101
+
102
+ // forwardChain AIC 安全(validator.py validata_aics_safe)
103
+ if (raw.forwardChain !== undefined && raw.forwardChain !== null) {
104
+ const chain = Array.isArray(raw.forwardChain) ? raw.forwardChain : [raw.forwardChain];
105
+ if (!chain.every((aic) => typeof aic === 'string' && aic.trim().length > 0)) {
106
+ return error(ADP_ERROR.FORWARD_CHAIN_INVALID, 'forwardChain 包含非法 AIC');
107
+ }
108
+ }
109
+
110
+ // filter 结构与运算符合法性(spec:不合法条件 → 40004 FilterInvalid)
111
+ if (raw.filter !== undefined && raw.filter !== null) {
112
+ const fe = validateFilterShape(raw.filter);
113
+ if (fe) return error(ADP_ERROR.FILTER_INVALID, fe);
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ // 过滤条件结构校验:field 非空字符串、op ∈ 运算符全集、value 可选
120
+ export function validateFilterShape(filter, depth = 0) {
121
+ if (filter === null || typeof filter !== 'object' || Array.isArray(filter)) return 'filter 必须是对象';
122
+ if (depth > 3) return 'filter 嵌套深度超过建议上限 3 层';
123
+
124
+ const conditions = Array.isArray(filter.conditions) ? filter.conditions : [];
125
+ const groups = Array.isArray(filter.groups) ? filter.groups : [];
126
+ if (filter.logic !== undefined && !['and', 'or', 'not'].includes(filter.logic)) {
127
+ return "logic 必须是 'and' | 'or' | 'not'";
128
+ }
129
+ for (const c of conditions) {
130
+ if (c === null || typeof c !== 'object') return 'condition 必须是对象';
131
+ if (typeof c.field !== 'string' || c.field.trim().length === 0) return 'condition.field 必须是非空字符串';
132
+ if (!FILTER_OPERATORS.includes(c.op)) return 'condition.op 非法: ' + JSON.stringify(c.op);
133
+ }
134
+ for (const g of groups) {
135
+ const ge = validateFilterShape(g, depth + 1);
136
+ if (ge) return ge;
137
+ }
138
+ return null;
139
+ }
140
+
141
+ // limit 归一化:缺省 DISCOVERY_LIMIT_DEFAULT,钳制 [1, DISCOVERY_LIMIT_MAX]
142
+ export function normalizeLimit(limit) {
143
+ if (!Number.isFinite(Number(limit))) return DISCOVERY_LIMIT_DEFAULT;
144
+ const n = Math.floor(Number(limit));
145
+ if (n < 1) return 1;
146
+ if (n > DISCOVERY_LIMIT_MAX) return DISCOVERY_LIMIT_MAX;
147
+ return n;
148
+ }
149
+
150
+ // 成功/失败响应构造(DiscoveryResponse:result 与 error 互斥,reference DiscoveryResponse.success/failure)
151
+ export function successResponse(result) {
152
+ return { result };
153
+ }
154
+
155
+ export function failureResponse(code, message, data) {
156
+ return { error: { code, message, data } };
157
+ }
@@ -0,0 +1,251 @@
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
+ // ADP 发现服务(Agent Discovery Protocol):独立模块,核心无感知。
19
+ // 消费 tool-descriptor catalog(工具目录)与 agent-descriptor 目录(智能体描述),
20
+ // 提供 POST /discover 语义查询(type: explicit/exploratory/trending/filtered)+ .well-known 预置。
21
+ // 基准:ACPs-community v2.1.0(discovery-server/app/discovery/service.py 响应构造 + acps_sdk/adp 模型)。
22
+ // active 语义:装配可配 discovery.enabled(默认开);节点 active=false 不出现在查询结果(读 config 键,默认 active=true)。
23
+
24
+ import {
25
+ QUERY_TYPES, QUERY_TYPE_EXPLICIT, ADP_ERROR, FILTER_OPERATORS,
26
+ validateDiscoveryRequest, normalizeLimit, successResponse, failureResponse,
27
+ } from './schema.js';
28
+ import { evaluateFilter } from './filter.js';
29
+
30
+ const LOCAL_SERVER_AIC = 'AIC-DS-A'; // 参考实现 _build_response 默认 forwardChain 首跳占位(服务自身 AIC)
31
+
32
+ // ── 内部条目模型 ──
33
+ // entry = { aic, name, description, version, active, skills: AgentSkill[], acs: <原始描述>, acsView: <ACS 形状归一视图> }
34
+ // AgentSkill = { id, name, description, version, tags }
35
+ // acsView:filter 条件作用于 ACS 形状字段(aic/active/name/description/version/skills.*),
36
+ // 与原始描述(tool 6 属性 / agent 14+8)解耦——filter 引擎不感知描述器内部字段形态。
37
+ function toAgentSkill(skill) {
38
+ return {
39
+ id: skill.skillId ?? skill.id ?? '',
40
+ name: skill.name ?? '',
41
+ description: skill.description ?? '',
42
+ version: skill.version ?? '',
43
+ tags: Array.isArray(skill.tags) ? skill.tags : [],
44
+ };
45
+ }
46
+
47
+ function buildAcsView(entry) {
48
+ return {
49
+ aic: entry.aic,
50
+ active: entry.active,
51
+ name: entry.name,
52
+ description: entry.description,
53
+ version: entry.version,
54
+ skills: entry.skills,
55
+ };
56
+ }
57
+
58
+ // 节点 active 判定:config.nodes[<aic|name>].active === false → 隐藏;描述自带 active=false → 隐藏;缺省 active=true
59
+ function isNodeActive(entry, nodes) {
60
+ const override = nodes?.[entry.aic] ?? nodes?.[entry.name];
61
+ if (override && typeof override === 'object' && override.active === false) return false;
62
+ if (entry.acs && typeof entry.acs === 'object' && entry.acs.active === false) return false;
63
+ return true;
64
+ }
65
+
66
+ export function createDiscoveryService({ catalog, agentDescriptors = [], config = {} }) {
67
+ // config = capabilities.discovery 解析结果({ enabled, nodes })
68
+ const nodes = (config && typeof config.nodes === 'object' && config.nodes) || {};
69
+
70
+ // ── 目录装配:工具条目(tool-descriptor catalog)+ 智能体条目(agent-descriptor 目录)──
71
+ const entries = [];
72
+
73
+ // 工具:每个工具 = 一条目,技能 = 工具自身(P7 载体:skill 即 tool)
74
+ const toolList = (catalog && typeof catalog.list === 'function') ? catalog.list() : [];
75
+ for (const d of toolList) {
76
+ const entry = {
77
+ aic: d.toolId ?? d.name ?? '',
78
+ name: d.name ?? '',
79
+ description: d.description ?? '',
80
+ version: d.version ?? '',
81
+ skills: [{ id: d.toolId ?? d.name ?? '', name: d.name ?? '', description: d.description ?? '', version: d.version ?? '', tags: [] }],
82
+ acs: d,
83
+ kind: 'tool',
84
+ };
85
+ entry.active = isNodeActive(entry, nodes);
86
+ entry.acsView = buildAcsView(entry);
87
+ entries.push(entry);
88
+ }
89
+
90
+ // 智能体:agent-descriptor 目录(每 role 一份描述)
91
+ for (const d of agentDescriptors) {
92
+ const skills = Array.isArray(d.skills) ? d.skills.map(toAgentSkill) : [];
93
+ const entry = {
94
+ aic: d.agentId ?? d.aic ?? '',
95
+ name: d.name ?? '',
96
+ description: d.description ?? '',
97
+ version: d.version ?? '',
98
+ skills,
99
+ acs: d,
100
+ kind: 'agent',
101
+ };
102
+ entry.active = isNodeActive(entry, nodes);
103
+ entry.acsView = buildAcsView(entry);
104
+ entries.push(entry);
105
+ }
106
+
107
+ // ── 显式查询打分(能力/名称/身份码关键词匹配;本地确定性匹配,无 LLM——诚实披露)──
108
+ // 参考实现 exploratory 依赖 LLM 拆解子任务;本插件为单机治理插件,无 LLM 依赖,
109
+ // exploratory 退化为「query 关键词匹配 + filter 过滤」(本地模式),结果 memo 标注 local。
110
+ function scoreEntry(entry, query) {
111
+ const q = String(query ?? '').trim().toLowerCase();
112
+ if (!q) return 0;
113
+ const corpus = [
114
+ entry.name, entry.description, entry.aic,
115
+ ...entry.skills.flatMap((s) => [s.id, s.name, s.description, ...s.tags]),
116
+ ].filter(Boolean).join(' ').toLowerCase();
117
+ let score = 0;
118
+ for (const token of q.split(/\s+/)) {
119
+ if (!token) continue;
120
+ if (corpus.includes(token)) score += 1;
121
+ if ((entry.name ?? '').toLowerCase() === token) score += 2; // 名称精确命中加权
122
+ if ((entry.aic ?? '').toLowerCase() === token) score += 2; // 身份码精确命中加权
123
+ }
124
+ return score;
125
+ }
126
+
127
+ // 结果行 → DiscoveryAgentSkill:取最佳命中技能(显式查询)或首个技能
128
+ function toAgentSkillRow(entry, query) {
129
+ let skillId = '';
130
+ if (entry.skills.length > 0) {
131
+ if (query) {
132
+ const q = String(query).trim().toLowerCase();
133
+ const scored = entry.skills
134
+ .map((s) => ({ s, score: [s.id, s.name, s.description, ...s.tags].filter(Boolean).join(' ').toLowerCase().includes(q) ? 1 : 0 }))
135
+ .sort((a, b) => b.score - a.score);
136
+ skillId = scored[0].s.id;
137
+ } else {
138
+ skillId = entry.skills[0].id;
139
+ }
140
+ }
141
+ return { aic: entry.aic, skillId, ranking: 0, memo: undefined };
142
+ }
143
+
144
+ // 过滤 + active + limit → 结果条目(含评分)
145
+ function applyFilterAndActive(request, scored) {
146
+ const activeOnly = scored.filter((e) => e.entry.active === true);
147
+ const filtered = request.filter
148
+ ? activeOnly.filter((e) => evaluateFilter(e.entry.acsView, request.filter))
149
+ : activeOnly;
150
+ return filtered;
151
+ }
152
+
153
+ function buildResponse(request, rows, durationMs) {
154
+ // rows: [{ entry, score? }] 已按 ranking 排序
155
+ const acsMap = {};
156
+ const agentSkills = rows.map(({ entry, score }, idx) => {
157
+ const row = toAgentSkillRow(entry, request.type === QUERY_TYPE_EXPLICIT ? request.query : null);
158
+ row.ranking = idx + 1;
159
+ row.memo = request.type === 'trending' ? 'trending'
160
+ : request.type === 'filtered' ? 'Filtered query result'
161
+ : request.type === 'exploratory' ? 'exploratory-local (no LLM decomposition)'
162
+ : (score !== undefined && score > 0) ? 'keyword score: ' + score : undefined;
163
+ if (row.memo === undefined) delete row.memo;
164
+ acsMap[entry.aic] = entry.acs;
165
+ return row;
166
+ });
167
+
168
+ const group = request.type === 'filtered' ? 'filtered'
169
+ : request.type === 'trending' ? 'trending'
170
+ : request.type === 'exploratory' ? (request.query || 'exploratory')
171
+ : (request.query || '');
172
+ const agentGroups = [{ group, agentSkills }];
173
+ const route = {
174
+ forwardChain: (Array.isArray(request.forwardChain) && request.forwardChain.length > 0)
175
+ ? request.forwardChain
176
+ : [LOCAL_SERVER_AIC],
177
+ agentGroups,
178
+ status: 'ok',
179
+ durationMs: Math.round(durationMs),
180
+ };
181
+ return successResponse({ acsMap, agents: agentGroups, routes: [route] });
182
+ }
183
+
184
+ // ── discover:统一 POST /discover 语义 ──
185
+ function discover(raw = {}) {
186
+ const started = Date.now();
187
+ const err = validateDiscoveryRequest(raw);
188
+ if (err) return err; // { error: { code, message, data } }
189
+
190
+ const request = { ...raw, type: raw.type ?? QUERY_TYPE_EXPLICIT };
191
+ const limit = normalizeLimit(request.limit);
192
+
193
+ if (request.type === 'filtered') {
194
+ const rows = applyFilterAndActive(request, entries.map((entry) => ({ entry })));
195
+ return buildResponse(request, rows.slice(0, limit), Date.now() - started);
196
+ }
197
+
198
+ if (request.type === 'trending') {
199
+ const rows = applyFilterAndActive(request, entries.map((entry) => ({ entry })));
200
+ // 参考实现 discover_agents_trending:随机打散后取前 limit
201
+ const shuffled = [...rows].sort(() => Math.random() - 0.5);
202
+ return buildResponse(request, shuffled.slice(0, limit), Date.now() - started);
203
+ }
204
+
205
+ // explicit / exploratory:关键词打分 + 过滤
206
+ const scored = entries.map((entry) => ({ entry, score: scoreEntry(entry, request.query) }));
207
+ const rows = applyFilterAndActive(request, scored);
208
+ const sorted = request.type === QUERY_TYPE_EXPLICIT
209
+ ? [...rows].sort((a, b) => b.score - a.score)
210
+ : [...rows].sort((a, b) => String(a.entry.name).localeCompare(String(b.entry.name)));
211
+ // 显式查询:score=0 表示无命中 → 仅返回有命中条目(与参考实现检索语义一致)
212
+ const matched = request.type === QUERY_TYPE_EXPLICIT ? sorted.filter((r) => r.score > 0) : sorted;
213
+ return buildResponse(request, matched.slice(0, limit), Date.now() - started);
214
+ }
215
+
216
+ // ── .well-known 预置:声明发现服务地址/协议版本/能力概要 ──
217
+ function wellKnown() {
218
+ return {
219
+ protocol: 'ACPs',
220
+ protocolVersion: '02.01', // ACPs v2.1.0(参考实现 ACS protocolVersion 示例)
221
+ service: 'dsh-punky-swarm',
222
+ discovery: {
223
+ endpoint: '/api/dsh-punky-swarm/discover',
224
+ transport: 'HTTP_JSON',
225
+ security: ['mutualTLS'],
226
+ },
227
+ capabilities: {
228
+ queryTypes: QUERY_TYPES,
229
+ defaultLimit: 5,
230
+ maxLimit: 50,
231
+ filterOperators: FILTER_OPERATORS,
232
+ },
233
+ updatedAt: new Date().toISOString(),
234
+ };
235
+ }
236
+
237
+ return {
238
+ discover,
239
+ wellKnown,
240
+ // 只读快照(测试/诊断用)
241
+ stats: () => ({
242
+ enabled: config.enabled !== false,
243
+ entries: entries.length,
244
+ tools: entries.filter((e) => e.kind === 'tool').length,
245
+ agents: entries.filter((e) => e.kind === 'agent').length,
246
+ active: entries.filter((e) => e.active === true).length,
247
+ }),
248
+ };
249
+ }
250
+
251
+ export { ADP_ERROR };
@@ -0,0 +1,252 @@
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
+ // lib/hot/config-watch.js —— R1 热更新运行时(能力开关实时生效,叠加非替换)
19
+ // 设计依据:exec/panel-design.md §3.1(触发源/传播/生效语义)+ §3.1.5(判定语义双套保留、值传播)
20
+ // 链路:<root>/config/runtime.json(JSON)──fs.watch + 防抖 300ms──▶ 原子读重试 ──▶ 覆盖键校验 ──▶
21
+ // deepMerge(导出复用 assembly/schema.js,禁止复制)──▶ 新快照 ──▶ onChange({key,value,config}) 广播
22
+ // 语义:
23
+ // - 叠加非替换:只影响被覆盖键的后续读取;不写任何静态文件、不改变 cordis.patch.yml 读取结果(D2)
24
+ // - 缺省 {} → 快照 = 静态 config 原样(零行为变化,启动不广播)
25
+ // - 覆盖键校验:仅既有 schema 路径(注册表能力根 + 插件消费配置段),拒绝未知顶层键/未知 capabilities 子键
26
+ // - 快照 diff:无变化键不广播(防 fs.watch 重复事件抖动)
27
+ // - 坏 JSON / 读取失败:保持旧快照零行为变化(不广播),warn 留痕
28
+ // - 零新依赖:node:fs watch + JSON.parse(D1)
29
+ // 实施回注(本环境实测,2026-08-29):设计 §3.1.3「对文件所在目录 watch」在本部署(Windows/Node v24)不可用——
30
+ // 目录级 fs.watch 在目录内任意文件写入/重命名时触发 libuv 断言崩溃(src\win\fs-event.c:72,原生 abort 不可捕获,
31
+ // 探针复现:direct write 与 tmp+rename 两种写入模式均崩)。改为「文件级 fs.watch(runtime.json 直 watch)+
32
+ // 存在性轮询 bootstrap(文件缺失时低频探测,出现即建 watch + 触发一次重读)」。文件级 watch 在本环境实测稳定
33
+ // (direct write→change 事件、tmp+rename→rename 事件均正常,探针通过)。此回注写入 exec/panel-a-fix.md。
34
+ // 生命周期:start()(幂等)/ stop()(幂等)/ dispose();watcher/timer 均 unref(不阻塞进程退出)
35
+ // 宿主事件广播由装配侧(index.js)承担:ctx.emit('dsh-punky-swarm/config.changed', payload);
36
+ // 本模块只负责文件 watch → 快照 → onChange 回调(可单测,无宿主依赖)
37
+ import { watch, readFileSync, existsSync, mkdirSync } from 'node:fs';
38
+ import { join } from 'node:path';
39
+ import { deepMerge, CAPABILITY_REGISTRY } from '../assembly/schema.js';
40
+
41
+ // cordis 总线事件名(config.changed 广播契约;装配侧 ctx.emit 用)
42
+ export const CONFIG_CHANGED_EVENT = 'dsh-punky-swarm/config.changed';
43
+
44
+ const DEFAULT_DEBOUNCE_MS = 300; // 防抖窗口(设计 §3.1.3)
45
+ const PARSE_RETRY_MS = 50; // 原子读重试间隔(写半文件/并发写窗口)
46
+ const PARSE_RETRY_MAX = 4; // 重试上限(仍失败 → 保持旧快照)
47
+ const DEFAULT_POLL_MS = 1000; // 存在性轮询间隔(文件缺失 bootstrap;文件级 watch 需文件存在)
48
+
49
+ // 允许的 runtime.json 顶层键:注册表能力根(aip/acps/capabilities)+ 插件消费的非能力配置段
50
+ // (mailbox/resume/ratchet/escalation)。热更新只做值传播、只覆盖既有 schema 路径——
51
+ // 拒绝未知顶层键防拼写漂移产生幽灵配置(设计 §3.1.3「拒绝未知顶层键」;契约验收④)
52
+ export const ALLOWED_TOP_KEYS = new Set([
53
+ 'aip', 'acps', 'capabilities',
54
+ 'mailbox', 'resume', 'ratchet', 'escalation',
55
+ ]);
56
+
57
+ // capabilities 子键白名单(注册表 path[0]==='capabilities' 的既有键)——
58
+ // 拒绝 capabilities.<未知> 幽灵配置(discovery/verify/watch/worktree/budget/trajectory/logs/topic)
59
+ export const ALLOWED_CAPS_KEYS = new Set(
60
+ CAPABILITY_REGISTRY.filter((e) => e.path[0] === 'capabilities').map((e) => e.path[1]),
61
+ );
62
+
63
+ // 覆盖层校验(纯函数,单测面):返回 { ok, errors }
64
+ export function validateOverlay(overlay) {
65
+ const errors = [];
66
+ if (overlay === null || typeof overlay !== 'object' || Array.isArray(overlay)) {
67
+ return { ok: false, errors: ['runtime overlay must be a JSON object'] };
68
+ }
69
+ for (const [k, v] of Object.entries(overlay)) {
70
+ if (!ALLOWED_TOP_KEYS.has(k)) {
71
+ errors.push('unknown top-level key: ' + k + ' (allowed: ' + [...ALLOWED_TOP_KEYS].join(', ') + ')');
72
+ continue;
73
+ }
74
+ if (k === 'capabilities' && v !== null && typeof v === 'object' && !Array.isArray(v)) {
75
+ for (const ck of Object.keys(v)) {
76
+ if (!ALLOWED_CAPS_KEYS.has(ck)) {
77
+ errors.push('unknown capabilities key: ' + ck + ' (allowed: ' + [...ALLOWED_CAPS_KEYS].join(', ') + ')');
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return { ok: errors.length === 0, errors };
83
+ }
84
+
85
+ function sleep(ms) {
86
+ return new Promise((r) => setTimeout(r, ms));
87
+ }
88
+
89
+ // 原子读:JSON.parse 失败短等待重试(原子写 = tmp+rename,读者只会看到旧或新完整文件;
90
+ // 首次创建窗口仍可能读到半文件——重试兜底);重试耗尽抛错(保持旧快照由调用方处置)
91
+ async function readOverlayFile(file) {
92
+ for (let i = 0; i < PARSE_RETRY_MAX; i++) {
93
+ try {
94
+ if (!existsSync(file)) return {};
95
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
96
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
97
+ throw new Error('runtime.json must be a JSON object');
98
+ }
99
+ return parsed;
100
+ } catch (e) {
101
+ if (i === PARSE_RETRY_MAX - 1) throw e;
102
+ await sleep(PARSE_RETRY_MS * (i + 1));
103
+ }
104
+ }
105
+ return {};
106
+ }
107
+
108
+ export function createConfigWatcher({ root, config, onChange, logger, debounceMs = DEFAULT_DEBOUNCE_MS, useWatcher = true, pollMs = DEFAULT_POLL_MS } = {}) {
109
+ const configDir = join(root, 'config');
110
+ const runtimeFile = join(configDir, 'runtime.json');
111
+ const log = logger ?? null;
112
+ let snapshot = config; // 缺省 = 静态 config 原样(零行为变化)
113
+ let overlay = {}; // 当前生效覆盖层
114
+ let fileWatcher = null; // 文件级 watch(本环境目录级 watch 触发 libuv 断言崩溃,实施回注)
115
+ let existenceTimer = null; // 文件缺失 bootstrap 轮询
116
+ let debounceTimer = null;
117
+ let started = false;
118
+ let disposed = false;
119
+
120
+ function mergeAndNotify(nextOverlay) {
121
+ const next = deepMerge(config, nextOverlay);
122
+ // 快照 diff:只广播实际变化的顶层键(无变化键不广播——防 fs.watch 重复事件/无意义变更抖动)
123
+ const changed = [];
124
+ const keys = new Set([...Object.keys(snapshot), ...Object.keys(next)]);
125
+ for (const k of keys) {
126
+ if (JSON.stringify(snapshot[k]) !== JSON.stringify(next[k])) changed.push(k);
127
+ }
128
+ if (changed.length === 0) return { changed: [] };
129
+ snapshot = next;
130
+ for (const k of changed) {
131
+ try {
132
+ onChange?.({ key: k, value: next[k], config: next });
133
+ } catch (e) {
134
+ log?.warn?.('[dsh-punky-swarm] hot config onChange failed: ' + String(e?.message ?? e));
135
+ }
136
+ }
137
+ log?.info?.('[dsh-punky-swarm] runtime config applied: ' + changed.join(', ') + '(热更新叠加,静态配置零改动)');
138
+ return { changed };
139
+ }
140
+
141
+ // 重读 runtime.json → 校验 → 合并 → 广播(坏 JSON/未知键 → 保持旧快照零行为变化)
142
+ async function reload() {
143
+ if (disposed) return { ok: false };
144
+ let nextOverlay;
145
+ try {
146
+ nextOverlay = await readOverlayFile(runtimeFile);
147
+ } catch (e) {
148
+ log?.warn?.('[dsh-punky-swarm] runtime.json read failed (keep previous snapshot): ' + String(e?.message ?? e));
149
+ return { ok: false, reason: 'read-failed' };
150
+ }
151
+ const v = validateOverlay(nextOverlay);
152
+ if (!v.ok) {
153
+ log?.warn?.('[dsh-punky-swarm] runtime.json overlay rejected (keep previous snapshot): ' + v.errors.join('; '));
154
+ return { ok: false, reason: 'invalid-overlay', errors: v.errors };
155
+ }
156
+ overlay = nextOverlay;
157
+ return { ok: true, ...mergeAndNotify(nextOverlay) };
158
+ }
159
+
160
+ async function handleFsChange() {
161
+ if (disposed || !started) return;
162
+ clearTimeout(debounceTimer);
163
+ debounceTimer = setTimeout(() => { reload().catch(() => {}); }, debounceMs);
164
+ }
165
+
166
+ // 文件级 watch 建立/重建(幂等):每次事件后重建以跟随原子替换(tmp+rename 换 inode);
167
+ // 文件缺失 → 存在性轮询 bootstrap(低频,unref),出现后建 watch + 触发一次重读
168
+ function ensureWatching() {
169
+ if (disposed || !started) return;
170
+ if (fileWatcher) {
171
+ try { fileWatcher.close(); } catch {}
172
+ fileWatcher = null;
173
+ }
174
+ if (!existsSync(runtimeFile)) {
175
+ if (!existenceTimer) {
176
+ existenceTimer = setInterval(() => {
177
+ if (disposed || !started) { clearInterval(existenceTimer); existenceTimer = null; return; }
178
+ if (existsSync(runtimeFile)) {
179
+ clearInterval(existenceTimer); existenceTimer = null;
180
+ ensureWatching();
181
+ handleFsChange().catch(() => {}); // 文件出现 → 重读一次(覆盖建 watch 前的首次写入)
182
+ }
183
+ }, pollMs);
184
+ if (typeof existenceTimer.unref === 'function') existenceTimer.unref();
185
+ }
186
+ return;
187
+ }
188
+ try {
189
+ fileWatcher = watch(runtimeFile, () => {
190
+ ensureWatching(); // 跟随原子替换重建(幂等)
191
+ handleFsChange().catch(() => {});
192
+ });
193
+ if (typeof fileWatcher.unref === 'function') fileWatcher.unref();
194
+ fileWatcher.on?.('error', (e) => {
195
+ log?.warn?.('[dsh-punky-swarm] runtime.json watch error (config hot reload degraded): ' + String(e?.message ?? e));
196
+ ensureWatching();
197
+ });
198
+ } catch (e) {
199
+ log?.warn?.('[dsh-punky-swarm] runtime.json watch failed (config hot reload disabled, restart to apply): ' + String(e?.message ?? e));
200
+ fileWatcher = null;
201
+ }
202
+ }
203
+
204
+ function start() {
205
+ if (started) return { started: true, snapshot };
206
+ started = true;
207
+ mkdirSync(configDir, { recursive: true });
208
+ // 初始 overlay 同步读取(启动不广播——启动时静态 config 即现状;缺省 {} → 快照 = 静态 config 原样)
209
+ if (existsSync(runtimeFile)) {
210
+ try {
211
+ const init = JSON.parse(readFileSync(runtimeFile, 'utf8'));
212
+ if (init !== null && typeof init === 'object' && !Array.isArray(init)) {
213
+ const v = validateOverlay(init);
214
+ if (v.ok) {
215
+ overlay = init;
216
+ snapshot = deepMerge(config, init);
217
+ } else {
218
+ log?.warn?.('[dsh-punky-swarm] runtime.json initial overlay rejected (use static config): ' + v.errors.join('; '));
219
+ }
220
+ } else {
221
+ log?.warn?.('[dsh-punky-swarm] runtime.json initial overlay must be a JSON object (use static config)');
222
+ }
223
+ } catch (e) {
224
+ log?.warn?.('[dsh-punky-swarm] runtime.json initial read failed (use static config): ' + String(e?.message ?? e));
225
+ }
226
+ }
227
+ // 文件级 watch(实施回注:本环境目录级 watch 触发 libuv 断言崩溃,改直 watch 文件)+ 存在性轮询 bootstrap
228
+ if (useWatcher && typeof watch === 'function') {
229
+ ensureWatching();
230
+ }
231
+ return { started: true, snapshot };
232
+ }
233
+
234
+ function stop() {
235
+ started = false;
236
+ if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = null; }
237
+ if (fileWatcher) { try { fileWatcher.close(); } catch {} fileWatcher = null; }
238
+ if (existenceTimer) { clearInterval(existenceTimer); existenceTimer = null; }
239
+ return { stopped: true };
240
+ }
241
+
242
+ function dispose() {
243
+ disposed = true;
244
+ stop();
245
+ }
246
+
247
+ function readSnapshot() {
248
+ return snapshot;
249
+ }
250
+
251
+ return { start, stop, dispose, reload, readSnapshot, validateOverlay, runtimeFile, eventName: CONFIG_CHANGED_EVENT };
252
+ }