@stofone/dsh-agent-roster 0.0.0-stage → 0.5.0

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.
@@ -0,0 +1,9 @@
1
+ # @stofone/dsh-agent-roster 的 Host 行。
2
+ #
3
+ # config 会被整体替换(不会深合并):覆盖本行时请把需要的字段全部重述一遍。
4
+ # 所有字段都有默认值,留空即可用「项目内 .dsh/agent-rosters/」与「用户级
5
+ # <dshHome>/agent-rosters/」两个目录。
6
+ - insert:
7
+ - id: agent-roster
8
+ name: '@stofone/dsh-agent-roster'
9
+ config: {}
@@ -0,0 +1,82 @@
1
+ # 示例档案:一个三人的代码审查小组。
2
+ #
3
+ # 目录约定:档案库根目录下,每份档案一个目录,目录名必须等于 roster 字段。
4
+ # 本文件位于 <根>/core-review/roster.yaml,所以 roster 必须是 core-review。
5
+ kind: dsh-agent-roster/v1
6
+ roster: core-review
7
+ version: 1.0.0
8
+ title: 代码审查小组
9
+ description: 一名队长加两名审查者:一人盯正确性,一人盯可维护性与边界条件。
10
+ license: MIT
11
+ maintainers:
12
+ - STofone
13
+ tags:
14
+ - review
15
+ - example
16
+
17
+ # defaults 是成员的省缺值:成员自己没写的字段才继承;写了就覆盖。
18
+ # provider/model 是"运行时真值",会原样交给 dsh-agent-teams 建员;
19
+ # toolPolicy 需要部署方声明过对应 id,否则只是声明、运行时不会生效(验证器会提醒)。
20
+ defaults:
21
+ provider: deepseek
22
+ model: deepseek-v4.1-flash
23
+ reasoningEffort: high
24
+ toolPolicy: read-only
25
+
26
+ members:
27
+ - name: lead
28
+ role: 队长
29
+ title: 编排与取舍
30
+ description: 编排这次审查、拆任务、合并结论;不写具体代码
31
+ aliases:
32
+ - coordinator
33
+ skills:
34
+ - frontend-skill
35
+ examples:
36
+ - 这次改动影响面有多大,分给谁看
37
+ - 两人结论冲突,你判断谁对
38
+ systemPrompt: |
39
+ 你负责这次审查的编排:先划定改动影响面,再把任务分给两名审查者,最后合并结论。
40
+ 只在两种情况下自己下结论:两人结论冲突且证据不足,或风险高到必须先止损。
41
+ 报告要能直接贴进 PR:结论在前,证据在后,不确定的地方明确写"未验证"。
42
+ tags:
43
+ - orchestration
44
+ notes: 队长的工具边界通常比审查者宽(要能派活),这里刻意收窄成只读,作为示例。
45
+
46
+ - name: correctness
47
+ role: 正确性审查
48
+ title: 找会出错的地方
49
+ description: 只找"什么输入下会做错事",每条结论给可复现的触发条件
50
+ systemPrompt: |
51
+ 你只关心"这段代码在什么输入下会做错事":边界值、空值、并发、错误路径、类型退化。
52
+ 每条结论必须给出可复现的触发条件;给不出就标注为"怀疑,未验证"。
53
+ 不要评论风格,那是另一个人的事。
54
+ tags:
55
+ - correctness
56
+ toolScope:
57
+ allow:
58
+ - read
59
+ - grep
60
+ - glob
61
+
62
+ - name: maintainer
63
+ role: 可维护性审查
64
+ title: 看六个月后谁来改
65
+ description: 看命名、重复、耦合、测试与错误信息;只给最小修改方向
66
+ systemPrompt: |
67
+ 你关心的是"半年后接手的人能不能安全改动":命名、重复、隐含耦合、缺测试、错误信息质量。
68
+ 对每处问题给出最小修改方向,不要重写整个模块。
69
+ 风格问题只在它影响可读性判断时提,且最多三条。
70
+ tags:
71
+ - maintainability
72
+
73
+ # 队长段:队长=创建团队的那个会话,不是成员。这里只记录它应该是什么样,
74
+ # 真要生效得改 profile 的 agent-default-model 与 agent preset 的 persona。
75
+ captain:
76
+ name: lead
77
+ description: 主会话即队长,负责拆活与验收
78
+ provider: deepseek
79
+ model: deepseek-v4.1-flash
80
+ reasoningEffort: high
81
+ personaPrefix: |
82
+ 派活前先复述目标与验收标准;汇报时先结论后证据。
@@ -0,0 +1,42 @@
1
+ # 示例档案:外部调研小组。
2
+ #
3
+ # 与 core-review 刻意不同:这份档案让成员分别用不同模型,并给"复核者"更弱的工具边界。
4
+ kind: dsh-agent-roster/v1
5
+ roster: research
6
+ version: 0.1.0
7
+ title: 外部调研小组
8
+ description: 一手资料收集 + 交叉复核;复核者的结论必须能被另一条独立来源支撑。
9
+ tags:
10
+ - research
11
+ - example
12
+
13
+ defaults:
14
+ provider: deepseek
15
+ model: deepseek-v4.1-pro
16
+ reasoningEffort: medium
17
+
18
+ members:
19
+ - name: scout
20
+ role: 一手资料
21
+ title: 找原始出处
22
+ systemPrompt: |
23
+ 你负责找一手资料:官方文档、源码、发布说明、原始数据。二手转述只能用来说明"存在争议"。
24
+ 每条事实必须附可点击链接与日期;找不到就写"没找到",不要用常识补。
25
+ tags:
26
+ - sourcing
27
+
28
+ - name: verifier
29
+ role: 交叉复核
30
+ title: 独立复现结论
31
+ systemPrompt: |
32
+ 你只做一件事:拿另一条独立来源验证别人的结论。你有意不共享 scout 的上下文,避免同源偏差。
33
+ 结论分三档:已复现 / 部分一致 / 无法复现。无法复现时必须写清缺哪条关键证据。
34
+ model: deepseek-v4.1-flash
35
+ reasoningEffort: high
36
+ tags:
37
+ - verification
38
+ toolScope:
39
+ allow:
40
+ - read
41
+ - grep
42
+ notes: 刻意用更小的模型做复核,成本低且能暴露"结论依赖模型能力"的问题。
package/icon.svg ADDED
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Agent Roster">
2
+ <rect x="2" y="2" width="60" height="60" rx="14" fill="#1d4ed8"/>
3
+ <g fill="#ffffff">
4
+ <rect x="14" y="16" width="24" height="5" rx="2.5"/>
5
+ <rect x="14" y="27" width="32" height="5" rx="2.5" opacity="0.9"/>
6
+ <rect x="14" y="38" width="20" height="5" rx="2.5" opacity="0.75"/>
7
+ </g>
8
+ <g fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">
9
+ <path d="M40 47.5l4.6 4.5L54 42.5"/>
10
+ </g>
11
+ </svg>
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "../schema/registry.v1.schema.json",
3
+ "kind": "dsh-agent-roster-registry/v1",
4
+ "version": 1,
5
+ "generatedAt": "2026-10-30T00:00:00.000Z",
6
+ "generator": "STofone/dsh-agent-roster (hand-maintained; verified by .github/workflows/registry.yml)",
7
+ "source": "https://github.com/STofone/dsh-agent-roster",
8
+ "collections": [
9
+ {
10
+ "id": "agent-roster-examples",
11
+ "title": "官方起步档案集",
12
+ "description": "评审与调研两套起步档案,用来验证格式、桥接与核对链路;也可以直接复制成自己的档案。",
13
+ "repo": "github:STofone/dsh-agent-roster",
14
+ "ref": "main",
15
+ "path": "examples",
16
+ "rosters": ["core-review", "research"],
17
+ "tags": ["starter", "review", "research"],
18
+ "license": "MIT",
19
+ "maintainers": ["STofone"],
20
+ "homepage": "https://github.com/STofone/dsh-agent-roster#examples"
21
+ }
22
+ ]
23
+ }
package/index.js ADDED
@@ -0,0 +1,298 @@
1
+ /**
2
+ * @stofone/dsh-agent-roster —— 多 Agent 团队的静态成员档案层。
3
+ *
4
+ * 定位:**定义与校验 + 桥接参数生成 + 可管理它的面板**。本插件不运行团队、不调度任务、
5
+ * 不替代任何团队运行时,也不写别人的状态。它做的事是:
6
+ *
7
+ * 1. 把成员档案(谁、能做什么、用什么模型、受什么工具约束)固化成可校验、可版本化的静态文件;
8
+ * 2. 校验失败时 fail-loud:带文件路径与字段路径报错,不静默降级;
9
+ * 3. 在 dsh-agent-teams 创建成员时,把档案翻译成建员参数,并如实标注每个字段的归宿
10
+ * (运行时接收 / 仅声明 / 需落部署配置),事后再拿 team.json 的实际值核对;
11
+ * 4. 给用户一个能看到、能编辑、能新建/导入/删除档案,并能查看/核对团队的面板。
12
+ *
13
+ * @module dsh-agent-roster
14
+ */
15
+ import { readFileSync } from "node:fs";
16
+
17
+ import z from "@deepseek-ai/schemastery";
18
+
19
+ import { configView } from "./lib/config-view.js";
20
+ import { collectLibrary } from "./lib/library.js";
21
+ import { parseDocumentText } from "./lib/parse.js";
22
+ import { validateRoster } from "./lib/validate.js";
23
+ import { sendError, sendJson, createAuthGate, readJsonBody, requireMethod } from "./lib/http.js";
24
+ import { buildPanelSnapshot, installCollection, loadRemotePanel, resolveRoots } from "./lib/service.js";
25
+ import { fetchCollection } from "./lib/remote.js";
26
+ import { registerRosterTools } from "./lib/tools.js";
27
+ import { deleteRosterDocument, readRosterDocument, writeRosterDocument } from "./lib/write.js";
28
+
29
+ const pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"));
30
+
31
+ /** Cordis 插件名(出现在宿主日志与设置页)。 */
32
+ export const name = "agent-roster";
33
+
34
+ /** 只依赖工具注册表;档案库与团队状态都是普通文件,不依赖别的插件。 */
35
+ export const inject = ["tools"];
36
+
37
+ /**
38
+ * 插件配置。
39
+ *
40
+ * 全部声明为 volatile:在设置页改完立即生效,不需要重启宿主——
41
+ * 这些值只是"去哪读文件、界面说什么语言",没有需要重建的生命周期资源。
42
+ */
43
+ export const Config = z.object({
44
+ rosterDirs: z
45
+ .array(z.string())
46
+ .default([])
47
+ .description(
48
+ "档案库目录。留空 = 全局默认目录 $DSH_HOME/agent-roster(默认 ~/.dsh/agent-roster," +
49
+ "docker 下就是被挂载的数据卷),与工作区无关。填绝对路径或 ~ 开头路径同样是全局的;" +
50
+ "只有填相对路径(如 examples)才按工作区解析。第一项是四个工具默认读取的档案库。",
51
+ )
52
+ .volatile(),
53
+ knownToolPolicies: z
54
+ .array(z.string())
55
+ .default([])
56
+ .description("部署已声明的 toolPolicy id 列表:填了就能在校验期发现档案里写错的策略 id。留空表示不做交叉校验。")
57
+ .volatile(),
58
+ agentTeamsStateDir: z
59
+ .string()
60
+ .default(".agent-teams")
61
+ .description("dsh-agent-teams 的状态目录(只读,用于核对成员实际字段)。相对路径以工作区为基准。")
62
+ .volatile(),
63
+ language: z
64
+ .union([z.const("auto"), z.const("zh"), z.const("en")])
65
+ .default("auto")
66
+ .description("面板与设置项的语言:auto 跟随 DSH 当前语言,zh/en 固定中文或英文。")
67
+ .volatile(),
68
+ indexes: z
69
+ .array(z.string())
70
+ .default([])
71
+ .description(
72
+ "公共档案源(registry)列表,可填 https URL 或 npm:pkg。留空 = 只用官方源 " +
73
+ "STofone/dsh-agent-roster 的 index/registry.json。拉取按 TTL 缓存(默认一小时一次)," +
74
+ "网络失败时沿用上次缓存,不会把“没拉到”当成“没有档案”。",
75
+ )
76
+ .volatile(),
77
+ region: z
78
+ .union([z.const("auto"), z.const("global"), z.const("china")])
79
+ .default("auto")
80
+ .description("下载区:china 用腾讯 npm 镜像读 npm 形式的目录包;auto 等同 global(不自动探测,避免额外的出网请求)。")
81
+ .volatile(),
82
+ });
83
+
84
+ /**
85
+ * 挂载插件。
86
+ *
87
+ * @param {import("@deepseek-ai/cordis").Context} ctx
88
+ * @param {object} config
89
+ * @returns {void}
90
+ */
91
+ export function apply(ctx, config) {
92
+ // volatile 字段在运行时是引用对象,必须走视图按当前值读,见 lib/config-view.js。
93
+ const settings = configView(config);
94
+
95
+ registerRosterTools(ctx, settings, { version: pkg.version });
96
+ ctx.logger?.info?.(`dsh-agent-roster ${pkg.version}: registered 4 tools`);
97
+
98
+ // 面板路由:读快照 / 读单份档案原文 / 保存 / 删除 / 导入。
99
+ // 没有 webServer 也不影响工具可用性。
100
+ ctx.inject(["webServer", "connection"], (webCtx) => {
101
+ const guard = createAuthGate(webCtx);
102
+ const register = (path, handler, label) =>
103
+ webCtx.effect(() => webCtx.webServer.register({ kind: "exact", path, handler }), `dsh-agent-roster: ${label}`);
104
+
105
+ register(
106
+ "/plugins/dsh-agent-roster/library",
107
+ async (req, res) => {
108
+ if (guard(req, res)) return;
109
+ if (!requireMethod(req, res, "GET")) return;
110
+ const snapshot = buildPanelSnapshot(ctx, settings, { name: pkg.name, version: pkg.version });
111
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams;
112
+ // 远程源是“附属信息”:拉不到也不能让本地档案库的响应失败,所以失败只体现在 remote.diagnostics 里。
113
+ const remote = await loadRemotePanel(settings, { refresh: query.get("refresh") === "1" });
114
+ sendJson(res, 200, { ...snapshot, remote });
115
+ },
116
+ "library route",
117
+ );
118
+
119
+ register(
120
+ "/plugins/dsh-agent-roster/collection",
121
+ async (req, res) => {
122
+ if (guard(req, res)) return;
123
+ if (!requireMethod(req, res, "GET")) return;
124
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams;
125
+ const id = query.get("id") ?? "";
126
+ const source = query.get("source") ?? "";
127
+ const remote = await loadRemotePanel(settings, { refresh: query.get("refresh") === "1" });
128
+ const collection = remote.collections.find(
129
+ (item) => item.id === id && (source === "" || item.origin?.source === source),
130
+ );
131
+ if (collection === undefined) {
132
+ sendJson(res, 404, { ok: false, error: { code: "collection-not-found" }, known: remote.collections.map((item) => item.id) });
133
+ return;
134
+ }
135
+ const fetched = await fetchCollection(collection);
136
+ sendJson(res, 200, {
137
+ ok: fetched.diagnostics.every((item) => item.severity !== "error"),
138
+ collection: {
139
+ id: collection.id,
140
+ title: collection.title,
141
+ description: collection.description,
142
+ repo: collection.repo,
143
+ ref: collection.ref,
144
+ path: collection.path,
145
+ rosters: collection.rosters,
146
+ license: collection.license,
147
+ homepage: collection.homepage,
148
+ origin: collection.origin,
149
+ },
150
+ rosters: fetched.rosters,
151
+ diagnostics: fetched.diagnostics,
152
+ });
153
+ },
154
+ "collection route",
155
+ );
156
+
157
+ register(
158
+ "/plugins/dsh-agent-roster/locale",
159
+ async (req, res) => {
160
+ if (guard(req, res)) return;
161
+ if (!requireMethod(req, res, "GET")) return;
162
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams;
163
+ const lang = query.get("lang") === "zh" ? "zh" : "en";
164
+ try {
165
+ const text = readFileSync(new URL(`./locale/${lang}.json`, import.meta.url), "utf8");
166
+ res.writeHead(200, {
167
+ "content-type": "application/json; charset=utf-8",
168
+ "cache-control": "no-store",
169
+ });
170
+ res.end(text);
171
+ } catch {
172
+ sendError(res, 500, "locale-unavailable");
173
+ }
174
+ },
175
+ "locale route",
176
+ );
177
+
178
+ register(
179
+ "/plugins/dsh-agent-roster/document",
180
+ async (req, res) => {
181
+ if (guard(req, res)) return;
182
+ if (!requireMethod(req, res, "GET")) return;
183
+ const query = new URL(req.url ?? "/", "http://localhost").searchParams;
184
+ const result = readRosterDocument({
185
+ roots: resolveRoots(ctx, settings),
186
+ rootDir: query.get("rootDir") ?? "",
187
+ id: query.get("id") ?? "",
188
+ });
189
+ if (!result.ok) {
190
+ sendError(res, result.code === "not-found" ? 404 : 400, result.code);
191
+ return;
192
+ }
193
+ // 编辑表单需要人设原文:解析一份给编辑器用,解析失败也不挡"看源码"这条路。
194
+ const parsed = parseDocumentText(result.text, { file: result.file, format: result.format });
195
+ const validated = parsed.ok ? validateRoster(parsed.value, { file: result.file }) : null;
196
+ sendJson(res, 200, {
197
+ ok: true,
198
+ file: result.file,
199
+ format: result.format,
200
+ text: result.text,
201
+ roster: validated?.ok ? validated.roster : null,
202
+ });
203
+ },
204
+ "document route",
205
+ );
206
+
207
+ register(
208
+ "/plugins/dsh-agent-roster/install",
209
+ async (req, res) => {
210
+ if (guard(req, res)) return;
211
+ if (!requireMethod(req, res, "POST")) return;
212
+ const body = await readJsonBody(req);
213
+ if (!body.ok) {
214
+ sendError(res, body.status, body.code);
215
+ return;
216
+ }
217
+ const input = body.value !== null && typeof body.value === "object" ? body.value : {};
218
+ if (typeof input.collection !== "string" || input.collection === "") {
219
+ sendError(res, 400, "missing-collection");
220
+ return;
221
+ }
222
+ const result = await installCollection(settings, {
223
+ collectionId: input.collection,
224
+ source: typeof input.source === "string" && input.source !== "" ? input.source : undefined,
225
+ overwrite: input.overwrite === true,
226
+ });
227
+ if (result.ok !== true && result.code !== undefined) {
228
+ sendJson(res, result.code === "collection-not-found" ? 404 : 400, {
229
+ ok: false,
230
+ error: { code: result.code },
231
+ known: result.known ?? [],
232
+ diagnostics: result.diagnostics ?? [],
233
+ });
234
+ return;
235
+ }
236
+ sendJson(res, 200, result);
237
+ },
238
+ "install route",
239
+ );
240
+
241
+ register(
242
+ "/plugins/dsh-agent-roster/rosters",
243
+ async (req, res) => {
244
+ if (guard(req, res)) return;
245
+ if (!requireMethod(req, res, "POST")) return;
246
+ const body = await readJsonBody(req);
247
+ if (!body.ok) {
248
+ sendError(res, body.status, body.code);
249
+ return;
250
+ }
251
+ const input = body.value !== null && typeof body.value === "object" ? body.value : {};
252
+ const roots = resolveRoots(ctx, settings);
253
+
254
+ if (input.action === "delete") {
255
+ const result = deleteRosterDocument({ roots, rootDir: input.rootDir, id: input.id });
256
+ if (!result.ok) {
257
+ sendError(res, result.code === "not-found" ? 404 : 400, result.code);
258
+ return;
259
+ }
260
+ sendJson(res, 200, { ok: true, removed: result.removed });
261
+ return;
262
+ }
263
+
264
+ if (input.action !== "save" && input.action !== "import") {
265
+ sendError(res, 400, "unknown-action");
266
+ return;
267
+ }
268
+
269
+ const result = writeRosterDocument({
270
+ roots,
271
+ rootDir: input.rootDir,
272
+ text: input.text,
273
+ format: input.format,
274
+ // import 语义上就是"往库里放一份",默认允许覆盖同名档案;save 必须由用户确认。
275
+ overwrite: input.action === "import" ? input.overwrite !== false : input.overwrite === true,
276
+ knownToolPolicies: settings.knownToolPolicies,
277
+ });
278
+ if (!result.ok) {
279
+ const status =
280
+ result.code === "exists" || result.code === "format-conflict" ? 409 : result.code === "unknown-root" ? 403 : 422;
281
+ sendJson(res, status, {
282
+ ok: false,
283
+ error: { code: result.code },
284
+ existing: result.existing ?? null,
285
+ diagnostics: result.diagnostics ?? [],
286
+ });
287
+ return;
288
+ }
289
+ sendJson(res, 200, result);
290
+ },
291
+ "rosters route",
292
+ );
293
+ });
294
+ }
295
+
296
+ // 面板快照逻辑在 lib/library.js 与 lib/service.js:纯数据投影,可脱离宿主单测。
297
+ export { collectLibrary, markDuplicateIds, serializeRoster } from "./lib/library.js";
298
+ export { buildPanelSnapshot } from "./lib/service.js";