@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
package/lib/roster2.js ADDED
@@ -0,0 +1,591 @@
1
+ /**
2
+ * 组装(roster@2)层:只描述「谁、按什么顺序、走什么流程」,不装人设。
3
+ *
4
+ * 一份 roster 是物料清单:成员是对角色的**引用**(`role: name@version`),只能覆盖少量
5
+ * 桥接字段、只能 prepend/append 正文片段,**不能重排或替换角色内部片段**——
6
+ * 想改行为就改角色版本,那是唯一入口。
7
+ *
8
+ * 与 v1 的关键差别:身份来自文档(v1 靠目录名),队长不是成员(v1 允许 `lead` 双写),
9
+ * 编排是一等公民(v1 完全没有这一层)。
10
+ *
11
+ * @module dsh-agent-roster/roster2
12
+ */
13
+ import {
14
+ CAPTAIN2_FIELDS,
15
+ DEFAULT_ON_MISSING,
16
+ DEFAULTS2_FIELDS,
17
+ GATE_FIELDS,
18
+ VERIFY_FIELDS,
19
+ LIMITS2,
20
+ MEMBER2_FIELDS,
21
+ MEMBER_PROMPT_FIELDS,
22
+ ON_MISSING,
23
+ ORCHESTRATION_FIELDS,
24
+ ORCHESTRATION_SHAPES,
25
+ PATTERNS2,
26
+ REQUIRES_FIELDS,
27
+ ROLE_REF,
28
+ ROSTER2_FIELDS,
29
+ ROSTER2_KIND,
30
+ ROSTER2_REQUIRED,
31
+ ROUTE_FIELDS,
32
+ STAGE_FIELDS,
33
+ TARGETS,
34
+ TOOL_SCOPE_FIELDS,
35
+ TOOLS_FIELDS,
36
+ } from "./format-v2.js";
37
+ import { PATTERNS, REASONING_EFFORTS } from "./format.js";
38
+ import { diagnostic } from "./parse.js";
39
+ import { isSafeRelative } from "./role.js";
40
+
41
+ /**
42
+ * 校验 roster@2 的结构(纯函数、零 IO;不检查角色是否真实存在)。
43
+ *
44
+ * @param {unknown} value
45
+ * @param {{ file?: string, knownToolPolicies?: string[] | null, expectedId?: string | null }} [options]
46
+ * @returns {{ ok: boolean, roster: object | null, diagnostics: object[] }}
47
+ */
48
+ export function validateRoster2(value, options = {}) {
49
+ const file = String(options.file ?? "");
50
+ const expectedId = typeof options.expectedId === "string" ? options.expectedId : null;
51
+ const knownToolPolicies = Array.isArray(options.knownToolPolicies) ? options.knownToolPolicies : null;
52
+ const diagnostics = [];
53
+ const report = (severity, code, path, reason) => diagnostics.push(diagnostic(severity, code, path, reason, file));
54
+
55
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
56
+ report("error", "document-not-mapping", "", "文档顶层必须是映射(对象)");
57
+ return { ok: false, roster: null, diagnostics };
58
+ }
59
+
60
+ for (const key of Object.keys(value)) {
61
+ if (!ROSTER2_FIELDS.includes(key)) {
62
+ report("error", "unknown-field", key, `未知字段 ${key};允许的字段见 schema/roster.v2.schema.json`);
63
+ }
64
+ }
65
+ for (const key of ROSTER2_REQUIRED) {
66
+ if (value[key] === undefined) report("error", "missing-field", key, `缺少必填字段 ${key}`);
67
+ }
68
+
69
+ if (value.kind !== ROSTER2_KIND) {
70
+ report("error", "invalid-kind", "kind", `kind 必须是 "${ROSTER2_KIND}",实际是 ${describe(value.kind)}`);
71
+ }
72
+
73
+ const id = text(value.id, "id", LIMITS2.roleNameChars, report);
74
+ if (id !== null && !PATTERNS2.id.test(id)) {
75
+ report("error", "invalid-id", "id", `id 必须是 kebab-case 的 2~64 字符:${id}`);
76
+ }
77
+ if (id !== null && expectedId !== null && id !== expectedId) {
78
+ // v2 不再要求目录名等于 id(身份来自文档),但仍把不一致作为 info 报出来:
79
+ // 它不影响正确性,只影响人类找文件的效率。
80
+ report("info", "id-directory-differs", "id", `文档 id(${id})与目录名(${expectedId})不同;v2 允许,但建议一致`);
81
+ }
82
+
83
+ const version = text(value.version, "version", 64, report);
84
+ if (version !== null && !PATTERNS2.semver.test(version)) {
85
+ report("error", "invalid-semver", "version", `version 必须是精确 SemVer:${version}`);
86
+ }
87
+
88
+ const title = text(value.title, "title", LIMITS2.titleChars, report);
89
+ const description = text(value.description, "description", LIMITS2.descriptionChars, report);
90
+ const license = text(value.license, "license", LIMITS2.titleChars, report);
91
+ const notes = text(value.notes, "notes", LIMITS2.notesChars, report);
92
+ const maintainers = list(value.maintainers, "maintainers", LIMITS2.maintainers, PATTERNS2.handle, report);
93
+ const tags = list(value.tags, "tags", LIMITS2.tagsPerRole, PATTERNS2.tag, report);
94
+
95
+ const defaults = validateDefaults(value.defaults, report);
96
+ const targets = validateTargets(value.targets, report);
97
+ const requires = validateRequires(value.requires, "requires", report);
98
+
99
+ let onMissing = DEFAULT_ON_MISSING;
100
+ if (value.onMissing !== undefined) {
101
+ if (!ON_MISSING.includes(value.onMissing)) report("error", "invalid-on-missing", "onMissing", `onMissing 只能是 ${ON_MISSING.join(" / ")}`);
102
+ else onMissing = value.onMissing;
103
+ }
104
+
105
+ const members = validateMembers(value.members, defaults, knownToolPolicies, report);
106
+ const captain = validateCaptain(value.captain, knownToolPolicies, report);
107
+ const orchestration = validateOrchestration(value.orchestration, members, report);
108
+
109
+ if (diagnostics.some((item) => item.severity === "error")) {
110
+ return { ok: false, roster: null, diagnostics };
111
+ }
112
+
113
+ return {
114
+ ok: true,
115
+ roster: {
116
+ kind: ROSTER2_KIND,
117
+ id,
118
+ version,
119
+ title,
120
+ description,
121
+ license,
122
+ notes,
123
+ maintainers,
124
+ tags,
125
+ defaults,
126
+ targets,
127
+ members,
128
+ captain,
129
+ orchestration,
130
+ requires,
131
+ onMissing,
132
+ },
133
+ diagnostics,
134
+ };
135
+ }
136
+
137
+ function validateDefaults(value, report) {
138
+ const defaults = { route: {}, tools: {} };
139
+ if (value === undefined) return defaults;
140
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
141
+ report("error", "invalid-field", "defaults", "defaults 必须是映射");
142
+ return defaults;
143
+ }
144
+ for (const key of Object.keys(value)) {
145
+ if (!DEFAULTS2_FIELDS.includes(key)) report("error", "unknown-field", `defaults.${key}`, `未知字段 defaults.${key}`);
146
+ }
147
+ defaults.route = route(value.route, "defaults.route", report) ?? {};
148
+ defaults.tools = tools(value.tools, "defaults.tools", null, report) ?? {};
149
+ return defaults;
150
+ }
151
+
152
+ function validateTargets(value, report) {
153
+ if (value === undefined) return ["agent-teams/members"];
154
+ if (!Array.isArray(value) || value.length === 0) {
155
+ report("error", "invalid-targets", "targets", "targets 必须是非空数组(至少声明一个编译目标)");
156
+ return [];
157
+ }
158
+ const out = [];
159
+ value.forEach((item, index) => {
160
+ if (!TARGETS.includes(item)) {
161
+ report("error", "invalid-target", `targets[${index}]`, `未知编译目标 ${describe(item)};允许:${TARGETS.join(", ")}`);
162
+ return;
163
+ }
164
+ out.push(item);
165
+ });
166
+ if (new Set(out).size !== out.length) report("error", "duplicate-target", "targets", "编译目标重复");
167
+ return out;
168
+ }
169
+
170
+ function validateMembers(value, defaults, knownToolPolicies, report) {
171
+ if (!Array.isArray(value) || value.length === 0) {
172
+ report("error", "missing-field", "members", "members 必须是非空数组");
173
+ return [];
174
+ }
175
+ if (value.length > LIMITS2.members) report("error", "too-many-members", "members", `members 最多 ${LIMITS2.members} 个`);
176
+ const out = [];
177
+ const seen = new Set();
178
+ value.forEach((raw, index) => {
179
+ const path = `members[${index}]`;
180
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
181
+ report("error", "invalid-member", path, "成员必须是映射");
182
+ return;
183
+ }
184
+ for (const key of Object.keys(raw)) {
185
+ if (!MEMBER2_FIELDS.includes(key)) report("error", "unknown-field", `${path}.${key}`, `未知字段 ${path}.${key}`);
186
+ }
187
+ const as = text(raw.as, `${path}.as`, LIMITS2.roleNameChars, report);
188
+ if (as !== null && !PATTERNS2.id.test(as)) report("error", "invalid-member-name", `${path}.as`, `as 必须是 kebab-case:${as}`);
189
+ if (as !== null) {
190
+ if (seen.has(as)) report("error", "duplicate-member", `${path}.as`, `成员名重复:${as}`);
191
+ seen.add(as);
192
+ }
193
+ const roleRef = ref(raw.role, `${path}.role`, report);
194
+
195
+ const ownRoute = mergeRoute(defaults.route, route(raw.route, `${path}.route`, report) ?? {});
196
+ const ownTools = mergeTools(defaults.tools, tools(raw.tools, `${path}.tools`, knownToolPolicies, report) ?? {});
197
+
198
+ let prompt = { prepend: null, append: null };
199
+ if (raw.prompt !== undefined) {
200
+ if (raw.prompt === null || typeof raw.prompt !== "object" || Array.isArray(raw.prompt)) {
201
+ report("error", "invalid-field", `${path}.prompt`, "members[].prompt 必须是映射");
202
+ } else {
203
+ for (const key of Object.keys(raw.prompt)) {
204
+ if (!MEMBER_PROMPT_FIELDS.includes(key)) {
205
+ report("error", "unknown-field", `${path}.prompt.${key}`, `未知字段 ${path}.prompt.${key};只能 prepend / append`);
206
+ }
207
+ }
208
+ for (const key of MEMBER_PROMPT_FIELDS) {
209
+ const item = raw.prompt[key];
210
+ if (item === undefined) continue;
211
+ if (typeof item !== "string" || !isSafeRelative(item)) {
212
+ report("error", "unsafe-path", `${path}.prompt.${key}`, `${key} 必须是角色/组装目录内的相对路径:${describe(item)}`);
213
+ continue;
214
+ }
215
+ prompt[key] = item;
216
+ }
217
+ }
218
+ }
219
+
220
+ const memberRequires = validateRequires(raw.requires, `${path}.requires`, report);
221
+ const memberNotes = text(raw.notes, `${path}.notes`, LIMITS2.notesChars, report);
222
+
223
+ out.push({ as, role: roleRef, route: ownRoute, tools: ownTools, prompt, requires: memberRequires, notes: memberNotes });
224
+ });
225
+ return out;
226
+ }
227
+
228
+ function validateCaptain(value, knownToolPolicies, report) {
229
+ if (value === undefined) return null;
230
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
231
+ report("error", "invalid-field", "captain", "captain 必须是映射");
232
+ return null;
233
+ }
234
+ for (const key of Object.keys(value)) {
235
+ if (!CAPTAIN2_FIELDS.includes(key)) report("error", "unknown-field", `captain.${key}`, `未知字段 captain.${key}`);
236
+ }
237
+ const roleRef = value.role === undefined ? null : ref(value.role, "captain.role", report);
238
+ const routeFields = route(value.route, "captain.route", report) ?? {};
239
+ const policyOverride = tools(value.tools, "captain.tools", knownToolPolicies, report);
240
+ let persona = null;
241
+ if (value.persona !== undefined) {
242
+ if (typeof value.persona !== "string" || !isSafeRelative(value.persona)) {
243
+ report("error", "unsafe-path", "captain.persona", `persona 必须是组装目录内的相对路径:${describe(value.persona)}`);
244
+ } else {
245
+ persona = value.persona;
246
+ }
247
+ }
248
+ return { role: roleRef, route: routeFields, tools: policyOverride ?? {}, persona, notes: text(value.notes, "captain.notes", LIMITS2.notesChars, report) };
249
+ }
250
+
251
+ function validateOrchestration(value, members, report) {
252
+ if (value === undefined) return null;
253
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
254
+ report("error", "invalid-field", "orchestration", "orchestration 必须是映射");
255
+ return null;
256
+ }
257
+ for (const key of Object.keys(value)) {
258
+ if (!ORCHESTRATION_FIELDS.includes(key)) report("error", "unknown-field", `orchestration.${key}`, `未知字段 orchestration.${key}`);
259
+ }
260
+ let shape = null;
261
+ if (value.shape !== undefined) {
262
+ if (!ORCHESTRATION_SHAPES.includes(value.shape)) {
263
+ report("error", "invalid-shape", "orchestration.shape", `shape 只能是 ${ORCHESTRATION_SHAPES.join(" / ")}`);
264
+ } else {
265
+ shape = value.shape;
266
+ }
267
+ }
268
+ const aliases = new Set(members.map((member) => member.as).filter(Boolean));
269
+ const stageIds = new Set();
270
+ const stages = [];
271
+ if (value.stages !== undefined) {
272
+ if (!Array.isArray(value.stages) || value.stages.length > LIMITS2.stages) {
273
+ report("error", "invalid-stages", "orchestration.stages", `stages 必须是数组(最多 ${LIMITS2.stages} 个)`);
274
+ } else {
275
+ value.stages.forEach((raw, index) => {
276
+ const path = `orchestration.stages[${index}]`;
277
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
278
+ report("error", "invalid-stage", path, "阶段必须是映射");
279
+ return;
280
+ }
281
+ for (const key of Object.keys(raw)) {
282
+ if (!STAGE_FIELDS.includes(key)) report("error", "unknown-field", `${path}.${key}`, `未知字段 ${path}.${key}`);
283
+ }
284
+ const id = text(raw.id, `${path}.id`, LIMITS2.roleNameChars, report);
285
+ if (id !== null) {
286
+ if (stageIds.has(id)) report("error", "duplicate-stage", `${path}.id`, `阶段 id 重复:${id}`);
287
+ stageIds.add(id);
288
+ }
289
+ const parallel = nameList(raw.parallel, `${path}.parallel`, aliases, report);
290
+ const serial = nameList(raw.serial, `${path}.serial`, aliases, report);
291
+ if (parallel.length > 0 && serial.length > 0) {
292
+ report("error", "stage-conflict", path, "一个阶段不能同时是 parallel 与 serial");
293
+ }
294
+ let maxRounds = 1;
295
+ if (raw.maxRounds !== undefined) {
296
+ if (!Number.isInteger(raw.maxRounds) || raw.maxRounds < 1 || raw.maxRounds > LIMITS2.maxRounds) {
297
+ report("error", "invalid-max-rounds", `${path}.maxRounds`, `maxRounds 必须是 1~${LIMITS2.maxRounds} 的整数`);
298
+ } else {
299
+ maxRounds = raw.maxRounds;
300
+ }
301
+ }
302
+ stages.push({
303
+ id,
304
+ parallel,
305
+ serial,
306
+ join: text(raw.join, `${path}.join`, LIMITS2.roleNameChars, report),
307
+ on: text(raw.on, `${path}.on`, LIMITS2.roleChars, report),
308
+ role: raw.role === undefined ? null : ref(raw.role, `${path}.role`, report),
309
+ maxRounds,
310
+ });
311
+ });
312
+ }
313
+ }
314
+ const gates = [];
315
+ if (value.gates !== undefined) {
316
+ if (!Array.isArray(value.gates) || value.gates.length > LIMITS2.gates) {
317
+ report("error", "invalid-gates", "orchestration.gates", `gates 必须是数组(最多 ${LIMITS2.gates} 个)`);
318
+ } else {
319
+ value.gates.forEach((raw, index) => {
320
+ const path = `orchestration.gates[${index}]`;
321
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
322
+ report("error", "invalid-gate", path, "门必须是映射");
323
+ return;
324
+ }
325
+ for (const key of Object.keys(raw)) {
326
+ if (!GATE_FIELDS.includes(key)) report("error", "unknown-field", `${path}.${key}`, `未知字段 ${path}.${key}`);
327
+ }
328
+ const id = text(raw.id, `${path}.id`, LIMITS2.roleNameChars, report);
329
+ const after = stringArray(raw.after, `${path}.after`, LIMITS2.stages, report);
330
+ for (const stageId of after) {
331
+ if (!stageIds.has(stageId)) {
332
+ report("error", "unknown-stage", `${path}.after`, `门等待了不存在的阶段:${stageId}`);
333
+ }
334
+ }
335
+ // 判据是句子,不是引用:只钉条数与长度,不假装能验它的内容。
336
+ const requiresList = stringArray(raw.requires, `${path}.requires`, 16, report, LIMITS2.gateCriterionChars);
337
+ const verifyList = gateVerify(raw.verify, `${path}.verify`, report);
338
+ if (verifyList.length === 0) {
339
+ report(
340
+ "error",
341
+ "gate-unverifiable",
342
+ `${path}.verify`,
343
+ "门没有任何可执行判据:它只能自己给自己盖章。至少写一条 verify(跑什么命令、期望退出码)",
344
+ );
345
+ }
346
+ const by = text(raw.by, `${path}.by`, LIMITS2.roleNameChars, report);
347
+ if (by !== null && !aliases.has(by)) {
348
+ report("error", "unknown-member", `${path}.by`, `门由不存在的成员判定:${by}`);
349
+ }
350
+ gates.push({ id, after, requires: requiresList, verify: verifyList, by });
351
+ });
352
+ }
353
+ }
354
+ if (shape !== null && stages.length === 0) {
355
+ report("error", "empty-orchestration", "orchestration", "声明了 shape 就必须至少有一个 stage");
356
+ }
357
+ return shape === null && stages.length === 0 && gates.length === 0 ? null : { shape, stages, gates };
358
+ }
359
+
360
+ function nameList(value, path, aliases, report) {
361
+ if (value === undefined) return [];
362
+ if (!Array.isArray(value) || value.length > LIMITS2.members) {
363
+ report("error", "invalid-field", path, `${path} 必须是成员名数组`);
364
+ return [];
365
+ }
366
+ const out = [];
367
+ value.forEach((item, index) => {
368
+ if (typeof item !== "string" || !PATTERNS2.id.test(item)) {
369
+ report("error", "invalid-item", `${path}[${index}]`, `${path}[${index}] 必须是成员名`);
370
+ return;
371
+ }
372
+ if (!aliases.has(item)) report("error", "unknown-member", `${path}[${index}]`, `阶段引用了不存在的成员:${item}`);
373
+ out.push(item);
374
+ });
375
+ return out;
376
+ }
377
+
378
+ function validateRequires(value, path, report) {
379
+ if (value === undefined) return { skills: [], toolPolicies: [] };
380
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
381
+ report("error", "invalid-field", path, "requires 必须是映射");
382
+ return { skills: [], toolPolicies: [] };
383
+ }
384
+ for (const key of Object.keys(value)) {
385
+ if (!REQUIRES_FIELDS.includes(key)) report("error", "unknown-field", `${path}.${key}`, `未知要求 ${path}.${key}:只有有证据源的要求才能被声明`);
386
+ }
387
+ return {
388
+ skills: list(value.skills, `${path}.skills`, LIMITS2.skillsPerRole, PATTERNS2.skill, report),
389
+ toolPolicies: list(value.toolPolicies, `${path}.toolPolicies`, 16, PATTERNS2.id, report),
390
+ };
391
+ }
392
+
393
+ function route(value, path, report) {
394
+ if (value === undefined) return null;
395
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
396
+ report("error", "invalid-field", path, "route 必须是映射");
397
+ return null;
398
+ }
399
+ for (const key of Object.keys(value)) {
400
+ if (!ROUTE_FIELDS.includes(key)) report("error", "unknown-field", `${path}.${key}`, `未知字段 ${path}.${key}`);
401
+ }
402
+ const out = {};
403
+ if (value.provider !== undefined) {
404
+ if (typeof value.provider !== "string" || !PATTERNS2.provider.test(value.provider)) report("error", "invalid-provider", `${path}.provider`, `provider 形态不合法:${describe(value.provider)}`);
405
+ else out.provider = value.provider;
406
+ }
407
+ if (value.model !== undefined) {
408
+ if (typeof value.model !== "string" || !PATTERNS2.model.test(value.model)) report("error", "invalid-model", `${path}.model`, `model 形态不合法:${describe(value.model)}`);
409
+ else out.model = value.model;
410
+ }
411
+ if (value.reasoningEffort !== undefined) {
412
+ if (!REASONING_EFFORTS.includes(value.reasoningEffort)) report("error", "invalid-effort", `${path}.reasoningEffort`, `reasoningEffort 只能是 ${REASONING_EFFORTS.join(" / ")}`);
413
+ else out.reasoningEffort = value.reasoningEffort;
414
+ }
415
+ if ((out.provider === undefined) !== (out.model === undefined)) {
416
+ report("error", "incomplete-route", path, "provider 与 model 必须成对出现");
417
+ }
418
+ return out;
419
+ }
420
+
421
+ function tools(value, path, knownToolPolicies, report) {
422
+ if (value === undefined) return null;
423
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
424
+ report("error", "invalid-field", path, "tools 必须是映射");
425
+ return null;
426
+ }
427
+ for (const key of Object.keys(value)) {
428
+ if (!TOOLS_FIELDS.includes(key)) report("error", "unknown-field", `${path}.${key}`, `未知字段 ${path}.${key}`);
429
+ }
430
+ const out = {};
431
+ if (value.policy !== undefined) {
432
+ if (typeof value.policy !== "string" || !PATTERNS2.id.test(value.policy)) {
433
+ report("error", "invalid-tool-policy", `${path}.policy`, `policy 必须是 kebab-case:${describe(value.policy)}`);
434
+ } else if (knownToolPolicies !== null && knownToolPolicies.length > 0 && !knownToolPolicies.includes(value.policy)) {
435
+ report("error", "unknown-tool-policy", `${path}.policy`, `部署没有声明工具策略 ${value.policy};已知:${knownToolPolicies.join(", ")}`);
436
+ } else {
437
+ out.policy = value.policy;
438
+ }
439
+ }
440
+ if (value.scope !== undefined) {
441
+ if (value.scope === null || typeof value.scope !== "object" || Array.isArray(value.scope)) {
442
+ report("error", "invalid-field", `${path}.scope`, "scope 必须是映射");
443
+ } else {
444
+ out.scope = {};
445
+ for (const key of Object.keys(value.scope)) {
446
+ if (!TOOL_SCOPE_FIELDS.includes(key)) report("error", "unknown-field", `${path}.scope.${key}`, `未知字段 ${path}.scope.${key}`);
447
+ }
448
+ for (const key of TOOL_SCOPE_FIELDS) {
449
+ if (value.scope[key] === undefined) continue;
450
+ const items = value.scope[key];
451
+ if (!Array.isArray(items) || items.length > 64 || items.some((item) => typeof item !== "string" || !PATTERNS.toolPattern.test(item))) {
452
+ report("error", "invalid-tool-scope", `${path}.scope.${key}`, `${key} 必须是工具名数组(最多 64 项)`);
453
+ continue;
454
+ }
455
+ out.scope[key] = items.slice();
456
+ }
457
+ }
458
+ }
459
+ return out;
460
+ }
461
+
462
+ function mergeRoute(base, own) {
463
+ const out = { ...(base ?? {}) };
464
+ for (const [key, value] of Object.entries(own ?? {})) if (value !== undefined) out[key] = value;
465
+ return out;
466
+ }
467
+
468
+ function mergeTools(base, own) {
469
+ return { ...(base ?? {}), ...(own ?? {}) };
470
+ }
471
+
472
+ function ref(value, path, report) {
473
+ if (value === undefined) {
474
+ report("error", "missing-field", path, "缺少角色引用(name@version)");
475
+ return null;
476
+ }
477
+ if (typeof value !== "string") {
478
+ report("error", "invalid-role-ref", path, `角色引用必须是字符串:${describe(value)}`);
479
+ return null;
480
+ }
481
+ const match = ROLE_REF.exec(value);
482
+ if (match === null) {
483
+ report("error", "invalid-role-ref", path, `角色引用必须是 name@x.y.z:${value}`);
484
+ return null;
485
+ }
486
+ return { name: match[1], version: match[2], raw: value };
487
+ }
488
+
489
+ function text(value, path, maxChars, report) {
490
+ if (value === undefined) return null;
491
+ if (typeof value !== "string" || value === "") {
492
+ report("error", "invalid-field", path, `${path} 必须是非空字符串`);
493
+ return null;
494
+ }
495
+ if (value.includes("\n")) {
496
+ report("error", "multiline-field", path, `${path} 必须是单行;长文本请写进角色正文或片段`);
497
+ return null;
498
+ }
499
+ if ([...value].length > maxChars) {
500
+ report("error", "field-too-long", path, `${path} 有 ${[...value].length} 字符,超过 ${maxChars}`);
501
+ return null;
502
+ }
503
+ return value;
504
+ }
505
+
506
+ function stringArray(value, path, max, report, maxChars = null) {
507
+ if (value === undefined) return [];
508
+ if (!Array.isArray(value) || value.length > max || value.some((item) => typeof item !== "string" || item === "")) {
509
+ report("error", "invalid-field", path, `${path} 必须是最多 ${max} 项的字符串数组`);
510
+ return [];
511
+ }
512
+ if (maxChars !== null) {
513
+ const tooLong = value.filter((item) => [...item].length > maxChars);
514
+ if (tooLong.length > 0) {
515
+ report("error", "field-too-long", path, `${path} 有 ${tooLong.length} 项超过 ${maxChars} 字符`);
516
+ return [];
517
+ }
518
+ }
519
+ return value.slice();
520
+ }
521
+
522
+ function list(value, path, max, pattern, report) {
523
+ if (value === undefined) return [];
524
+ if (!Array.isArray(value)) {
525
+ report("error", "invalid-field", path, `${path} 必须是数组`);
526
+ return [];
527
+ }
528
+ if (value.length > max) report("error", "too-many-items", path, `${path} 最多 ${max} 项`);
529
+ const out = [];
530
+ value.forEach((item, index) => {
531
+ if (typeof item !== "string" || !pattern.test(item)) {
532
+ report("error", "invalid-item", `${path}[${index}]`, `${path}[${index}] 形态不合法:${describe(item)}`);
533
+ return;
534
+ }
535
+ out.push(item);
536
+ });
537
+ if (new Set(out).size !== out.length) report("error", "duplicate-item", path, `${path} 里有重复项`);
538
+ return out;
539
+ }
540
+
541
+ function describe(value) {
542
+ if (value === undefined) return "缺失";
543
+ if (value === null) return "null";
544
+ if (typeof value === "string") return JSON.stringify(value);
545
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
546
+ if (Array.isArray(value)) return `数组(${value.length})`;
547
+ return "对象";
548
+ }
549
+
550
+ /**
551
+ * 门的可执行判据。
552
+ *
553
+ * 判据只有两种下场:要么能被跑出来,要么就是签字画押。这里只收前者——
554
+ * 门的存在意义是「别人能复核」,而 `requires` 里的一句话复核不了。
555
+ */
556
+ function gateVerify(value, path, report) {
557
+ if (value === undefined) return [];
558
+ if (!Array.isArray(value) || value.length > LIMITS2.gateVerifyCommands) {
559
+ report("error", "invalid-gate-verify", path, `verify 必须是数组(最多 ${LIMITS2.gateVerifyCommands} 条)`);
560
+ return [];
561
+ }
562
+ const list = [];
563
+ value.forEach((raw, index) => {
564
+ const at = `${path}[${index}]`;
565
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
566
+ report("error", "invalid-gate-verify", at, "一条判据必须是 {command, expectExit} 映射");
567
+ return;
568
+ }
569
+ for (const key of Object.keys(raw)) {
570
+ if (!VERIFY_FIELDS.includes(key)) report("error", "unknown-field", `${at}.${key}`, `未知字段 ${at}.${key}`);
571
+ }
572
+ const command = raw.command;
573
+ if (typeof command !== "string" || command.trim() === "") {
574
+ report("error", "invalid-gate-verify", `${at}.command`, "command 必须是非空字符串");
575
+ return;
576
+ }
577
+ if (command.length > LIMITS2.gateVerifyCommandChars) {
578
+ report("error", "field-too-long", `${at}.command`, `command 超过 ${LIMITS2.gateVerifyCommandChars} 字符`);
579
+ }
580
+ let expectExit = 0;
581
+ if (raw.expectExit !== undefined) {
582
+ if (!Number.isInteger(raw.expectExit) || raw.expectExit < 0 || raw.expectExit > 255) {
583
+ report("error", "invalid-gate-verify", `${at}.expectExit`, "expectExit 必须是 0..255 的整数");
584
+ } else {
585
+ expectExit = raw.expectExit;
586
+ }
587
+ }
588
+ list.push({ command, expectExit });
589
+ });
590
+ return list;
591
+ }