@stofone/dsh-agent-roster 0.7.0 → 0.8.1

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 (39) hide show
  1. package/CHANGELOG.md +52 -3
  2. package/README.md +40 -3
  3. package/README.zh.md +40 -3
  4. package/bin/cli.mjs +10 -0
  5. package/index/registry.json +49 -5
  6. package/lib/format.js +4 -5
  7. package/lib/organization.js +331 -0
  8. package/lib/registry.js +83 -1
  9. package/lib/remote.js +1 -1
  10. package/lib/render.js +15 -0
  11. package/lib/tools.js +3 -0
  12. package/orgs/agent-roster-collective/api-design/roster.yaml +67 -0
  13. package/orgs/agent-roster-collective/content-ops/roster.yaml +55 -0
  14. package/orgs/agent-roster-collective/contract-review/roster.yaml +58 -0
  15. package/orgs/agent-roster-collective/data-quality/roster.yaml +64 -0
  16. package/orgs/agent-roster-collective/db-migration/roster.yaml +64 -0
  17. package/orgs/agent-roster-collective/docs-writer/roster.yaml +59 -0
  18. package/orgs/agent-roster-collective/finance-recon/roster.yaml +63 -0
  19. package/orgs/agent-roster-collective/frontend-ui/roster.yaml +60 -0
  20. package/orgs/agent-roster-collective/i18n-l10n/roster.yaml +60 -0
  21. package/orgs/agent-roster-collective/incident-response/roster.yaml +65 -0
  22. package/orgs/agent-roster-collective/literature-review/roster.yaml +61 -0
  23. package/orgs/agent-roster-collective/market-research/roster.yaml +60 -0
  24. package/orgs/agent-roster-collective/ml-eval/roster.yaml +63 -0
  25. package/orgs/agent-roster-collective/organization.yaml +118 -0
  26. package/orgs/agent-roster-collective/performance/roster.yaml +63 -0
  27. package/orgs/agent-roster-collective/product-discovery/roster.yaml +55 -0
  28. package/orgs/agent-roster-collective/qa-regression/roster.yaml +64 -0
  29. package/orgs/agent-roster-collective/recruiting/roster.yaml +58 -0
  30. package/orgs/agent-roster-collective/release-manager/roster.yaml +59 -0
  31. package/orgs/agent-roster-collective/security-audit/capabilities.yaml +12 -0
  32. package/orgs/agent-roster-collective/security-audit/roster.yaml +65 -0
  33. package/orgs/agent-roster-collective/sre-oncall/roster.yaml +60 -0
  34. package/orgs/agent-roster-collective/support-triage/roster.yaml +58 -0
  35. package/package.json +5 -4
  36. package/schema/capabilities.v1.schema.json +1 -1
  37. package/schema/organization.v1.schema.json +50 -0
  38. package/schema/registry.v1.schema.json +83 -0
  39. package/schema/roster.v1.schema.json +1 -1
