@stofone/dsh-agent-roster 0.9.2 → 0.10.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.
Files changed (167) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +78 -4
  3. package/README.zh.md +44 -1
  4. package/bin/cli.mjs +228 -2
  5. package/docs/roster-v2.md +220 -0
  6. package/examples/dist/core-review-1.0.0.json +160 -0
  7. package/examples/dist/research-0.1.0.json +95 -0
  8. package/examples/roles/core-review-correctness/AGENT.md +20 -0
  9. package/examples/roles/core-review-lead/AGENT.md +23 -0
  10. package/examples/roles/core-review-lead-captain/AGENT.md +13 -0
  11. package/examples/roles/core-review-maintainer/AGENT.md +20 -0
  12. package/examples/roles/research-scout/AGENT.md +16 -0
  13. package/examples/roles/research-verifier/AGENT.md +16 -0
  14. package/examples/rosters/core-review/roster.yaml +39 -0
  15. package/examples/rosters/research/roster.yaml +20 -0
  16. package/examples-orchestration/dist/core-review-1.0.0.json +185 -0
  17. package/examples-orchestration/dist/core-review-1.0.0.tasks.json +76 -0
  18. package/examples-orchestration/roles/fixer/AGENT.md +18 -0
  19. package/examples-orchestration/roles/lead/AGENT.md +18 -0
  20. package/examples-orchestration/roles/reviewer/AGENT.md +23 -0
  21. package/examples-orchestration/roles/reviewer/parts/checklist.md +6 -0
  22. package/examples-orchestration/rosters/core-review/overrides/fixer.md +1 -0
  23. package/examples-orchestration/rosters/core-review/roster.yaml +43 -0
  24. package/index.js +8 -1
  25. package/lib/compile2.js +522 -0
  26. package/lib/format-v2.js +196 -0
  27. package/lib/frontmatter.js +113 -0
  28. package/lib/migrate.js +326 -0
  29. package/lib/plan.js +167 -0
  30. package/lib/registry.js +65 -11
  31. package/lib/role.js +498 -0
  32. package/lib/roster2.js +591 -0
  33. package/lib/run.js +188 -0
  34. package/lib/write.js +14 -8
  35. package/orgs/agent-roster-collective/dist/api-design-1.0.0.json +130 -0
  36. package/orgs/agent-roster-collective/dist/content-ops-1.0.0.json +130 -0
  37. package/orgs/agent-roster-collective/dist/contract-review-1.0.0.json +130 -0
  38. package/orgs/agent-roster-collective/dist/data-quality-1.0.0.json +130 -0
  39. package/orgs/agent-roster-collective/dist/db-migration-1.0.0.json +130 -0
  40. package/orgs/agent-roster-collective/dist/docs-writer-1.0.0.json +130 -0
  41. package/orgs/agent-roster-collective/dist/finance-recon-1.0.0.json +130 -0
  42. package/orgs/agent-roster-collective/dist/frontend-ui-1.0.0.json +130 -0
  43. package/orgs/agent-roster-collective/dist/i18n-l10n-1.0.0.json +130 -0
  44. package/orgs/agent-roster-collective/dist/incident-response-1.0.0.json +130 -0
  45. package/orgs/agent-roster-collective/dist/literature-review-1.0.0.json +130 -0
  46. package/orgs/agent-roster-collective/dist/market-research-1.0.0.json +130 -0
  47. package/orgs/agent-roster-collective/dist/ml-eval-1.0.0.json +130 -0
  48. package/orgs/agent-roster-collective/dist/performance-1.0.0.json +130 -0
  49. package/orgs/agent-roster-collective/dist/product-discovery-1.0.0.json +130 -0
  50. package/orgs/agent-roster-collective/dist/qa-regression-1.0.0.json +130 -0
  51. package/orgs/agent-roster-collective/dist/recruiting-1.0.0.json +130 -0
  52. package/orgs/agent-roster-collective/dist/release-manager-1.0.0.json +130 -0
  53. package/orgs/agent-roster-collective/dist/security-audit-1.0.0.json +132 -0
  54. package/orgs/agent-roster-collective/dist/sre-oncall-1.0.0.json +130 -0
  55. package/orgs/agent-roster-collective/dist/support-triage-1.0.0.json +130 -0
  56. package/orgs/agent-roster-collective/roles/api-design-failure-mode/AGENT.md +19 -0
  57. package/orgs/agent-roster-collective/roles/api-design-lead/AGENT.md +19 -0
  58. package/orgs/agent-roster-collective/roles/api-design-schema/AGENT.md +20 -0
  59. package/orgs/agent-roster-collective/roles/content-ops-distributor/AGENT.md +19 -0
  60. package/orgs/agent-roster-collective/roles/content-ops-editor/AGENT.md +19 -0
  61. package/orgs/agent-roster-collective/roles/content-ops-lead/AGENT.md +19 -0
  62. package/orgs/agent-roster-collective/roles/contract-review-clause-reader/AGENT.md +19 -0
  63. package/orgs/agent-roster-collective/roles/contract-review-lead/AGENT.md +19 -0
  64. package/orgs/agent-roster-collective/roles/contract-review-risk-note/AGENT.md +19 -0
  65. package/orgs/agent-roster-collective/roles/data-quality-lead/AGENT.md +19 -0
  66. package/orgs/agent-roster-collective/roles/data-quality-profiler/AGENT.md +19 -0
  67. package/orgs/agent-roster-collective/roles/data-quality-rule-author/AGENT.md +19 -0
  68. package/orgs/agent-roster-collective/roles/db-migration-lead/AGENT.md +19 -0
  69. package/orgs/agent-roster-collective/roles/db-migration-migrator/AGENT.md +19 -0
  70. package/orgs/agent-roster-collective/roles/db-migration-verifier/AGENT.md +19 -0
  71. package/orgs/agent-roster-collective/roles/docs-writer-example-runner/AGENT.md +19 -0
  72. package/orgs/agent-roster-collective/roles/docs-writer-lead/AGENT.md +19 -0
  73. package/orgs/agent-roster-collective/roles/docs-writer-writer/AGENT.md +19 -0
  74. package/orgs/agent-roster-collective/roles/finance-recon-exception/AGENT.md +19 -0
  75. package/orgs/agent-roster-collective/roles/finance-recon-lead/AGENT.md +19 -0
  76. package/orgs/agent-roster-collective/roles/finance-recon-matcher/AGENT.md +19 -0
  77. package/orgs/agent-roster-collective/roles/frontend-ui-a11y-reviewer/AGENT.md +19 -0
  78. package/orgs/agent-roster-collective/roles/frontend-ui-implementer/AGENT.md +19 -0
  79. package/orgs/agent-roster-collective/roles/frontend-ui-lead/AGENT.md +19 -0
  80. package/orgs/agent-roster-collective/roles/i18n-l10n-lead/AGENT.md +19 -0
  81. package/orgs/agent-roster-collective/roles/i18n-l10n-reviewer/AGENT.md +19 -0
  82. package/orgs/agent-roster-collective/roles/i18n-l10n-translator/AGENT.md +19 -0
  83. package/orgs/agent-roster-collective/roles/incident-response-commander/AGENT.md +19 -0
  84. package/orgs/agent-roster-collective/roles/incident-response-scribe/AGENT.md +19 -0
  85. package/orgs/agent-roster-collective/roles/incident-response-triage/AGENT.md +19 -0
  86. package/orgs/agent-roster-collective/roles/literature-review-critic/AGENT.md +19 -0
  87. package/orgs/agent-roster-collective/roles/literature-review-extractor/AGENT.md +19 -0
  88. package/orgs/agent-roster-collective/roles/literature-review-lead/AGENT.md +19 -0
  89. package/orgs/agent-roster-collective/roles/market-research-lead/AGENT.md +19 -0
  90. package/orgs/agent-roster-collective/roles/market-research-researcher/AGENT.md +19 -0
  91. package/orgs/agent-roster-collective/roles/market-research-synthesizer/AGENT.md +19 -0
  92. package/orgs/agent-roster-collective/roles/ml-eval-dataset/AGENT.md +19 -0
  93. package/orgs/agent-roster-collective/roles/ml-eval-lead/AGENT.md +19 -0
  94. package/orgs/agent-roster-collective/roles/ml-eval-metric/AGENT.md +19 -0
  95. package/orgs/agent-roster-collective/roles/performance-lead/AGENT.md +19 -0
  96. package/orgs/agent-roster-collective/roles/performance-load-tester/AGENT.md +19 -0
  97. package/orgs/agent-roster-collective/roles/performance-profiler/AGENT.md +19 -0
  98. package/orgs/agent-roster-collective/roles/product-discovery-interviewer/AGENT.md +19 -0
  99. package/orgs/agent-roster-collective/roles/product-discovery-lead/AGENT.md +19 -0
  100. package/orgs/agent-roster-collective/roles/product-discovery-skeptic/AGENT.md +19 -0
  101. package/orgs/agent-roster-collective/roles/qa-regression-case-writer/AGENT.md +19 -0
  102. package/orgs/agent-roster-collective/roles/qa-regression-flake-hunter/AGENT.md +19 -0
  103. package/orgs/agent-roster-collective/roles/qa-regression-lead/AGENT.md +19 -0
  104. package/orgs/agent-roster-collective/roles/recruiting-lead/AGENT.md +19 -0
  105. package/orgs/agent-roster-collective/roles/recruiting-scheduler/AGENT.md +19 -0
  106. package/orgs/agent-roster-collective/roles/recruiting-screener/AGENT.md +19 -0
  107. package/orgs/agent-roster-collective/roles/release-manager-changelog/AGENT.md +19 -0
  108. package/orgs/agent-roster-collective/roles/release-manager-lead/AGENT.md +19 -0
  109. package/orgs/agent-roster-collective/roles/release-manager-rollback/AGENT.md +19 -0
  110. package/orgs/agent-roster-collective/roles/security-audit-code-auditor/AGENT.md +19 -0
  111. package/orgs/agent-roster-collective/roles/security-audit-lead/AGENT.md +19 -0
  112. package/orgs/agent-roster-collective/roles/security-audit-threat-modeler/AGENT.md +19 -0
  113. package/orgs/agent-roster-collective/roles/sre-oncall-capacity/AGENT.md +19 -0
  114. package/orgs/agent-roster-collective/roles/sre-oncall-lead/AGENT.md +19 -0
  115. package/orgs/agent-roster-collective/roles/sre-oncall-runbook/AGENT.md +19 -0
  116. package/orgs/agent-roster-collective/roles/support-triage-classifier/AGENT.md +19 -0
  117. package/orgs/agent-roster-collective/roles/support-triage-lead/AGENT.md +19 -0
  118. package/orgs/agent-roster-collective/roles/support-triage-responder/AGENT.md +19 -0
  119. package/orgs/agent-roster-collective/rosters/api-design/roster.yaml +26 -0
  120. package/orgs/agent-roster-collective/rosters/content-ops/roster.yaml +25 -0
  121. package/orgs/agent-roster-collective/rosters/contract-review/roster.yaml +25 -0
  122. package/orgs/agent-roster-collective/rosters/data-quality/roster.yaml +26 -0
  123. package/orgs/agent-roster-collective/rosters/db-migration/roster.yaml +26 -0
  124. package/orgs/agent-roster-collective/rosters/docs-writer/roster.yaml +25 -0
  125. package/orgs/agent-roster-collective/rosters/finance-recon/roster.yaml +25 -0
  126. package/orgs/agent-roster-collective/rosters/frontend-ui/roster.yaml +26 -0
  127. package/orgs/agent-roster-collective/rosters/i18n-l10n/roster.yaml +26 -0
  128. package/orgs/agent-roster-collective/rosters/incident-response/roster.yaml +26 -0
  129. package/orgs/agent-roster-collective/rosters/literature-review/roster.yaml +25 -0
  130. package/orgs/agent-roster-collective/rosters/market-research/roster.yaml +25 -0
  131. package/orgs/agent-roster-collective/rosters/ml-eval/roster.yaml +25 -0
  132. package/orgs/agent-roster-collective/rosters/performance/roster.yaml +25 -0
  133. package/orgs/agent-roster-collective/rosters/product-discovery/roster.yaml +25 -0
  134. package/orgs/agent-roster-collective/rosters/qa-regression/roster.yaml +26 -0
  135. package/orgs/agent-roster-collective/rosters/recruiting/roster.yaml +25 -0
  136. package/orgs/agent-roster-collective/rosters/release-manager/roster.yaml +25 -0
  137. package/orgs/agent-roster-collective/rosters/security-audit/roster.yaml +30 -0
  138. package/orgs/agent-roster-collective/rosters/sre-oncall/roster.yaml +26 -0
  139. package/orgs/agent-roster-collective/rosters/support-triage/roster.yaml +25 -0
  140. package/package.json +3 -1
  141. package/schema/compiled.v1.schema.json +391 -0
  142. package/schema/role.v1.schema.json +240 -0
  143. package/schema/roster.v2.schema.json +437 -0
  144. package/examples/core-review/roster.yaml +0 -82
  145. package/examples/research/roster.yaml +0 -42
  146. package/orgs/agent-roster-collective/api-design/roster.yaml +0 -67
  147. package/orgs/agent-roster-collective/content-ops/roster.yaml +0 -55
  148. package/orgs/agent-roster-collective/contract-review/roster.yaml +0 -58
  149. package/orgs/agent-roster-collective/data-quality/roster.yaml +0 -64
  150. package/orgs/agent-roster-collective/db-migration/roster.yaml +0 -64
  151. package/orgs/agent-roster-collective/docs-writer/roster.yaml +0 -59
  152. package/orgs/agent-roster-collective/finance-recon/roster.yaml +0 -63
  153. package/orgs/agent-roster-collective/frontend-ui/roster.yaml +0 -60
  154. package/orgs/agent-roster-collective/i18n-l10n/roster.yaml +0 -60
  155. package/orgs/agent-roster-collective/incident-response/roster.yaml +0 -65
  156. package/orgs/agent-roster-collective/literature-review/roster.yaml +0 -61
  157. package/orgs/agent-roster-collective/market-research/roster.yaml +0 -60
  158. package/orgs/agent-roster-collective/ml-eval/roster.yaml +0 -63
  159. package/orgs/agent-roster-collective/performance/roster.yaml +0 -63
  160. package/orgs/agent-roster-collective/product-discovery/roster.yaml +0 -55
  161. package/orgs/agent-roster-collective/qa-regression/roster.yaml +0 -64
  162. package/orgs/agent-roster-collective/recruiting/roster.yaml +0 -58
  163. package/orgs/agent-roster-collective/release-manager/roster.yaml +0 -59
  164. package/orgs/agent-roster-collective/security-audit/roster.yaml +0 -65
  165. package/orgs/agent-roster-collective/sre-oncall/roster.yaml +0 -60
  166. package/orgs/agent-roster-collective/support-triage/roster.yaml +0 -58
  167. /package/orgs/agent-roster-collective/{security-audit → rosters/security-audit}/capabilities.yaml +0 -0
