@stofone/dsh-agent-roster 0.10.0 → 0.10.2

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.10.2] - 2026-11-03
8
+
9
+ ### Fixed
10
+
11
+ - **Every v2 collection was unfetchable (HTTP 404).** The index assumed the v1 flat layout
12
+ (`<path>/<id>/roster.yaml`), which stopped existing the moment rosters moved to `rosters/<id>/`.
13
+ A collection may now pin one relative path per roster in an optional `files` map;
14
+ `registry sync` fills it in and points v2 rosters at the self-contained `dist/<id>-<ver>.json`
15
+ (an assembly alone cannot be pulled — its members are role references).
16
+ - Pulling a compiled artifact flattens it into an inline v1 document (personas are the assembled
17
+ prompt, which is what would actually be sent); run `migrate` afterwards if you want the role split
18
+ on your machine. A bare `roster@2` is reported as `collection-roster-needs-roles` instead of being
19
+ fetched as if it were one file.
20
+ - `flattenCompiledToRoster` no longer emits empty `title`/`description` strings — v1 rejects those,
21
+ so a flattened artifact used to fail its own validation.
22
+
23
+ ## [0.10.1] - 2026-11-03
24
+
25
+ ### Docs
26
+
27
+ - The Chinese README said `migrate` was "not implemented", listed one example library where there are
28
+ two, and had a file tree from before `roles/`, `roster.v2` and `compiled.v1` existed — docs that
29
+ describe last week's format read as current, which is worse than no docs.
30
+ - Both READMEs now carry the same two sections: one-command v1→v2 migration (including why role names
31
+ are prefixed and why a colliding captain gets `-captain`), and feeding a failed round back into the
32
+ plan with `compile --run`.
33
+
7
34
  ## [0.10.0] - 2026-11-03
8
35
 
9
36
  ### Roles, rosters and compiled artifacts (v2)
@@ -445,7 +472,10 @@
445
472
  - 工具返回值含 `undefined` 键,运行时按 `value is not lossless JSON` 整次拒绝;缺失值统一写 `null`,
446
473
  且所有返回值先过 `lib/json.js#lossless`。
447
474
 