@@ -0,0 +1,331 @@
1
+ /**
2
+ * 组织档案(organization):一座档案库的"目录页"。
3
+ *
4
+ * 为什么需要它:档案库只能回答"这一份档案写了什么",回答不了"这些档案是什么关系、
5
+ * 哪份是生产、哪份是归档、谁维护"。当一座库里放二十份来自不同来源的档案时,
6
+ * 缺了这层就只能靠目录名猜。
7
+ *
8
+ * 边界(与 capabilities.yaml 同一条纪律):
9
+ * - 组织只做**声明与核对**:声明库里有哪几份档案、各自什么状态;
10
+ * 它不创建团队、不改档案、不进 `agent_teams_add_member`。
11
+ * - 它**能被机械核对**:声明了但目录不在 = error,目录在却没声明 = warn,
12
+ * 已归档却仍在用 = info。核对不了的字段一律不收(未知键即错误)。
13
+ * - 位置即身份:`organization` 必须等于所在目录名,和 `roster` 的规则一致。
14
+ *
15
+ * @module dsh-agent-roster/organization
16
+ */
17
+
18
+ /** 组织文件的 `kind`。与 roster / registry / capabilities 并列,互不冒充。 */
19
+ export const ORGANIZATION_KIND = "dsh-agent-roster-organization/v1";
20
+
21
+ /** 组织文件允许的文件名,按优先级排列。 */
22
+ export const ORGANIZATION_FILE_NAMES = ["organization.yaml", "organization.yml", "organization.json"];
23
+
24
+ /** 档案在组织里的状态。刻意只有两档:归档不等于删除,删除就是没有这个目录。 */
25
+ export const ORGANIZATION_STATUSES = ["active", "archived"];
26
+
27
+ /** 长度上限:与档案的 LIMITS 同一套口径,超了就报错而不是截断。 */
28
+ export const LIMITS = {
29
+ rosters: 64,
30
+ titleChars: 120,
31
+ descriptionChars: 500,
32
+ licenseChars: 120,
33
+ homepageChars: 200,
34
+ maintainers: 16,
35
+ maintainerChars: 120,
36
+ noteChars: 300,
37
+ };
38
+
39
+ /** 组织 id 与档案 id 同形:小写、连字符、首字符必须是字母或数字。 */
40
+ export const ID_PATTERN = /^[a-z0-9][a-z0-9-]{1,63}$/;
41
+
42
+ const ROOT_FIELDS = new Set([
43
+ "kind",
44
+ "organization",
45
+ "title",
46
+ "description",
47
+ "license",
48
+ "homepage",
49
+ "maintainers",
50
+ "rosters",
51
+ ]);
52
+
53
+ const ROSTER_FIELDS = new Set(["id", "title", "status", "origin", "note"]);
54
+
55
+ /** 顶层字段清单(供 schema 对齐测试反向钉住,避免两处各写一份)。 */
56
+ export const ORGANIZATION_FIELDS = Object.freeze([...ROOT_FIELDS]);
57
+
58
+ /** 每条登记项的字段清单。 */
59
+ export const ORGANIZATION_ROSTER_FIELDS = Object.freeze([...ROSTER_FIELDS]);
60
+
61
+ /**
62
+ * 一条诊断。形状与 `lib/validate.js` 保持一致,好让 render / panel / CLI 用同一套渲染。
63
+ *
64
+ * @param {"error"|"warn"|"info"} severity
65
+ * @param {string} code
66
+ * @param {string} file
67
+ * @param {string} reason
68
+ * @returns {{severity: string, code: string, path: string, file: string, reason: string}}
69
+ */
70
+ function diagnose(severity, code, file, reason) {
71
+ return { severity, code, path: "", file, reason };
72
+ }
73
+
74
+ /**
75
+ * 校验一份组织档案。
76
+ *
77
+ * 与 roster 的校验同一条纪律:**能核对的才收**。`homepage` 只校验是不是 http(s),
78
+ * 不联网;`maintainers` 只校验形态,不校验这个人存在不存在。
79
+ *
80
+ * @param {unknown} value 解析后的 JSON/YAML 值
81
+ * @param {{file?: string, expectedId?: string|null}} [options]
82
+ * @returns {{ok: boolean, organization: object|null, diagnostics: object[]}}
83
+ */
84
+ export function validateOrganization(value, options = {}) {
85
+ const file = options.file ?? ORGANIZATION_FILE_NAMES[0];
86
+ const expectedId = options.expectedId ?? null;
87
+ /** @type {object[]} */
88
+ const diagnostics = [];
89
+
90
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
91
+ diagnostics.push(diagnose("error", "organization-not-object", file, "组织文件必须是一个对象(键值对)。"));
92
+ return { ok: false, organization: null, diagnostics };
93
+ }
94
+
95
+ for (const key of Object.keys(value)) {
96
+ if (!ROOT_FIELDS.has(key)) {
97
+ diagnostics.push(
98
+ diagnose(
99
+ "error",
100
+ "unknown-field",
101
+ file,
102
+ `未知字段 "${key}"。组织文件只收 ${[...ROOT_FIELDS].join(" / ")}:` +
103
+ "核对不了的字段不收——写了也不会被任何人验证,只会被误当成事实。",
104
+ ),
105
+ );
106
+ }
107
+ }
108
+
109
+ if (value.kind !== ORGANIZATION_KIND) {
110
+ diagnostics.push(
111
+ diagnose("error", "organization-kind", file, `kind 必须是 "${ORGANIZATION_KIND}",实际是 ${JSON.stringify(value.kind)}。`),
112
+ );
113
+ }
114
+
115
+ const id = value.organization;
116
+ if (typeof id !== "string" || !ID_PATTERN.test(id)) {
117
+ diagnostics.push(
118
+ diagnose("error", "organization-id", file, "organization 必须是 2–64 位小写字母/数字/连字符,且以字母或数字开头。"),
119
+ );
120
+ } else if (expectedId !== null && id !== expectedId) {
121
+ diagnostics.push(
122
+ diagnose(
123
+ "error",
124
+ "organization-id-mismatch",
125
+ file,
126
+ `organization 是 "${id}",但所在目录叫 "${expectedId}"。位置即身份:组织 id 必须等于目录名。`,
127
+ ),
128
+ );
129
+ }
130
+
131
+ const title = optionalString(value, "title", LIMITS.titleChars, diagnostics, file, "organization-title");
132
+ optionalString(value, "description", LIMITS.descriptionChars, diagnostics, file, "organization-description");
133
+ optionalString(value, "license", LIMITS.licenseChars, diagnostics, file, "organization-license");
134
+
135
+ if (value.homepage !== undefined) {
136
+ const homepage = optionalString(value, "homepage", LIMITS.homepageChars, diagnostics, file, "organization-homepage");
137
+ if (typeof homepage === "string" && !/^https?:\/\/\S+$/i.test(homepage)) {
138
+ diagnostics.push(
139
+ diagnose("error", "organization-homepage", file, "homepage 只收 http(s) 地址:别的写法既打不开也核对不了。"),
140
+ );
141
+ }
142
+ }
143
+
144
+ if (value.maintainers !== undefined) {
145
+ if (!Array.isArray(value.maintainers)) {
146
+ diagnostics.push(diagnose("error", "organization-maintainers", file, "maintainers 必须是字符串数组。"));
147
+ } else if (value.maintainers.length > LIMITS.maintainers) {
148
+ diagnostics.push(
149
+ diagnose("error", "organization-maintainers", file, `maintainers 最多 ${LIMITS.maintainers} 项。`),
150
+ );
151
+ } else {
152
+ for (const who of value.maintainers) {
153
+ if (typeof who !== "string" || who.length === 0 || who.length > LIMITS.maintainerChars) {
154
+ diagnostics.push(
155
+ diagnose("error", "organization-maintainer", file, `维护者名字必须是 1–${LIMITS.maintainerChars} 个字符的字符串。`),
156
+ );
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ const rosters = [];
164
+ const rawRosters = value.rosters;
165
+ if (!Array.isArray(rawRosters) || rawRosters.length === 0) {
166
+ diagnostics.push(diagnose("error", "organization-rosters", file, "rosters 必须是非空数组:组织的意义就是登记这些档案。"));
167
+ } else if (rawRosters.length > LIMITS.rosters) {
168
+ diagnostics.push(diagnose("error", "organization-rosters", file, `rosters 最多 ${LIMITS.rosters} 项。`));
169
+ } else {
170
+ const seen = new Set();
171
+ rawRosters.forEach((item, index) => {
172
+ const where = `rosters[${index}]`;
173
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
174
+ diagnostics.push(diagnose("error", "organization-roster-not-object", file, `${where} 必须是对象。`));
175
+ return;
176
+ }
177
+ for (const key of Object.keys(item)) {
178
+ if (!ROSTER_FIELDS.has(key)) {
179
+ diagnostics.push(
180
+ diagnose("error", "unknown-field", file, `${where} 的未知字段 "${key}"(只收 ${[...ROSTER_FIELDS].join(" / ")})。`),
181
+ );
182
+ }
183
+ }
184
+ if (typeof item.id !== "string" || !ID_PATTERN.test(item.id)) {
185
+ diagnostics.push(diagnose("error", "organization-roster-id", file, `${where}.id 必须是合法档案 id。`));
186
+ return;
187
+ }
188
+ if (seen.has(item.id)) {
189
+ diagnostics.push(
190
+ diagnose("error", "organization-duplicate-roster", file, `档案 "${item.id}" 在组织里登记了两次。`),
191
+ );
192
+ return;
193
+ }
194
+ seen.add(item.id);
195
+ const status = item.status ?? "active";
196
+ if (!ORGANIZATION_STATUSES.includes(status)) {
197
+ diagnostics.push(
198
+ diagnose("error", "organization-roster-status", file, `${where}.status 只能是 ${ORGANIZATION_STATUSES.join(" / ")}。`),
199
+ );
200
+ }
201
+ const entryTitle = optionalString(item, "title", LIMITS.titleChars, diagnostics, file, "organization-roster-title", where);
202
+ const origin = optionalString(item, "origin", LIMITS.titleChars, diagnostics, file, "organization-roster-origin", where);
203
+ const note = optionalString(item, "note", LIMITS.noteChars, diagnostics, file, "organization-roster-note", where);
204
+ rosters.push({
205
+ id: item.id,
206
+ title: entryTitle ?? "",
207
+ status,
208
+ origin: origin ?? "",
209
+ note: note ?? "",
210
+ });
211
+ });
212
+ }
213
+
214
+ if (diagnostics.some((item) => item.severity === "error")) {
215
+ return { ok: false, organization: null, diagnostics };
216
+ }
217
+
218
+ return {
219
+ ok: true,
220
+ organization: {
221
+ kind: ORGANIZATION_KIND,
222
+ id,
223
+ title: title ?? "",
224
+ description: value.description ?? "",
225
+ license: value.license ?? "",
226
+ homepage: value.homepage ?? "",
227
+ maintainers: Array.isArray(value.maintainers) ? [...value.maintainers] : [],
228
+ rosters,
229
+ },
230
+ diagnostics,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * 读一个可选字符串字段并钉住上限。
236
+ *
237
+ * @param {object} source
238
+ * @param {string} key
239
+ * @param {number} max
240
+ * @param {object[]} diagnostics
241
+ * @param {string} file
242
+ * @param {string} code
243
+ * @param {string} [where]
244
+ * @returns {string|undefined}
245
+ */
246
+ function optionalString(source, key, max, diagnostics, file, code, where = "") {
247
+ const value = source[key];
248
+ if (value === undefined) return undefined;
249
+ const label = where === "" ? key : `${where}.${key}`;
250
+ if (typeof value !== "string" || value.length > max) {
251
+ diagnostics.push(diagnose("error", code, file, `${label} 必须是长度不超过 ${max} 的字符串。`));
252
+ return undefined;
253
+ }
254
+ return value;
255
+ }
256
+
257
+ /**
258
+ * 组织声明与磁盘实际情况的交叉核对。
259
+ *
260
+ * 三件事分开说,因为它们的严重度不同:
261
+ * - **缺**(声明了却找不到目录):error。组织页在撒谎,比没有组织页更糟。
262
+ * - **多**(目录在却没登记):warn。不是错误,但意味着有人绕过组织加了档案。
263
+ * - **归档仍在**:info。归档是声明,不是删除;提醒一句即可。
264
+ *
265
+ * @param {object|null} organization
266
+ * @param {string[]} rosterIds 磁盘上真正通过校验的档案 id
267
+ * @returns {{declared: number, present: number, missing: string[], undeclared: string[], archived: string[], status: string}}
268
+ */
269
+ export function checkOrganization(organization, rosterIds) {
270
+ const onDisk = new Set(rosterIds);
271
+ const declared = new Set(organization === null ? [] : organization.rosters.map((item) => item.id));
272
+ const missing = [...declared].filter((id) => !onDisk.has(id)).sort();
273
+ const undeclared = [...onDisk].filter((id) => !declared.has(id)).sort();
274
+ const archived = organization === null
275
+ ? []
276
+ : organization.rosters.filter((item) => item.status === "archived" && onDisk.has(item.id)).map((item) => item.id).sort();
277
+ return {
278
+ declared: declared.size,
279
+ present: onDisk.size,
280
+ missing,
281
+ undeclared,
282
+ archived,
283
+ status: organization === null ? "absent" : missing.length > 0 ? "incomplete" : "ok",
284
+ };
285
+ }
286
+
287
+ /**
288
+ * 把交叉核对结果折成诊断。
289
+ *
290
+ * @param {object|null} organization
291
+ * @param {object} check {@link checkOrganization} 的返回值
292
+ * @param {string} file 组织文件相对路径
293
+ * @returns {object[]}
294
+ */
295
+ export function organizationDiagnostics(organization, check, file) {
296
+ const diagnostics = [];
297
+ if (organization === null) return diagnostics;
298
+ if (check.missing.length > 0) {
299
+ diagnostics.push(
300
+ diagnose(
301
+ "error",
302
+ "organization-roster-missing",
303
+ file,
304
+ `组织声明了 ${check.missing.length} 份档案但库里找不到:${check.missing.join(", ")}。` +
305
+ "要么把目录加回来,要么把它们从 organization 里删掉——组织页不能比磁盘更乐观。",
306
+ ),
307
+ );
308
+ }
309
+ if (check.undeclared.length > 0) {
310
+ diagnostics.push(
311
+ diagnose(
312
+ "warn",
313
+ "organization-roster-undeclared",
314
+ file,
315
+ `库里有 ${check.undeclared.length} 份档案没在组织里登记:${check.undeclared.join(", ")}。` +
316
+ "它们仍会被加载,但组织页看不到它们——补一行登记,或确认它们不该在这里。",
317
+ ),
318
+ );
319
+ }
320
+ if (check.archived.length > 0) {
321
+ diagnostics.push(
322
+ diagnose(
323
+ "info",
324
+ "organization-roster-archived",
325
+ file,
326
+ `已归档但仍在库里的档案:${check.archived.join(", ")}。归档只是声明,不影响加载。`,
327
+ ),
328
+ );
329
+ }
330
+ return diagnostics;
331
+ }
package/lib/registry.js CHANGED
@@ -27,6 +27,12 @@ import nodePath from "node:path";
27
27
  import { CAPABILITIES_FILE_NAMES, validateCapabilities } from "./capabilities.js";
28
28
  import { sha256Hex } from "./digest.js";
29
29
  import { MEMBER_FILE_NAMES } from "./format.js";
30
+ import {
31
+ ORGANIZATION_FILE_NAMES,
32
+ checkOrganization,
33
+ organizationDiagnostics,
34
+ validateOrganization,
35
+ } from "./organization.js";
30
36
  import { parseDocumentText } from "./parse.js";
31
37
  import { validateRoster } from "./validate.js";
32
38
 
@@ -57,6 +63,9 @@ export function scanRosterLibrary({ rootDir, knownToolPolicies = null }) {
57
63
  const capabilities = {};
58
64
  /** 档案 id → 能力要求的文件名(诊断要指到具体那份文件)。 */
59
65
  const capabilityFiles = {};
66
+ /** 组织档案(organization.yaml)与它的交叉核对结果。 */
67
+ let organization = null;
68
+ let organizationFile = null;
60
69
  const stats = {
61
70
  total: 0,
62
71
  ok: 0,
@@ -82,6 +91,8 @@ export function scanRosterLibrary({ rootDir, knownToolPolicies = null }) {
82
91
  if (child.name.startsWith(".") || SKIP_DIRS.has(child.name) || child.isSymbolicLink()) continue;
83
92
 
84
93
  if (!child.isDirectory()) {
94
+ // 库根上的 organization.yaml 是合法的:它是这座库的目录页,不是散落的档案。
95
+ if (ORGANIZATION_FILE_NAMES.includes(child.name)) continue;
85
96
  // 散落的 *.yaml 曾经是另一种布局;现在只认目录,必须明确告知而不是忽略。
86
97
  diagnostics.push({
87
98
  severity: "warn",
@@ -125,6 +136,17 @@ export function scanRosterLibrary({ rootDir, knownToolPolicies = null }) {
125
136
  }
126
137
  }
127
138
 
139
+ // 组织档案在目录扫完之后读:它的交叉核对需要知道磁盘上到底有哪些档案。
140
+ const wantedOrganization = loadOrganization(rootDir, nodePath.basename(rootDir));
141
+ organization = wantedOrganization.organization;
142
+ organizationFile = wantedOrganization.file;
143
+ diagnostics.push(...wantedOrganization.diagnostics);
144
+ const organizationCheck = checkOrganization(
145
+ organization,
146
+ rosters.map((item) => item.id),
147
+ );
148
+ diagnostics.push(...organizationDiagnostics(organization, organizationCheck, organizationFile ?? ORGANIZATION_FILE_NAMES[0]));
149
+
128
150
  if (stats.total > 0 && rosters.length === 0) {
129
151
  diagnostics.push({
130
152
  severity: "error",
@@ -165,7 +187,19 @@ export function scanRosterLibrary({ rootDir, knownToolPolicies = null }) {
165
187
  String(left.file ?? "").localeCompare(String(right.file ?? "")),
166
188
  );
167
189
 
168
- return { rootDir, missing: childDirs === null, entries, rosters, capabilities, capabilityFiles, stats, diagnostics };
190
+ return {
191
+ rootDir,
192
+ missing: childDirs === null,
193
+ entries,
194
+ rosters,
195
+ capabilities,
196
+ capabilityFiles,
197
+ organization,
198
+ organizationFile,
199
+ organizationCheck,
200
+ stats,
201
+ diagnostics,
202
+ };
169
203
  }
170
204
 
171
205
  /**
@@ -280,6 +314,54 @@ function loadDocument(found, expectedId, { knownToolPolicies }, rawText = null)
280
314
  * @param {string} dirName 目录名(同时也是期望的 `roster` 字段)
281
315
  * @returns {{ capabilities: object | null, file: string | null, diagnostics: object[] }}
282
316
  */
317
+ export function loadOrganization(rootDir, dirName) {
318
+ let names;
319
+ try {
320
+ names = readdirSync(rootDir);
321
+ } catch {
322
+ return { organization: null, file: null, diagnostics: [] };
323
+ }
324
+ const present = ORGANIZATION_FILE_NAMES.filter((name) => names.includes(name));
325
+ if (present.length === 0) return { organization: null, file: null, diagnostics: [] };
326
+
327
+ const diagnostics = [];
328
+ const relative = present[0];
329
+ if (present.length > 1) {
330
+ diagnostics.push({
331
+ severity: "warn",
332
+ code: "multiple-organization-files",
333
+ path: "",
334
+ file: relative,
335
+ reason:
336
+ `档案库同时存在 ${present.join("、")},只读 ${present[0]};` +
337
+ "请删掉多余文件,否则另一份永远不会生效。",
338
+ });
339
+ }
340
+
341
+ let raw;
342
+ try {
343
+ raw = readFileSync(nodePath.join(rootDir, relative), "utf8");
344
+ } catch (error) {
345
+ diagnostics.push({
346
+ severity: "error",
347
+ code: "unreadable-file",
348
+ path: "",
349
+ file: relative,
350
+ reason: `文件不可读:${messageOf(error)}`,
351
+ });
352
+ return { organization: null, file: relative, diagnostics };
353
+ }
354
+
355
+ const format = relative.endsWith(".json") ? "json" : "yaml";
356
+ const parsed = parseDocumentText(raw, { file: relative, format });
357
+ if (!parsed.ok) return { organization: null, file: relative, diagnostics: [...diagnostics, ...parsed.diagnostics] };
358
+
359
+ const validated = validateOrganization(parsed.value, { file: relative, expectedId: dirName });
360
+ diagnostics.push(...validated.diagnostics);
361
+ if (!validated.ok) return { organization: null, file: relative, diagnostics };
362
+ return { organization: validated.organization, file: relative, diagnostics };
363
+ }
364
+
283
365
  export function loadCapabilities(dirPath, dirName) {
284
366
  let names;
285
367
  try {
package/lib/remote.js CHANGED
@@ -42,7 +42,7 @@ export const MIN_REFRESH_MS = 60 * 1000;
42
42
 
43
43
  /** 官方 registry 地址(本仓库的 `index/registry.json`,合并 PR 即更新)。 */
44
44
  export const DEFAULT_REGISTRY_URL =
45
- "https://raw.githubusercontent.com/STofone/dsh-agent-roster/main/index/registry.json";
45
+ "https://raw.githubusercontent.com/dsh-agent-roster/dsh-agent-roster/main/index/registry.json";
46
46
 
47
47
  /** 缓存目录名,放在档案库根下,跟着 `$DSH_HOME` 走。 */
48
48
  export const CACHE_DIR_NAME = ".index-cache";
package/lib/render.js CHANGED
@@ -89,6 +89,21 @@ export function renderRosterList(scan) {
89
89
  const { entries, stats, rootDir } = scan;
90
90
  const lines = [];
91
91
  lines.push(`档案库 ${rootDir}`);
92
+ const organization = scan.organization ?? null;
93
+ if (organization !== null) {
94
+ const check = scan.organizationCheck ?? { declared: 0, present: 0, missing: [], undeclared: [], archived: [] };
95
+ lines.push(`组织 ${organization.id} ${organization.title}`);
96
+ if (organization.description !== "") lines.push(` ${organization.description}`);
97
+ const parts = [
98
+ `登记 ${check.declared}`,
99
+ `在库 ${check.present}`,
100
+ `缺 ${check.missing.length}`,
101
+ `未登记 ${check.undeclared.length}`,
102
+ ];
103
+ if (check.archived.length > 0) parts.push(`已归档 ${check.archived.length}`);
104
+ if (organization.maintainers.length > 0) parts.push(`维护者 ${organization.maintainers.join(", ")}`);
105
+ lines.push(` ${parts.join(" · ")}`);
106
+ }
92
107
  if (entries.length === 0) {
93
108
  lines.push("");
94
109
  lines.push(scan.missing === true ? "(目录尚未创建:保存第一份档案时会自动建)" : "(空:没有任何 roster 目录)");
package/lib/tools.js CHANGED
@@ -94,6 +94,7 @@ export function registerRosterTools(ctx, config, meta) {
94
94
  const text = [
95
95
  statusLine("roster_list", {
96
96
  rootDir: rosterDir,
97
+ organization: scan.organization === null ? "none" : scan.organization.id,
97
98
  total: scan.stats.total,
98
99
  ok: scan.stats.ok,
99
100
  failed: scan.stats.failed,
@@ -106,6 +107,8 @@ export function registerRosterTools(ctx, config, meta) {
106
107
  workspace,
107
108
  rootDir: rosterDir,
108
109
  stats: scan.stats,
110
+ organization: scan.organization,
111
+ organizationCheck: scan.organizationCheck,
109
112
  rosters: scan.rosters.map((roster) => ({
110
113
  id: roster.id,
111
114
  version: roster.version,
@@ -0,0 +1,67 @@
1
+ # API 设计评审组
2
+ #
3
+ # 本档案由本项目自撰(未复制任何上游文本),用于演示同一座档案库里的多种团队形态。
4
+ # 目录约定:<root>/<roster-id>/roster.yaml,且 roster 必须等于目录名。
5
+ kind: dsh-agent-roster/v1
6
+ roster: api-design
7
+ version: 1.0.0
8
+ title: API 设计评审组
9
+ description: 在写第一行实现之前,把接口的形状、演进与失败语义定下来。
10
+ license: MIT
11
+ maintainers:
12
+ - STofone
13
+ tags:
14
+ - api
15
+ - design
16
+ - review
17
+
18
+ defaults:
19
+ provider: deepseek
20
+ model: deepseek-v4.1-flash
21
+ reasoningEffort: high
22
+
23
+ members:
24
+ - name: lead
25
+ role: 接口负责人
26
+ title: 决定这版接口要不要开工
27
+ description: 定资源模型与边界,只在两方僵持时裁决;不写实现。
28
+ systemPrompt: |
29
+ 你负责这次接口评审的编排:先把资源模型、边界与调用方列清楚,再分给两名评审者,最后合并成一份结论。
30
+ 不要自己写实现,也不要替调用方假设需求;需求不明就明确写“需要调用方确认”,不要补全。
31
+ 结论必须能直接贴进设计文档:决定在前、理由在后,每条都注明是哪位评审者的证据。
32
+ 证据不足时给出“暂不定”的选项,并写清还差哪一条信息才能定。
33
+ tags:
34
+ - orchestration
35
+ - name: schema
36
+ role: 结构与演进评审
37
+ title: 看命名、分页、错误码与兼容性
38
+ description: 只关心结构能不能长期演进:字段命名、分页、幂等、版本与弃用路径。
39
+ toolScope:
40
+ allow:
41
+ - read
42
+ - grep
43
+ - glob
44
+ systemPrompt: |
45
+ 你只看结构层面:字段命名是否自解释、分页与过滤是否可组合、写操作是否幂等、错误码能否让调用方分辨“重试有用/没用”。
46
+ 不评论实现细节与性能数据,那是另一个人的事;也不要提议重写整个资源模型。
47
+ 每条问题给出最小修改方向,并标注它是“现在改”还是“下个版本改”。
48
+ 对兼容性拿不准时,写成“破坏性:是/不确定”,不要默认无害。
49
+ tags:
50
+ - schema
51
+ - compatibility
52
+ - name: failure-mode
53
+ role: 失败语义评审
54
+ title: 想清楚超时、重试与部分成功
55
+ description: 逐个接口问“失败时调用方看到什么”,把部分成功与重试放大写进契约。
56
+ toolScope:
57
+ allow:
58
+ - read
59
+ - grep
60
+ - glob
61
+ systemPrompt: |
62
+ 你只问一件事:这个接口失败时调用方看到什么、能不能安全重试、会不会把一次失败放大成多次副作用。
63
+ 不讨论字段命名与版本策略;只输出失败场景、可观测信号与契约要求。
64
+ 每条场景写成“触发条件 → 调用方看到的状态 → 要求实现方保证什么”。
65
+ 无法判定是否幂等的接口,直接要求补幂等键,不要用“一般来说”糊过去。
66
+ tags:
67
+ - reliability
@@ -0,0 +1,55 @@
1
+ # 内容运营组
2
+ #
3
+ # 本档案由本项目自撰(未复制任何上游文本),用于演示同一座档案库里的多种团队形态。
4
+ # 目录约定:<root>/<roster-id>/roster.yaml,且 roster 必须等于目录名。
5
+ kind: dsh-agent-roster/v1
6
+ roster: content-ops
7
+ version: 1.0.0
8
+ title: 内容运营组
9
+ description: 一次发布:选题、成稿、分发,三件事各有验收。
10
+ license: MIT
11
+ maintainers:
12
+ - STofone
13
+ tags:
14
+ - content
15
+ - publishing
16
+
17
+ defaults:
18
+ provider: deepseek
19
+ model: deepseek-v4.1-flash
20
+ reasoningEffort: high
21
+
22
+ members:
23
+ - name: lead
24
+ role: 内容负责人
25
+ title: 定选题与验收
26
+ description: 决定写给谁、解决什么、发到哪里;不写正文。
27
+ systemPrompt: |
28
+ 你定选题:读者是谁、读完能带走什么、发在哪个渠道,以及每项的验收。
29
+ 不写正文,也不做排版;你负责取舍与排期。
30
+ 产出是一页 brief:读者 · 主张 · 证据要求 · 渠道 · 截止。
31
+ 选题无法说清“读者为什么要读”时,换题而不是硬写。
32
+ tags:
33
+ - brief
34
+ - name: editor
35
+ role: 成稿编辑
36
+ title: 让主张站得住、句子能被读完
37
+ description: 砍掉形容词,补上证据;每段只做一件事。
38
+ systemPrompt: |
39
+ 你编辑成稿:每段一个意思,删掉无法验证的形容词,给每个主张补证据或删除。
40
+ 不改事实,也不加未经验证的数据;发现缺证据就标出来让对方补。
41
+ 交付改后稿 + 一段“我删了什么、为什么”。
42
+ 拿不准的事实一律降级为“据说/待确认”,不要保留确定性语气。
43
+ tags:
44
+ - editing
45
+ - name: distributor
46
+ role: 分发与回读
47
+ title: 让内容被该看到的人看到
48
+ description: 按渠道改写首段与标题,发布后回收读数。
49
+ systemPrompt: |
50
+ 你按渠道改写标题与首段(同一主张不同入口),并规划发布时间与配套动作。
51
+ 不改正文主张;只调整入口表达与分发节奏。
52
+ 发布后回收读数:曝光、点击、读完率,并说明哪些指标不可得。
53
+ 读数缺失时如实写“无法获取”,不要用估算值冒充实测。
54
+ tags:
55
+ - distribution
@@ -0,0 +1,58 @@
1
+ # 合同审阅组
2
+ #
3
+ # 本档案由本项目自撰(未复制任何上游文本),用于演示同一座档案库里的多种团队形态。
4
+ # 目录约定:<root>/<roster-id>/roster.yaml,且 roster 必须等于目录名。
5
+ kind: dsh-agent-roster/v1
6
+ roster: contract-review
7
+ version: 1.0.0
8
+ title: 合同审阅组
9
+ description: 找到会让你赔钱、被锁定或无法退出的条款。
10
+ license: MIT
11
+ maintainers:
12
+ - STofone
13
+ tags:
14
+ - legal
15
+ - contract
16
+
17
+ defaults:
18
+ provider: deepseek
19
+ model: deepseek-v4.1-flash
20
+ reasoningEffort: high
21
+
22
+ members:
23
+ - name: lead
24
+ role: 审阅负责人
25
+ title: 定立场与红线
26
+ description: 确认我方角色、可接受与不可接受的条款;不逐条读。
27
+ systemPrompt: |
28
+ 你先确认我方角色与商业目标,再定红线:哪些条款出现就不签。
29
+ 不逐条解释条款;你负责范围与最终建议(签 / 改 / 不签)。
30
+ 产出是红线清单 + 建议的谈判顺序。
31
+ 立场不明时先问“最坏情况下我们能承受什么”,不要默认接受。
32
+ tags:
33
+ - position
34
+ - name: clause-reader
35
+ role: 条款识别
36
+ title: 把关键条款摘出来并定位
37
+ description: 找责任限制、赔偿、终止、续约、知识产权、争议解决。
38
+ toolScope:
39
+ allow:
40
+ - read
41
+ systemPrompt: |
42
+ 你摘出关键条款:责任上限、赔偿、终止与续约、知识产权归属、保密、争议解决。
43
+ 不做商业判断;每条必须给条款编号与原文摘录。
44
+ 输出“条款 · 原文 · 位置 · 对我方的影响(有利/中性/不利)”。
45
+ 条款缺失同样重要:明确写“未见某类条款”,不要当作默认有利。
46
+ tags:
47
+ - clauses
48
+ - name: risk-note
49
+ role: 风险提示
50
+ title: 把条款翻译成钱的后果
51
+ description: 用最坏情形估算敞口,给出可接受的修改方向。
52
+ systemPrompt: |
53
+ 你把条款翻译成后果:最坏情形下赔多少、被锁定多久、退出成本多少。
54
+ 不写法律意见书,也不替代律师;你给的是量化提示与修改方向。
55
+ 输出“风险 · 触发条件 · 最坏敞口 · 建议改法(一句话)”。
56
+ 无法量化时给区间并说明假设,不要给“风险较低”这种结论。
57
+ tags:
58
+ - risk