@stofone/dsh-agent-roster 0.9.2 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +80 -5
- package/README.zh.md +73 -6
- package/bin/cli.mjs +228 -2
- package/docs/roster-v2.md +220 -0
- package/examples/dist/core-review-1.0.0.json +160 -0
- package/examples/dist/research-0.1.0.json +95 -0
- package/examples/roles/core-review-correctness/AGENT.md +20 -0
- package/examples/roles/core-review-lead/AGENT.md +23 -0
- package/examples/roles/core-review-lead-captain/AGENT.md +13 -0
- package/examples/roles/core-review-maintainer/AGENT.md +20 -0
- package/examples/roles/research-scout/AGENT.md +16 -0
- package/examples/roles/research-verifier/AGENT.md +16 -0
- package/examples/rosters/core-review/roster.yaml +39 -0
- package/examples/rosters/research/roster.yaml +20 -0
- package/examples-orchestration/dist/core-review-1.0.0.json +185 -0
- package/examples-orchestration/dist/core-review-1.0.0.tasks.json +76 -0
- package/examples-orchestration/roles/fixer/AGENT.md +18 -0
- package/examples-orchestration/roles/lead/AGENT.md +18 -0
- package/examples-orchestration/roles/reviewer/AGENT.md +23 -0
- package/examples-orchestration/roles/reviewer/parts/checklist.md +6 -0
- package/examples-orchestration/rosters/core-review/overrides/fixer.md +1 -0
- package/examples-orchestration/rosters/core-review/roster.yaml +43 -0
- package/index.js +8 -1
- package/lib/compile2.js +522 -0
- package/lib/format-v2.js +196 -0
- package/lib/frontmatter.js +113 -0
- package/lib/migrate.js +326 -0
- package/lib/plan.js +167 -0
- package/lib/registry.js +65 -11
- package/lib/role.js +498 -0
- package/lib/roster2.js +591 -0
- package/lib/run.js +188 -0
- package/lib/write.js +14 -8
- package/orgs/agent-roster-collective/dist/api-design-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/content-ops-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/contract-review-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/data-quality-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/db-migration-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/docs-writer-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/finance-recon-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/frontend-ui-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/i18n-l10n-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/incident-response-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/literature-review-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/market-research-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/ml-eval-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/performance-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/product-discovery-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/qa-regression-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/recruiting-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/release-manager-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/security-audit-1.0.0.json +132 -0
- package/orgs/agent-roster-collective/dist/sre-oncall-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/dist/support-triage-1.0.0.json +130 -0
- package/orgs/agent-roster-collective/roles/api-design-failure-mode/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/api-design-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/api-design-schema/AGENT.md +20 -0
- package/orgs/agent-roster-collective/roles/content-ops-distributor/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/content-ops-editor/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/content-ops-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/contract-review-clause-reader/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/contract-review-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/contract-review-risk-note/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/data-quality-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/data-quality-profiler/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/data-quality-rule-author/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/db-migration-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/db-migration-migrator/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/db-migration-verifier/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/docs-writer-example-runner/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/docs-writer-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/docs-writer-writer/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/finance-recon-exception/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/finance-recon-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/finance-recon-matcher/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/frontend-ui-a11y-reviewer/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/frontend-ui-implementer/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/frontend-ui-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/i18n-l10n-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/i18n-l10n-reviewer/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/i18n-l10n-translator/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/incident-response-commander/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/incident-response-scribe/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/incident-response-triage/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/literature-review-critic/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/literature-review-extractor/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/literature-review-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/market-research-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/market-research-researcher/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/market-research-synthesizer/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/ml-eval-dataset/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/ml-eval-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/ml-eval-metric/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/performance-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/performance-load-tester/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/performance-profiler/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/product-discovery-interviewer/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/product-discovery-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/product-discovery-skeptic/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/qa-regression-case-writer/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/qa-regression-flake-hunter/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/qa-regression-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/recruiting-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/recruiting-scheduler/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/recruiting-screener/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/release-manager-changelog/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/release-manager-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/release-manager-rollback/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/security-audit-code-auditor/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/security-audit-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/security-audit-threat-modeler/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/sre-oncall-capacity/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/sre-oncall-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/sre-oncall-runbook/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/support-triage-classifier/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/support-triage-lead/AGENT.md +19 -0
- package/orgs/agent-roster-collective/roles/support-triage-responder/AGENT.md +19 -0
- package/orgs/agent-roster-collective/rosters/api-design/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/content-ops/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/contract-review/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/data-quality/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/db-migration/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/docs-writer/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/finance-recon/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/frontend-ui/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/i18n-l10n/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/incident-response/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/literature-review/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/market-research/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/ml-eval/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/performance/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/product-discovery/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/qa-regression/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/recruiting/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/release-manager/roster.yaml +25 -0
- package/orgs/agent-roster-collective/rosters/security-audit/roster.yaml +30 -0
- package/orgs/agent-roster-collective/rosters/sre-oncall/roster.yaml +26 -0
- package/orgs/agent-roster-collective/rosters/support-triage/roster.yaml +25 -0
- package/package.json +3 -1
- package/schema/compiled.v1.schema.json +391 -0
- package/schema/role.v1.schema.json +240 -0
- package/schema/roster.v2.schema.json +437 -0
- package/examples/core-review/roster.yaml +0 -82
- package/examples/research/roster.yaml +0 -42
- package/orgs/agent-roster-collective/api-design/roster.yaml +0 -67
- package/orgs/agent-roster-collective/content-ops/roster.yaml +0 -55
- package/orgs/agent-roster-collective/contract-review/roster.yaml +0 -58
- package/orgs/agent-roster-collective/data-quality/roster.yaml +0 -64
- package/orgs/agent-roster-collective/db-migration/roster.yaml +0 -64
- package/orgs/agent-roster-collective/docs-writer/roster.yaml +0 -59
- package/orgs/agent-roster-collective/finance-recon/roster.yaml +0 -63
- package/orgs/agent-roster-collective/frontend-ui/roster.yaml +0 -60
- package/orgs/agent-roster-collective/i18n-l10n/roster.yaml +0 -60
- package/orgs/agent-roster-collective/incident-response/roster.yaml +0 -65
- package/orgs/agent-roster-collective/literature-review/roster.yaml +0 -61
- package/orgs/agent-roster-collective/market-research/roster.yaml +0 -60
- package/orgs/agent-roster-collective/ml-eval/roster.yaml +0 -63
- package/orgs/agent-roster-collective/performance/roster.yaml +0 -63
- package/orgs/agent-roster-collective/product-discovery/roster.yaml +0 -55
- package/orgs/agent-roster-collective/qa-regression/roster.yaml +0 -64
- package/orgs/agent-roster-collective/recruiting/roster.yaml +0 -58
- package/orgs/agent-roster-collective/release-manager/roster.yaml +0 -59
- package/orgs/agent-roster-collective/security-audit/roster.yaml +0 -65
- package/orgs/agent-roster-collective/sre-oncall/roster.yaml +0 -60
- package/orgs/agent-roster-collective/support-triage/roster.yaml +0 -58
- /package/orgs/agent-roster-collective/{security-audit → rosters/security-audit}/capabilities.yaml +0 -0
package/lib/compile2.js
ADDED
|
@@ -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
|
+
}
|