@@ -0,0 +1,522 @@
1
+ /**
2
+ * 编译层:把「角色 + 组装」解析成唯一的产物(compiled@1)。
3
+ *
4
+ * 为什么必须有编译产物:运行时只收一条字符串。只要拼装逻辑留在插件代码里,
5
+ * 插件升级就能改变所有成员的行为,而档案 diff 一行没动。把拼装写进格式、把结果物化,
6
+ * 「模型到底会看到什么」才成为可 diff、可哈希、可复核的东西。
7
+ *
8
+ * 编译只读文件、不写文件、不建团队;产物由调用方决定写到哪(`dist/`、面板、工具输出)。
9
+ *
10
+ * @module dsh-agent-roster/compile2
11
+ */
12
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
13
+ import nodePath from "node:path";
14
+
15
+ import { assemblePrompt, charLength, splitFrontmatter } from "./frontmatter.js";
16
+ import { sha256Hex, shortSha } from "./digest.js";
17
+ import {
18
+ COMPILED_KIND,
19
+ LIMITS2,
20
+ ROLE_FILE_NAME,
21
+ ROLE_PARTS_DIR,
22
+ ROSTER2_FILE_NAMES,
23
+ } from "./format-v2.js";
24
+ import { diagnostic, parseDocumentText } from "./parse.js";
25
+ import { loadRoleAt, safeResolve } from "./role.js";
26
+ import { validateRoster2 } from "./roster2.js";
27
+
28
+ /** 递归扫描角色库,返回 `name → 角色` 映射;重名与版本重复都报出来。 */
29
+ export function scanRoles(rootDir, options = {}) {
30
+ const root = nodePath.resolve(String(rootDir));
31
+ const diagnostics = [];
32
+ const roles = new Map();
33
+ const files = [];
34
+
35
+ const walk = (dir, depth) => {
36
+ if (depth > 3) return;
37
+ let entries;
38
+ try {
39
+ entries = readdirSync(dir, { withFileTypes: true });
40
+ } catch (error) {
41
+ diagnostics.push(diagnostic("error", "unreadable-role-dir", "", `读不到目录 ${dir}:${error?.message ?? error}`, dir));
42
+ return;
43
+ }
44
+ const sorted = entries.slice().sort((a, b) => a.name.localeCompare(b.name));
45
+ if (sorted.some((entry) => entry.isFile() && entry.name === ROLE_FILE_NAME)) files.push(dir);
46
+ for (const entry of sorted) {
47
+ if (!entry.isDirectory()) continue;
48
+ if (entry.name.startsWith(".") || entry.name === ROLE_PARTS_DIR || entry.name === "node_modules") continue;
49
+ walk(nodePath.join(dir, entry.name), depth + 1);
50
+ }
51
+ };
52
+
53
+ if (!existsSync(root)) {
54
+ return { ok: false, roles, diagnostics: [diagnostic("error", "missing-roles-dir", "", `角色库目录不存在:${root}`, root)] };
55
+ }
56
+ walk(root, 0);
57
+
58
+ if (files.length > LIMITS2.roleFiles) {
59
+ diagnostics.push(diagnostic("error", "too-many-roles", "", `角色文件 ${files.length} 份,超过上限 ${LIMITS2.roleFiles}`, root));
60
+ }
61
+
62
+ for (const dir of files) {
63
+ const loaded = loadRoleAt(dir, { knownToolPolicies: options.knownToolPolicies ?? null });
64
+ diagnostics.push(...loaded.diagnostics);
65
+ if (!loaded.ok || loaded.role === null) continue;
66
+ const role = loaded.role;
67
+ const previous = roles.get(role.name);
68
+ if (previous !== undefined) {
69
+ // 重名就是重名:不靠"谁先扫到谁赢",直接报错(v1 用「目录名必须等于 id」避免了这一整类问题,
70
+ // v2 把身份放进文档,就必须换一条规则来保证唯一性)。
71
+ diagnostics.push(
72
+ diagnostic(
73
+ "error",
74
+ "duplicate-role",
75
+ role.name,
76
+ `角色名 ${role.name} 出现在两个地方:${nodePath.relative(root, previous.dir)} 与 ${nodePath.relative(root, dir)}`,
77
+ loaded.file,
78
+ ),
79
+ );
80
+ continue;
81
+ }
82
+ roles.set(role.name, {
83
+ dir,
84
+ file: loaded.file,
85
+ role,
86
+ prompt: loaded.prompt,
87
+ body: loaded.body,
88
+ sha256: sha256Hex(readFileSync(loaded.file, "utf8")),
89
+ });
90
+ }
91
+
92
+ return { ok: !diagnostics.some((item) => item.severity === "error"), roles, diagnostics };
93
+ }
94
+
95
+ /** 找一份组装文档(`rosters/<id>/roster.{yaml,yml,json}`)。 */
96
+ export function locateRoster(rootDir, id) {
97
+ const dir = nodePath.resolve(String(rootDir), String(id));
98
+ for (const name of ROSTER2_FILE_NAMES) {
99
+ const file = nodePath.join(dir, name);
100
+ if (existsSync(file) && statSync(file).isFile()) return { dir, file };
101
+ }
102
+ return { dir, file: null };
103
+ }
104
+
105
+ /**
106
+ * 编译一份组装文档。
107
+ *
108
+ * @param {{
109
+ * rosterDir: string,
110
+ * rosterFile?: string | null,
111
+ * roles: Map<string, object>,
112
+ * expectedId?: string | null,
113
+ * knownToolPolicies?: string[] | null,
114
+ * source?: { repo?: string, ref?: string, lock?: string | null },
115
+ * }} options
116
+ * @returns {{ ok: boolean, compiled: object | null, roster: object | null, diagnostics: object[] }}
117
+ */
118
+ export function compileRoster(options) {
119
+ const rosterDir = nodePath.resolve(String(options.rosterDir));
120
+ const diagnostics = [];
121
+ const file = options.rosterFile ?? findRosterFile(rosterDir);
122
+ if (file === null) {
123
+ return {
124
+ ok: false,
125
+ compiled: null,
126
+ roster: null,
127
+ diagnostics: [diagnostic("error", "missing-roster-file", "", `目录里没有 ${ROSTER2_FILE_NAMES.join(" / ")}`, rosterDir)],
128
+ };
129
+ }
130
+
131
+ let text;
132
+ try {
133
+ text = readFileSync(file, "utf8");
134
+ } catch (error) {
135
+ return { ok: false, compiled: null, roster: null, diagnostics: [diagnostic("error", "unreadable-roster-file", "", String(error?.message ?? error), file)] };
136
+ }
137
+
138
+ const parsed = parseDocumentText(text, { file, format: file.endsWith(".json") ? "json" : "yaml" });
139
+ diagnostics.push(...parsed.diagnostics);
140
+ if (!parsed.ok) return { ok: false, compiled: null, roster: null, diagnostics };
141
+
142
+ const validated = validateRoster2(parsed.value, {
143
+ file,
144
+ expectedId: options.expectedId ?? nodePath.basename(rosterDir),
145
+ knownToolPolicies: options.knownToolPolicies ?? null,
146
+ });
147
+ diagnostics.push(...validated.diagnostics);
148
+ if (!validated.ok || validated.roster === null) return { ok: false, compiled: null, roster: null, diagnostics };
149
+
150
+ const roster = validated.roster;
151
+ const roles = options.roles;
152
+ const declarations = [];
153
+ const members = [];
154
+ const memberRequires = [];
155
+
156
+ for (const member of roster.members) {
157
+ const role = roles.get(member.role.name);
158
+ if (role === undefined) {
159
+ diagnostics.push(
160
+ diagnostic("error", "unknown-role", `members.${member.as}`, `角色库里没有 ${member.role.name};先把它放进 roles/${member.role.name}/AGENT.md`),
161
+ );
162
+ continue;
163
+ }
164
+ if (role.role.version !== member.role.version) {
165
+ diagnostics.push(
166
+ diagnostic(
167
+ "error",
168
+ "role-version-mismatch",
169
+ `members.${member.as}`,
170
+ `引用 ${member.role.raw},但角色库里是 ${role.role.name}@${role.role.version}`,
171
+ ),
172
+ );
173
+ continue;
174
+ }
175
+
176
+ const extra = readMemberExtras(rosterDir, member, file, diagnostics);
177
+ if (extra === null) continue;
178
+ const assembled = assemblePrompt("", [role.prompt.text], extra);
179
+ const chars = charLength(assembled.text);
180
+ if (chars > LIMITS2.promptChars) {
181
+ diagnostics.push(
182
+ diagnostic("error", "prompt-too-long", `members.${member.as}`, `拼装后 ${chars} 字符,超过上限 ${LIMITS2.promptChars}`, file),
183
+ );
184
+ continue;
185
+ }
186
+
187
+ const route = { ...role.role.route, ...member.route };
188
+ const policy = member.tools?.policy ?? role.role.tools?.policy ?? null;
189
+
190
+ members.push({
191
+ name: member.as,
192
+ role: role.role.role,
193
+ roleRef: `${role.role.name}@${role.role.version}`,
194
+ roleName: role.role.name,
195
+ roleVersion: role.role.version,
196
+ provider: route.provider ?? null,
197
+ model: route.model ?? null,
198
+ reasoningEffort: route.reasoningEffort ?? null,
199
+ toolPolicy: policy,
200
+ executionPrompt: assembled.text,
201
+ promptStats: {
202
+ chars,
203
+ parts: [
204
+ ...(extra.prepend !== null ? [{ path: extra.prepend, kind: "prepend" }] : []),
205
+ { path: `roles/${nodePath.basename(role.dir)}/${ROLE_FILE_NAME}`, kind: "body+parts" },
206
+ ...(role.prompt.parts ?? []).map((part) => ({ path: part.path, kind: "part" })),
207
+ ...(extra.append !== null ? [{ path: extra.append, kind: "append" }] : []),
208
+ ],
209
+ sha256: sha256Hex(assembled.text),
210
+ },
211
+ provenance: { role: `${role.role.name}@${role.role.version}`, sha256: shortSha(role.sha256) },
212
+ });
213
+
214
+ collectDeclarations(declarations, member, role, roster, extra);
215
+ memberRequires.push({
216
+ skills: [...role.role.requires.skills, ...member.requires.skills],
217
+ toolPolicies: [...role.role.requires.toolPolicies, ...member.requires.toolPolicies],
218
+ });
219
+ }
220
+
221
+ const captain = compileCaptain(roster, rosterDir, roles, file, diagnostics, declarations);
222
+ const orchestration = roster.orchestration === null ? null : compileOrchestration(roster.orchestration, declarations);
223
+
224
+ if (diagnostics.some((item) => item.severity === "error")) {
225
+ return { ok: false, compiled: null, roster, diagnostics };
226
+ }
227
+
228
+ const compiled = {
229
+ kind: COMPILED_KIND,
230
+ source: {
231
+ roster: roster.id,
232
+ version: roster.version,
233
+ title: roster.title ?? null,
234
+ repo: options.source?.repo ?? null,
235
+ ref: options.source?.ref ?? null,
236
+ lock: options.source?.lock ?? null,
237
+ file: nodePath.basename(file),
238
+ },
239
+ targets: roster.targets,
240
+ captain,
241
+ members,
242
+ orchestration,
243
+ declarations,
244
+ requires: mergeRequires(roster.requires, memberRequires),
245
+ };
246
+
247
+ if (roster.captain !== null && roster.captain !== undefined && !roster.targets.includes("profile/agent-preset")) {
248
+ diagnostics.push(
249
+ diagnostic(
250
+ "warn",
251
+ "captain-not-targeted",
252
+ "targets",
253
+ "声明了 captain,但 targets 里没有 profile/agent-preset:队长只会作为说明出现,不会被落成配置",
254
+ file,
255
+ ),
256
+ );
257
+ }
258
+
259
+ return { ok: true, compiled, roster, diagnostics };
260
+ }
261
+
262
+ function compileCaptain(roster, rosterDir, roles, file, diagnostics, declarations) {
263
+ if (roster.captain === null || roster.captain === undefined) {
264
+ return null;
265
+ }
266
+ const captain = roster.captain;
267
+ let persona = null;
268
+ let personaPath = null;
269
+ if (captain.persona !== null) {
270
+ const resolved = safeResolve(rosterDir, captain.persona);
271
+ if (resolved === null || !existsSync(resolved)) {
272
+ diagnostics.push(diagnostic("error", "missing-persona", "captain.persona", `队长 persona 文件不存在:${captain.persona}`, file));
273
+ return null;
274
+ }
275
+ persona = readFileSync(resolved, "utf8").replace(/\s+$/, "");
276
+ personaPath = captain.persona;
277
+ }
278
+
279
+ let roleName = null;
280
+ let roleVersion = null;
281
+ let route = { ...captain.route };
282
+ if (captain.role !== null) {
283
+ const role = roles.get(captain.role.name);
284
+ if (role === undefined) {
285
+ diagnostics.push(diagnostic("error", "unknown-role", "captain.role", `角色库里没有 ${captain.role.name}`, file));
286
+ return null;
287
+ }
288
+ if (role.role.version !== captain.role.version) {
289
+ diagnostics.push(
290
+ diagnostic("error", "role-version-mismatch", "captain.role", `引用 ${captain.role.raw},角色库是 ${role.role.name}@${role.role.version}`, file),
291
+ );
292
+ return null;
293
+ }
294
+ roleName = role.role.name;
295
+ roleVersion = role.role.version;
296
+ route = { ...role.role.route, ...captain.route };
297
+ if (persona === null) {
298
+ persona = role.prompt.text;
299
+ personaPath = null;
300
+ }
301
+ }
302
+
303
+ declarations.push({
304
+ scope: "captain",
305
+ field: "route",
306
+ note: "config:队长的路由只能由 profile 的 agent-default-model 落地,插件不写运行时",
307
+ });
308
+ if (persona !== null) {
309
+ declarations.push({ scope: "captain", field: "persona", note: "config:需要落到 profile preset 的 persona" });
310
+ }
311
+
312
+ return {
313
+ role: roleName === null ? null : `${roleName}@${roleVersion}`,
314
+ provider: route.provider ?? null,
315
+ model: route.model ?? null,
316
+ reasoningEffort: route.reasoningEffort ?? null,
317
+ persona,
318
+ personaPath,
319
+ personaChars: persona === null ? 0 : charLength(persona),
320
+ personaSha256: persona === null ? null : sha256Hex(persona),
321
+ };
322
+ }
323
+
324
+ function compileOrchestration(orchestration, declarations) {
325
+ declarations.push({
326
+ scope: "orchestration",
327
+ field: "stages/gates",
328
+ note: "declaration:运行时不会执行这份编排;它先作为可校验的计划存在,编译成任务 DAG 是下一步",
329
+ });
330
+ return {
331
+ shape: orchestration.shape,
332
+ stages: orchestration.stages.map((stage) => ({
333
+ id: stage.id,
334
+ parallel: stage.parallel,
335
+ serial: stage.serial,
336
+ join: stage.join,
337
+ on: stage.on,
338
+ role: stage.role === null ? null : `${stage.role.name}@${stage.role.version}`,
339
+ maxRounds: stage.maxRounds,
340
+ })),
341
+ gates: orchestration.gates.map((gate) => ({
342
+ id: gate.id,
343
+ after: gate.after.slice(),
344
+ requires: gate.requires.slice(),
345
+ verify: gate.verify.map((item) => ({ command: item.command, expectExit: item.expectExit })),
346
+ by: gate.by,
347
+ })),
348
+ status: "declaration",
349
+ };
350
+ }
351
+
352
+ function collectDeclarations(declarations, member, role, roster, extra) {
353
+ if (role.role.tools?.scope !== undefined) {
354
+ declarations.push({ scope: "member", member: member.as, field: "tools.scope", note: "declaration:运行时不生效,只作说明" });
355
+ }
356
+ if (role.role.skills.length > 0) {
357
+ declarations.push({ scope: "member", member: member.as, field: "skills", note: "declaration:插件只核对技能是否安装" });
358
+ }
359
+ if (role.role.examples !== null) {
360
+ declarations.push({ scope: "member", member: member.as, field: "examples", note: "declaration:示例永不进 prompt" });
361
+ }
362
+ if (role.role.requires.skills.length > 0 || role.role.requires.toolPolicies.length > 0) {
363
+ declarations.push({ scope: "member", member: member.as, field: "requires", note: "deployment:角色级要求,编译时并入 requires" });
364
+ }
365
+ if (roster.defaults?.route && Object.keys(roster.defaults.route).length > 0) {
366
+ declarations.push({ scope: "roster", field: "defaults.route", note: "materialized:已展开成每个成员的显式值" });
367
+ }
368
+ if (extra.append !== null || extra.prepend !== null) {
369
+ declarations.push({ scope: "member", member: member.as, field: "prompt.append/prepend", note: "materialized:已拼进 executionPrompt" });
370
+ }
371
+ }
372
+
373
+ function mergeRequires(rosterRequires, memberRequires) {
374
+ const skills = new Set(rosterRequires.skills);
375
+ const toolPolicies = new Set(rosterRequires.toolPolicies);
376
+ for (const entry of memberRequires) {
377
+ for (const skill of entry.skills ?? []) skills.add(skill);
378
+ for (const policy of entry.toolPolicies ?? []) toolPolicies.add(policy);
379
+ }
380
+ return { skills: [...skills].sort(), toolPolicies: [...toolPolicies].sort() };
381
+ }
382
+
383
+ function readMemberExtras(rosterDir, member, file, diagnostics) {
384
+ const extra = { prepend: null, append: null };
385
+ for (const key of ["prepend", "append"]) {
386
+ const relative = member.prompt?.[key] ?? null;
387
+ if (relative === null) continue;
388
+ const resolved = safeResolve(rosterDir, relative);
389
+ if (resolved === null) {
390
+ diagnostics.push(diagnostic("error", "unsafe-path", `members.${member.as}.prompt.${key}`, `路径越出组装目录:${relative}`, file));
391
+ return null;
392
+ }
393
+ if (!existsSync(resolved)) {
394
+ diagnostics.push(diagnostic("error", "missing-prompt-part", `members.${member.as}.prompt.${key}`, `文件不存在:${relative}`, file));
395
+ return null;
396
+ }
397
+ let text;
398
+ try {
399
+ text = readFileSync(resolved, "utf8").replace(/\s+$/, "");
400
+ } catch (error) {
401
+ diagnostics.push(diagnostic("error", "unreadable-prompt-part", `members.${member.as}.prompt.${key}`, `读不到 ${relative}:${error?.message ?? error}`, file));
402
+ return null;
403
+ }
404
+ const chars = charLength(text);
405
+ if (chars > LIMITS2.appendChars) {
406
+ diagnostics.push(diagnostic("error", "prompt-part-too-long", `members.${member.as}.prompt.${key}`, `${relative} 有 ${chars} 字符,超过 ${LIMITS2.appendChars}`, file));
407
+ return null;
408
+ }
409
+ extra[key] = text;
410
+ }
411
+ return extra;
412
+ }
413
+
414
+ function findRosterFile(dir) {
415
+ for (const name of ROSTER2_FILE_NAMES) {
416
+ const file = nodePath.join(dir, name);
417
+ if (existsSync(file) && statSync(file).isFile()) return file;
418
+ }
419
+ return null;
420
+ }
421
+
422
+ /** 从磁盘加载一份组装并编译(CLI / 工具用;测试也可直接用 compileRoster 传内存 roles)。 */
423
+ export function compileRosterAt(rosterDir, options = {}) {
424
+ const scanned = scanRoles(options.rolesDir, { knownToolPolicies: options.knownToolPolicies ?? null });
425
+ const result = compileRoster({
426
+ rosterDir,
427
+ roles: scanned.roles,
428
+ expectedId: options.expectedId ?? null,
429
+ knownToolPolicies: options.knownToolPolicies ?? null,
430
+ source: options.source ?? {},
431
+ });
432
+ return { ...result, diagnostics: [...scanned.diagnostics, ...result.diagnostics], roles: scanned.roles };
433
+ }
434
+
435
+ /** 供 `roster create` 之类使用:把编译产物摊平成桥接层认识的成员形状。 */
436
+ export function compileToMembers(compiled) {
437
+ return (compiled?.members ?? []).map((member) => ({
438
+ name: member.name,
439
+ role: member.role,
440
+ systemPrompt: member.executionPrompt,
441
+ ...(member.provider === null ? {} : { provider: member.provider }),
442
+ ...(member.model === null ? {} : { model: member.model }),
443
+ ...(member.reasoningEffort === null ? {} : { reasoningEffort: member.reasoningEffort }),
444
+ ...(member.toolPolicy === null ? {} : { toolPolicy: member.toolPolicy }),
445
+ aliases: [],
446
+ skills: [],
447
+ examples: [],
448
+ }));
449
+ }
450
+
451
+ export { splitFrontmatter };
452
+
453
+ /**
454
+ * 把编译产物摊成「读档案的人」熟悉的形状。
455
+ *
456
+ * v2 的组装只写引用(`role: name@1.0.0`),人设正文在角色文件里;但面板、验证器与
457
+ * 桥接层仍然按「一份档案 = id + 成员 + 队长」来读。这里是两种形状之间唯一的转换点:
458
+ * 成员的人设一律取编译后的 `executionPrompt`(也就是运行时真正会收到的那个字符串),
459
+ * 不用角色文件里的正文——否则「声明的人设」和「发出的人设」又会分成两份。
460
+ */
461
+ export function rosterViewOf(compiled, roster2, options = {}) {
462
+ const roles = options.roles ?? new Map();
463
+ const roleOf = (ref) => {
464
+ const name = String(ref ?? "").replace(/@[^@]*$/, "");
465
+ return roles.get(name)?.role ?? null;
466
+ };
467
+ const members = (compiled.members ?? []).map((member) => {
468
+ const role = roleOf(member.roleRef);
469
+ const declared = (roster2?.members ?? []).find((item) => item.as === member.name) ?? {};
470
+ // 哪个字段是「继承来的」这件事只有组装与角色知道,编译产物里已经摊平成值了。
471
+ // 桥接层靠 `inherited` 判断该把这个值记成"成员自带"还是"继承 defaults",所以在这里还原。
472
+ const inherited = [];
473
+ for (const field of ["provider", "model", "reasoningEffort"]) {
474
+ if (!declared.route?.[field] && !role?.route?.[field]) inherited.push(field);
475
+ }
476
+ if (!declared.tools?.policy && !role?.tools?.policy) inherited.push("toolPolicy");
477
+ return {
478
+ name: member.name,
479
+ role: member.role ?? role?.role ?? "",
480
+ title: role?.title ?? "",
481
+ description: role?.description ?? "",
482
+ tags: role?.tags ?? [],
483
+ skills: role?.requires?.skills ?? [],
484
+ provider: member.provider,
485
+ model: member.model,
486
+ reasoningEffort: member.reasoningEffort,
487
+ toolPolicy: member.toolPolicy ?? null,
488
+ systemPrompt: member.executionPrompt,
489
+ roleRef: member.roleRef,
490
+ promptChars: member.promptStats?.chars ?? 0,
491
+ inherited,
492
+ };
493
+ });
494
+ const captain = compiled.captain
495
+ ? {
496
+ name: String(compiled.captain.role ?? "").replace(/@[^@]*$/, ""),
497
+ roleRef: compiled.captain.role ?? null,
498
+ description: roleOf(compiled.captain.role)?.description ?? "",
499
+ systemPrompt: "",
500
+ provider: compiled.captain.provider,
501
+ model: compiled.captain.model,
502
+ reasoningEffort: compiled.captain.reasoningEffort,
503
+ personaPrefix: compiled.captain.persona ?? "",
504
+ }
505
+ : null;
506
+ return {
507
+ format: "v2",
508
+ id: compiled.source.roster,
509
+ version: compiled.source.version,
510
+ file: compiled.source.file ?? null,
511
+ title: roster2?.title ?? compiled.source.title ?? "",
512
+ description: roster2?.description ?? "",
513
+ license: roster2?.license ?? "",
514
+ maintainers: roster2?.maintainers ?? [],
515
+ tags: roster2?.tags ?? [],
516
+ notes: roster2?.notes ?? null,
517
+ members,
518
+ captain,
519
+ requires: compiled.requires ?? { skills: [], toolPolicies: [] },
520
+ orchestration: compiled.orchestration ?? null,
521
+ };
522
+ }