448
- [Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.9.2...main
475
+ [Unreleased]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.2...main
476
+ [0.10.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.1...v0.10.2
477
+ [0.10.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/compare/v0.10.0...v0.10.1
478
+ [0.10.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.10.0
449
479
  [0.9.2]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.2
450
480
  [0.9.1]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.1
451
481
  [0.9.0]: https://github.com/dsh-agent-roster/dsh-agent-roster/releases/tag/v0.9.0
package/README.md CHANGED
@@ -383,6 +383,10 @@ repository's `index/registry.json`, and anyone who subscribes can see them in th
383
383
  - **Pulling**: writes are idempotent and report one of `created`, `updated` (explicit overwrite), `unchanged`
384
384
  (same content, nothing written) or `conflict` (the local copy differs and is **not** overwritten by default).
385
385
  Conflicts are yours to resolve: merge in the editor, or pull again with `overwrite` in the request body.
386
+ - **Where each roster lives**: a collection's `rosters[]` is just the list of ids; the optional
387
+ `files` map says where each one is (`{"core-review": "dist/core-review-1.0.0.json"}`). It is optional —
388
+ without it the fetcher assumes the v1 layout `<id>/roster.yaml` — but a v2 library needs it, because an
389
+ assembly only references roles and cannot be pulled on its own. `registry sync` writes this map for you.
386
390
  - **The index has a schema**: [`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) mirrors the
387
391
  runtime validator, so an editor flags a typo before CI does.
388
392
  - **Publishing your own**: add an entry to `collections` in `index/registry.json` (`id`, `repo: github:owner/repo`,
@@ -450,7 +454,8 @@ lib/ format, parse, validate, registry, bridge, verify, render, lib
450
454
  config-view, json, http, write, service, teams-state, tools
451
455
  client.js client plugin: sidebar entry, main panel (rosters/teams/health), settings section
452
456
  locale/ panel + listing dictionaries (en/zh)
453
- schema/ roster / capabilities / organization / registry v1 schemas (editor completion; kept aligned by tests)
457
+ schema/ roster v1 / role v1 / roster v2 / compiled v1 / capabilities / organization / registry
458
+ schemas (editor completion; kept aligned by tests)
454
459
  bin/cli.mjs validate/list/compile/migrate/registry for CI
455
460
  scripts/ host-smoke.mjs, client-smoke.mjs, check-locale.mjs (dev only, not shipped)
456
461
  examples/ a v2 library migrated from the v1 archives
package/README.zh.md CHANGED
@@ -82,7 +82,7 @@ captain: # 可选:记录队长(=创建团队的那个
82
82
 
83
83
  JSON 也可以(`roster.json`)。同一目录下按 `roster.yaml` → `roster.yml` → `roster.json` 顺序探测,**只认一份**;出现第二份会报诊断,绝不静默挑一份。
84
84
 
85
- 本包自带两份示例档案库([`examples/`](./examples))。`rosterDirs` 默认留空,也就是上面那座全局库;
85
+ 本包自带两份示例档案库:[`examples/`](./examples)(v2,由本仓历史档案迁移而来)与 [`examples-orchestration/`](./examples-orchestration)(v2,手写,用来演示门与施工图)。`rosterDirs` 默认留空,也就是上面那座全局库;
86
86
  新装时它是空的,所以面板一开始显示「0 份档案」——在面板里新建一份即可(目录会自动创建)。想先看示例库:
87
87
 
88
88
  ```bash
@@ -227,7 +227,7 @@ rosters/<id>/roster.yaml 组装:成员是引用,队长独立成段
227
227
  dist/<id>-<ver>.json 编译产物:自包含、可哈希,消费者只看它
228
228
  ```
229
229
 
230
- 判别键决定版本——`dsh-agent-roster/role@1`、`dsh-agent-roster/roster@2`、`dsh-agent-roster/compiled@1`。同一目录不允许两种格式共存:v1 冻结,`roster migrate`(尚未实现)负责原地重写。
230
+ 判别键决定版本——`dsh-agent-roster/role@1`、`dsh-agent-roster/roster@2`、`dsh-agent-roster/compiled@1`。同一目录不允许两种格式共存:v1 冻结,`migrate` 负责原地重写。
231
231
 
232
232
  拼装写进格式,而不是藏在代码里:
233
233
 
@@ -260,6 +260,28 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review --j
260
260
 
261
261
  `orchestration` 是格式的一部分,但编译时标 `status: "declaration"`:阶段与门会被校验(门的 `after` 必须指向真实阶段,`by` 必须指向真实成员),但没有任何东西执行它。能声明不等于已被证明会运行,所以产物在 `declarations[]` 里逐条说明谁属于哪一类。完整契约——字段归宿、拼装语义、身份与唯一性、迁移规则——见 [`docs/roster-v2.md`](./docs/roster-v2.md);schema 见 [`schema/role.v1.schema.json`](./schema/role.v1.schema.json)、[`schema/roster.v2.schema.json`](./schema/roster.v2.schema.json)、[`schema/compiled.v1.schema.json`](./schema/compiled.v1.schema.json)。
262
262
 
263
+ ### 从 v1 到 v2,一条命令
264
+
265
+ ```bash
266
+ node bin/cli.mjs migrate --dir examples # 只出计划:要写哪些文件、能不能编译
267
+ node bin/cli.mjs migrate --dir examples --write # 原地重写:roles/ + rosters/ + dist/,删掉旧目录
268
+ ```
269
+
270
+ 迁移逐字保留人设:成员的 `systemPrompt` 原文成为 `roles/<档案>-<成员>/AGENT.md` 的正文,成员本身变成引用。队长不再是一个散字段,而是自己的角色文件——与成员同名时得到 `-captain` 后缀,因为那是两份人设,悄悄合并会丢掉一份。
271
+
272
+ 角色名带档案 id 前缀,是因为 v2 要求同库角色名唯一:一座库里 21 套组装都叫自己的队长 `lead`,不可能有 21 个同名文件。
273
+
274
+ ### 把失败的一轮喂回施工图
275
+
276
+ `compile --run <文件>` 读执行回执(`run@1`),把上一轮的失败并进重试卡的 description:
277
+
278
+ ```bash
279
+ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review \
280
+ --run examples-orchestration/dist/core-review-1.0.0.run.json
281
+ ```
282
+
283
+ 回执由执行施工图的队长会话写,不由插件写:任务 id 属于团队,插件从不写团队状态。插件负责的是合并——只活在红卡上的失败,下一次编译就消失了,门也就变成了装饰。
284
+
263
285
  ## 工具
264
286
 
265
287
  | 工具 | 作用 |
@@ -342,6 +364,9 @@ node bin/cli.mjs compile --roster examples-orchestration/rosters/core-review --j
342
364
  冲突要你自己决定:要么在编辑器里合并,要么再点一次「拉取到本机」并在请求里带 `overwrite`。
343
365
  - **索引本身也有 schema**:[`schema/registry.v1.schema.json`](./schema/registry.v1.schema.json) 与运行时校验器对齐,
344
366
  拼错字段在编辑器里就会提示,不必等 CI。
367
+ - **每份档案住在哪**:collection 的 `rosters[]` 只是 id 名单,实际位置由可选的 `files` 映射给出
368
+ (如 `{"core-review": "dist/core-review-1.0.0.json"}`)。不写就按 v1 布局 `<id>/roster.yaml` 猜——v2 的库必须写,
369
+ 因为组装本身只引用角色,单独拉一个文件拉不到角色目录。`registry sync` 会替你填。
345
370
  - **登记自己的档案**:在 `index/registry.json` 的 `collections` 里加一条(`id`、`repo: github:owner/repo`、
346
371
  `ref`、`path`、`rosters[]`),提交 PR 即可;`.github/workflows/registry.yml` 会校验索引与仓库真实文件是否对得上。
347
372
 
@@ -405,11 +430,13 @@ lib/ format / parse / validate / registry / bridge / verify / rende
405
430
  config-view / json / http / write / service / teams-state / tools
406
431
  client.js 客户端半边:左侧栏图标 + 主面板(档案/团队/体检)+ 设置页小节
407
432
  locale/ 面板与列表词典(en/zh)
408
- schema/ roster / capabilities / organization / registry v1 schema(编辑器补全用,与校验器由测试对齐)
409
- bin/cli.mjs validate / list / version,给 CI 用
433
+ schema/ roster v1 / role v1 / roster v2 / compiled v1 / capabilities / organization / registry
434
+ schema(编辑器补全用,与校验器由测试对齐)
435
+ bin/cli.mjs validate / list / compile / migrate / registry,给 CI 用
410
436
  scripts/ host-smoke.mjs、client-smoke.mjs、check-locale.mjs(开发用,不随包发布)
411
- examples/ 两座示例档案库
412
- orgs/ 带 organization.yaml 的示例库(21 套档案)
437
+ examples/ 由历史 v1 档案迁移来的 v2 库(roles/ + rosters/ + dist/)
438
+ examples-orchestration/ 手写的 v2 库:编排、门与施工图
439
+ orgs/ 带 organization.yaml 的示例库(21 套 v2 档案)
413
440
  test/ 单测
414
441
  ```
415
442
 
package/bin/cli.mjs CHANGED
@@ -26,6 +26,7 @@ import { compileRosterAt } from "../lib/compile2.js";
26
26
  import { migrateLibrary } from "../lib/migrate.js";
27
27
  import { compilePlan } from "../lib/plan.js";
28
28
  import { mergeRunIntoPlan, validateRun } from "../lib/run.js";
29
+ import { DIST_DIR } from "../lib/migrate.js";
29
30
 
30
31
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
31
32
 
@@ -442,6 +443,17 @@ async function runRegistry(options) {
442
443
  entry.rosters = ids;
443
444
  changed = true;
444
445
  }
446
+ const files = {};
447
+ for (const roster of scanned.rosters.slice().sort((a, b) => a.id.localeCompare(b.id))) {
448
+ files[roster.id] =
449
+ roster.format === "v2"
450
+ ? `${DIST_DIR}/${roster.id}-${roster.version}.json`
451
+ : `${roster.id}/${roster.file ?? "roster.yaml"}`;
452
+ }
453
+ if (JSON.stringify(files) !== JSON.stringify(entry.files ?? {})) {
454
+ entry.files = files;
455
+ changed = true;
456
+ }
445
457
  }
446
458
  if (options.write) {
447
459
  writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
package/docs/roster-v2.md CHANGED
@@ -212,9 +212,14 @@ gates:
212
212
 
213
213
  三者正交:`requires` 说"必须有",`capabilities.yaml` 说"这份组装必须有",`organization.yaml` 说"这座库有哪些"。
214
214
 
215
+ 远端的 registry 索引也是同理:登记的 `rosters[]` 只是 id 名单,实际位置由 `files` 映射给出。
216
+ v1 的库可以省略(回退成 `<id>/roster.yaml`);v2 的库必须写,**指向 `dist/` 的编译产物**——
217
+ 组装文件本身只引用角色,单独拉一个 `roster.yaml` 拉不到角色目录,那不是"没实现",是"拉下来也不完整"。
218
+ 拉到编译产物后,客户端把它摊成一份内联 v1 文档(人设就是拼装结果),要角色分层就在本地跑 `migrate`。
219
+
215
220
  ## 10. 未决与下一步
216
221
 
217
222
  1. **施工图 ↔ 团队真实卡 id 的映射**:`run@1` 已经能读、能合并(`compile --run` 会把上一轮失败写进重试卡),但「施工图内部 id ↔ 团队真实 task id」的映射表还没落成文件——目前靠执行者按 id 命名建卡。
218
- 2. `dist` 的发布与 `registry` 只登记产物(现在 registry 登记的是档案路径)。
223
+ 2. `dist` 的发布流程(索引已能逐份指向 `dist/<id>-<ver>.json`,但"从 dist 反推角色版本、做跨库一致性检查"还没做)。
219
224
  3. `profile/agent-preset` 目标:把队长与成员的 preset 片段生成出来,仍然只产参不写入。
220
225
  4. 编辑器支持:三个 schema 已就位,LSP 级校验待做。
@@ -2,7 +2,7 @@
2
2
  "$schema": "../schema/registry.v1.schema.json",
3
3
  "kind": "dsh-agent-roster-registry/v1",
4
4
  "version": 1,
5
- "generatedAt": "2026-11-02T17:00:43.713Z",
5
+ "generatedAt": "2026-11-03T05:50:01.540Z",
6
6
  "generator": "dsh-agent-roster/dsh-agent-roster (hand-maintained; verified by .github/workflows/registry.yml)",
7
7
  "source": "https://github.com/dsh-agent-roster/dsh-agent-roster",
8
8
  "collections": [
@@ -26,7 +26,11 @@
26
26
  "maintainers": [
27
27
  "STofone"
28
28
  ],
29
- "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster#examples"
29
+ "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster#examples",
30
+ "files": {
31
+ "core-review": "dist/core-review-1.0.0.json",
32
+ "research": "dist/research-0.1.0.json"
33
+ }
30
34
  },
31
35
  {
32
36
  "id": "agent-roster-collective",
@@ -70,7 +74,30 @@
70
74
  "maintainers": [
71
75
  "STofone"
72
76
  ],
73
- "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster/tree/main/orgs/agent-roster-collective"
77
+ "homepage": "https://github.com/dsh-agent-roster/dsh-agent-roster/tree/main/orgs/agent-roster-collective",
78
+ "files": {
79
+ "api-design": "dist/api-design-1.0.0.json",
80
+ "content-ops": "dist/content-ops-1.0.0.json",
81
+ "contract-review": "dist/contract-review-1.0.0.json",
82
+ "data-quality": "dist/data-quality-1.0.0.json",
83
+ "db-migration": "dist/db-migration-1.0.0.json",
84
+ "docs-writer": "dist/docs-writer-1.0.0.json",
85
+ "finance-recon": "dist/finance-recon-1.0.0.json",
86
+ "frontend-ui": "dist/frontend-ui-1.0.0.json",
87
+ "i18n-l10n": "dist/i18n-l10n-1.0.0.json",
88
+ "incident-response": "dist/incident-response-1.0.0.json",
89
+ "literature-review": "dist/literature-review-1.0.0.json",
90
+ "market-research": "dist/market-research-1.0.0.json",
91
+ "ml-eval": "dist/ml-eval-1.0.0.json",
92
+ "performance": "dist/performance-1.0.0.json",
93
+ "product-discovery": "dist/product-discovery-1.0.0.json",
94
+ "qa-regression": "dist/qa-regression-1.0.0.json",
95
+ "recruiting": "dist/recruiting-1.0.0.json",
96
+ "release-manager": "dist/release-manager-1.0.0.json",
97
+ "security-audit": "dist/security-audit-1.0.0.json",
98
+ "sre-oncall": "dist/sre-oncall-1.0.0.json",
99
+ "support-triage": "dist/support-triage-1.0.0.json"
100
+ }
74
101
  },
75
102
  {
76
103
  "id": "agent-teams",
package/lib/compile2.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  ROLE_PARTS_DIR,
22
22
  ROSTER2_FILE_NAMES,
23
23
  } from "./format-v2.js";
24
+ import { ROSTER_KIND } from "./format.js";
24
25
  import { diagnostic, parseDocumentText } from "./parse.js";
25
26
  import { loadRoleAt, safeResolve } from "./role.js";
26
27
  import { validateRoster2 } from "./roster2.js";
@@ -520,3 +521,110 @@ export function rosterViewOf(compiled, roster2, options = {}) {
520
521
  orchestration: compiled.orchestration ?? null,
521
522
  };
522
523
  }
524
+
525
+ /**
526
+ * 校验一份编译产物。
527
+ *
528
+ * 这是「消费者只看产物」的入口:远端订阅拿到的就是它,所以这里只认产物该有的形状,
529
+ * 不回头读角色文件——拉一份档案不该变成拉一个仓库。
530
+ *
531
+ * @param {unknown} value
532
+ * @param {{ file?: string | null }} [options]
533
+ * @returns {{ ok: boolean, compiled: object | null, diagnostics: object[] }}
534
+ */
535
+ export function validateCompiled(value, { file = null } = {}) {
536
+ const diagnostics = [];
537
+ const at = (relative) => (file === null ? relative : `${file} · ${relative}`);
538
+ const fail = () => ({ ok: false, compiled: null, diagnostics });
539
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
540
+ diagnostics.push({ severity: "error", code: "compiled-not-object", reason: "编译产物必须是一个对象", file });
541
+ return fail();
542
+ }
543
+ if (value.kind !== COMPILED_KIND) {
544
+ diagnostics.push({
545
+ severity: "error",
546
+ code: "compiled-kind",
547
+ reason: `判别键必须是 ${COMPILED_KIND},实际是 ${JSON.stringify(value.kind ?? null)}`,
548
+ file,
549
+ });
550
+ }
551
+ const source = value.source;
552
+ if (source === null || typeof source !== "object") {
553
+ diagnostics.push({ severity: "error", code: "compiled-source", reason: "缺少 source", file });
554
+ } else {
555
+ for (const key of ["roster", "version"]) {
556
+ if (typeof source[key] !== "string" || source[key] === "") {
557
+ diagnostics.push({
558
+ severity: "error",
559
+ code: "compiled-source",
560
+ reason: `source.${key} 必须是非空字符串`,
561
+ file: at("source"),
562
+ });
563
+ }
564
+ }
565
+ }
566
+ const members = value.members;
567
+ if (!Array.isArray(members) || members.length === 0) {
568
+ diagnostics.push({ severity: "error", code: "compiled-members", reason: "members 必须是非空数组", file });
569
+ } else {
570
+ for (const [index, member] of members.entries()) {
571
+ const where = at(`members[${String(index)}]`);
572
+ if (member === null || typeof member !== "object") {
573
+ diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员必须是对象", file: where });
574
+ continue;
575
+ }
576
+ if (typeof member.name !== "string" || member.name === "") {
577
+ diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员缺少 name", file: where });
578
+ }
579
+ if (typeof member.executionPrompt !== "string" || member.executionPrompt.trim() === "") {
580
+ diagnostics.push({ severity: "error", code: "compiled-member", reason: "成员缺少 executionPrompt", file: where });
581
+ }
582
+ }
583
+ }
584
+ const ok = diagnostics.every((item) => item.severity !== "error");
585
+ return { ok, compiled: ok ? value : null, diagnostics };
586
+ }
587
+
588
+ /**
589
+ * 把编译产物摊回一份 v1 档案文档。
590
+ *
591
+ * 远端订阅只拿得到产物:产物里的人设已经是最终拼装结果,所以导入本地时写成 v1 内联档案,
592
+ * 不假装自己带着角色目录。想要角色分层,导入后跑一次 `migrate` 重写即可。
593
+ *
594
+ * @param {object} compiled
595
+ * @returns {object}
596
+ */
597
+ export function flattenCompiledToRoster(compiled) {
598
+ const source = compiled.source ?? {};
599
+ const members = (compiled.members ?? []).map((member) => {
600
+ const flat = {
601
+ name: member.name,
602
+ role: member.role ?? "",
603
+ systemPrompt: member.executionPrompt,
604
+ };
605
+ if (member.title) flat.title = member.title;
606
+ if (member.description) flat.description = member.description;
607
+ if (member.provider) flat.provider = member.provider;
608
+ if (member.model) flat.model = member.model;
609
+ if (member.reasoningEffort) flat.reasoningEffort = member.reasoningEffort;
610
+ if (member.toolPolicy) flat.toolPolicy = member.toolPolicy;
611
+ return flat;
612
+ });
613
+ const roster = {
614
+ kind: ROSTER_KIND,
615
+ roster: source.roster,
616
+ version: source.version,
617
+ members,
618
+ };
619
+ if (source.title) roster.title = source.title;
620
+ if (source.description) roster.description = source.description;
621
+ if (compiled.captain) {
622
+ const captain = { name: String(compiled.captain.role ?? "").replace(/@[^@]*$/, "") };
623
+ if (compiled.captain.provider) captain.provider = compiled.captain.provider;
624
+ if (compiled.captain.model) captain.model = compiled.captain.model;
625
+ if (compiled.captain.reasoningEffort) captain.reasoningEffort = compiled.captain.reasoningEffort;
626
+ if (compiled.captain.persona) captain.personaPrefix = compiled.captain.persona;
627
+ roster.captain = captain;
628
+ }
629
+ return roster;
630
+ }
package/lib/format.js CHANGED
@@ -199,6 +199,7 @@ export const COLLECTION_FIELDS = [
199
199
  "ref",
200
200
  "path",
201
201
  "rosters",
202
+ "files",
202
203
  "tags",
203
204
  "license",
204
205
  "maintainers",
package/lib/remote.js CHANGED
@@ -23,6 +23,8 @@ import fs from "node:fs";
23
23
  import nodePath from "node:path";
24
24
  import { gunzipSync } from "node:zlib";
25
25
 
26
+ import { flattenCompiledToRoster, validateCompiled } from "./compile2.js";
27
+ import { COMPILED_KIND, ROSTER2_KIND } from "./format-v2.js";
26
28
  import {
27
29
  COLLECTION_FIELDS,
28
30
  LIMITS,
@@ -486,6 +488,25 @@ export function validateRegistry(value, meta = {}) {
486
488
  }
487
489
  if (bad) return;
488
490
 
491
+ const files = {};
492
+ if (entry.files !== undefined) {
493
+ if (entry.files === null || typeof entry.files !== "object" || Array.isArray(entry.files)) {
494
+ diagnostics.push(diagnostic("warn", "collection-files", `${where}.files 必须是「档案 id → 仓库内相对路径」的对象`, file));
495
+ } else {
496
+ for (const [key, value] of Object.entries(entry.files)) {
497
+ if (!rosters.includes(key)) {
498
+ diagnostics.push(diagnostic("warn", "collection-files-unknown", `${where}.files 里的 ${key} 不在 rosters 名单里,已忽略`, file));
499
+ continue;
500
+ }
501
+ if (typeof value !== "string" || value.trim() === "" || !PATTERNS.relPath.test(value) || value.split("/").includes("..")) {
502
+ diagnostics.push(diagnostic("warn", "collection-files-path", `${where}.files.${key} 必须是仓库内相对路径`, file));
503
+ continue;
504
+ }
505
+ files[key] = value.trim();
506
+ }
507
+ }
508
+ }
509
+
489
510
  seen.add(id);
490
511
  collections.push({
491
512
  id,
@@ -495,6 +516,7 @@ export function validateRegistry(value, meta = {}) {
495
516
  ref,
496
517
  path: path.replace(/^\/+|\/+$/g, ""),
497
518
  rosters,
519
+ files,
498
520
  tags: Array.isArray(entry.tags) ? entry.tags.filter((tag) => typeof tag === "string") : [],
499
521
  license: typeof entry.license === "string" ? entry.license : "",
500
522
  maintainers: Array.isArray(entry.maintainers) ? entry.maintainers.filter((item) => typeof item === "string") : [],
@@ -526,8 +548,10 @@ export async function fetchCollection(collection, options = {}) {
526
548
  for (const rosterId of wanted) {
527
549
  let loaded = null;
528
550
  let lastError = "";
529
- for (const fileName of ["roster.yaml", "roster.yml", "roster.json"]) {
530
- const url = collectionFileUrl(collection, rosterId, fileName);
551
+ const listedPath = collection.files?.[rosterId] ?? null;
552
+ const candidateNames = listedPath === null ? ["roster.yaml", "roster.yml", "roster.json"] : [null];
553
+ for (const fileName of candidateNames) {
554
+ const url = listedPath === null ? collectionFileUrl(collection, rosterId, fileName) : collectionFileUrl(collection, rosterId, "");
531
555
  try {
532
556
  const response = await fetchImpl(url, { signal: AbortSignal.timeout(TIMEOUT_MS) });
533
557
  if (!response.ok) {
@@ -545,15 +569,37 @@ export async function fetchCollection(collection, options = {}) {
545
569
  continue;
546
570
  }
547
571
  const parsed = parseDocumentText(loaded.text, { file: loaded.url });
548
- const checked = parsed.ok
549
- ? validateRoster(parsed.value, { file: loaded.url, expectedId: rosterId })
550
- : { ok: false, diagnostics: parsed.diagnostics };
572
+ if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === ROSTER2_KIND) {
573
+ diagnostics.push(
574
+ diagnostic(
575
+ "warn",
576
+ "collection-roster-needs-roles",
577
+ `${collection.id}/${rosterId} 是 v2 组装,成员是角色引用,单个文件拉不到角色目录;索引应指向 dist/ 里的编译产物`,
578
+ ),
579
+ );
580
+ continue;
581
+ }
582
+ let checked;
583
+ if (parsed.ok && parsed.value !== null && typeof parsed.value === "object" && parsed.value.kind === COMPILED_KIND) {
584
+ const compiled = validateCompiled(parsed.value, { file: loaded.url });
585
+ if (!compiled.ok) {
586
+ diagnostics.push(...compiled.diagnostics);
587
+ continue;
588
+ }
589
+ checked = validateRoster(flattenCompiledToRoster(compiled.compiled), { file: loaded.url, expectedId: rosterId });
590
+ checked = { ...checked, format: "compiled" };
591
+ } else {
592
+ checked = parsed.ok
593
+ ? validateRoster(parsed.value, { file: loaded.url, expectedId: rosterId })
594
+ : { ok: false, diagnostics: parsed.diagnostics };
595
+ }
551
596
  if (!checked.ok) {
552
597
  diagnostics.push(...checked.diagnostics);
553
598
  continue;
554
599
  }
555
600
  rosters.push({
556
601
  ...checked.roster,
602
+ ...(checked.format === undefined ? {} : { format: checked.format }),
557
603
  origin: {
558
604
  kind: "remote",
559
605
  collection: collection.id,
@@ -569,10 +615,12 @@ export async function fetchCollection(collection, options = {}) {
569
615
  return { collection, rosters, diagnostics };
570
616
  }
571
617
 
572
- /** 拼一份档案的 raw 地址。 */
618
+ /** 拼一份档案的 raw 地址(relativePath 由索引给出,或回退到 v1 的 `<id>/roster.<ext>`)。 */
573
619
  export function collectionFileUrl(collection, rosterId, fileName) {
574
620
  const repo = collection.repo.replace(/^github:/, "");
575
- return `${GITHUB_RAW}/${repo}/${encodeURIComponent(collection.ref)}/${collection.path}/${rosterId}/${fileName}`;
621
+ const relative = collection.files?.[rosterId] ?? `${rosterId}/${fileName}`;
622
+ const base = collection.path === "" ? "" : `${collection.path}/`;
623
+ return `${GITHUB_RAW}/${repo}/${encodeURIComponent(collection.ref)}/${base}${relative}`;
576
624
  }
577
625
 
578
626
  /** 一次读多个源(串行:GitHub 未认证配额经不起并发烧)。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stofone/dsh-agent-roster",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "多 Agent 团队的静态成员档案定义与校验层:档案库在面板里看得见、改得动,团队成员的人设、模型与工具边界固化成可校验、可版本化的文件,并在 dsh-agent-teams 创建成员时把档案桥接给运行时。",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -5,32 +5,57 @@
5
5
  "description": "公共档案索引:登记若干 collection(仓库 + 目录 + 确有哪几份档案),供订阅与一键安装。权威校验实现是 lib/remote.js#validateRegistry;本文件供编辑器与外部工具提示,两者由 test/registry-schema.test.js 对齐。注意:运行时对未知字段只告警不判错,这里写成 additionalProperties:false 是为了让编辑器早点指出拼写错误。",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
- "required": ["kind", "collections"],
8
+ "required": [
9
+ "kind",
10
+ "collections"
11
+ ],
9
12
  "properties": {
10
13
  "$schema": {
11
14
  "type": "string",
12
15
  "description": "编辑器用的 schema 地址,插件不读它。"
13
16
  },
14
- "kind": { "const": "dsh-agent-roster-registry/v1" },
15
- "version": { "type": "integer", "minimum": 1 },
17
+ "kind": {
18
+ "const": "dsh-agent-roster-registry/v1"
19
+ },
20
+ "version": {
21
+ "type": "integer",
22
+ "minimum": 1
23
+ },
16
24
  "generatedAt": {
17
25
  "type": "string",
18
26
  "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z$",
19
27
  "description": "上一次校验通过的时间,由 CI 或维护者盖章。"
20
28
  },
21
- "generator": { "type": "string", "maxLength": 200 },
22
- "source": { "type": "string", "maxLength": 200 },
29
+ "generator": {
30
+ "type": "string",
31
+ "maxLength": 200
32
+ },
33
+ "source": {
34
+ "type": "string",
35
+ "maxLength": 200
36
+ },
23
37
  "collections": {
24
38
  "type": "array",
25
39
  "maxItems": 500,
26
40
  "items": {
27
41
  "type": "object",
28
42
  "additionalProperties": false,
29
- "required": ["id", "repo"],
43
+ "required": [
44
+ "id",
45
+ "repo"
46
+ ],
30
47
  "properties": {
31
- "id": { "$ref": "#/$defs/id" },
32
- "title": { "type": "string", "maxLength": 120 },
33
- "description": { "type": "string", "maxLength": 500 },
48
+ "id": {
49
+ "$ref": "#/$defs/id"
50
+ },
51
+ "title": {
52
+ "type": "string",
53
+ "maxLength": 120
54
+ },
55
+ "description": {
56
+ "type": "string",
57
+ "maxLength": 500
58
+ },
34
59
  "repo": {
35
60
  "type": "string",
36
61
  "pattern": "^(?:github:)?[A-Za-z0-9][A-Za-z0-9_.-]{0,38}/[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$",
@@ -52,7 +77,9 @@
52
77
  "type": "array",
53
78
  "maxItems": 200,
54
79
  "description": "目录里确实存在的档案 id。索引不撒谎:不存在的 id 会被 registry sync 剔除。",
55
- "items": { "$ref": "#/$defs/id" }
80
+ "items": {
81
+ "$ref": "#/$defs/id"
82
+ }
56
83
  },
57
84
  "tags": {
58
85
  "type": "array",
@@ -62,13 +89,34 @@
62
89
  "pattern": "^[a-z0-9][a-z0-9-]{0,31}$"
63
90
  }
64
91
  },
65
- "license": { "type": "string", "maxLength": 120 },
92
+ "license": {
93
+ "type": "string",
94
+ "maxLength": 120
95
+ },
66
96
  "maintainers": {
67
97
  "type": "array",
68
98
  "maxItems": 16,
69
- "items": { "type": "string", "minLength": 1, "maxLength": 120 }
99
+ "items": {
100
+ "type": "string",
101
+ "minLength": 1,
102
+ "maxLength": 120
103
+ }
70
104
  },
71
- "homepage": { "type": "string", "maxLength": 200, "pattern": "^https?://\\S+$" }
105
+ "homepage": {
106
+ "type": "string",
107
+ "maxLength": 200,
108
+ "pattern": "^https?://\\S+$"
109
+ },
110
+ "files": {
111
+ "type": "object",
112
+ "description": "档案 id → 仓库内相对路径。v2 档案指向 dist/ 的编译产物(自包含),v1 可省略。",
113
+ "propertyNames": {
114
+ "$ref": "#/$defs/id"
115
+ },
116
+ "additionalProperties": {
117
+ "$ref": "#/$defs/relPath"
118
+ }
119
+ }
72
120
  }
73
121
  }
74
122
  }
@@ -78,6 +126,13 @@
78
126
  "type": "string",
79
127
  "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
80
128
  "description": "kebab-case 标识,与档案目录名同规则。"
129
+ },
130
+ "relPath": {
131
+ "type": "string",
132
+ "minLength": 1,
133
+ "maxLength": 200,
134
+ "pattern": "^(?:[A-Za-z0-9._-]+/)*[A-Za-z0-9._-]+$",
135
+ "description": "仓库内相对路径,不含 ..、不以 / 开头。"
81
136
  }
82
137
  }
83
138
  }