@stofone/dsh-agent-roster 0.9.1 → 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.
- package/CHANGELOG.md +71 -1
- package/README.md +78 -4
- package/README.zh.md +44 -1
- package/bin/cli.mjs +228 -2
- package/client.js +49 -24
- 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/format-v2.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 格式常量:角色(role) / 组装(roster@2) / 编译产物(compiled)。
|
|
3
|
+
*
|
|
4
|
+
* 与 `format.js` 的关系:v1(`dsh-agent-roster/v1`)冻结,不再长新字段;
|
|
5
|
+
* v2 是同一项目的下一个格式版本,两者共用 id / provider / model / semver 这些底层模式,
|
|
6
|
+
* 但**不共用文档白名单**——v1 的字段以成员为中心,v2 的字段以「角色 + 引用 + 编排」为中心。
|
|
7
|
+
*
|
|
8
|
+
* 这里只有数据,没有逻辑;改这里等于改格式,必须同步 bump 对应的 `@N` 与三个 schema 文件。
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-agent-roster/format-v2
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { PATTERNS } from "./format.js";
|
|
14
|
+
|
|
15
|
+
const { id, semver, provider, model, tag, skill } = PATTERNS;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 维护者句柄:这是「人/账号」,不是档案 id。
|
|
19
|
+
*
|
|
20
|
+
* 所以它允许大写与点(`STofone`、`first.last`),也允许长度 1——把人的 GitHub 账号
|
|
21
|
+
* 强行塞进档案 id 的小写短横线规则里,只会让迁移去篡改真实姓名。
|
|
22
|
+
*/
|
|
23
|
+
const handle = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
24
|
+
|
|
25
|
+
/** 角色文档(`roles/<任意目录>/AGENT.md` 的 frontmatter)。 */
|
|
26
|
+
export const ROLE_KIND = "dsh-agent-roster/role@1";
|
|
27
|
+
|
|
28
|
+
/** 组装文档(`rosters/<id>/roster.yaml`)。 */
|
|
29
|
+
export const ROSTER2_KIND = "dsh-agent-roster/roster@2";
|
|
30
|
+
|
|
31
|
+
/** 编译产物(`dist/<id>-<version>.json`,以及在内存里生成的 review 视图)。 */
|
|
32
|
+
export const COMPILED_KIND = "dsh-agent-roster/compiled@1";
|
|
33
|
+
/** 施工图:把编排摊成可执行的卡片清单,由队长会话去调用,插件自己不调。 */
|
|
34
|
+
export const PLAN_KIND = "dsh-agent-roster/plan@1";
|
|
35
|
+
|
|
36
|
+
/** 锁文件(`.agent-roster/lock.yaml`)。 */
|
|
37
|
+
export const LOCK_KIND = "dsh-agent-roster/lock@1";
|
|
38
|
+
|
|
39
|
+
/** 角色文件名(只有一个,不再按扩展名探测三种)。 */
|
|
40
|
+
export const ROLE_FILE_NAME = "AGENT.md";
|
|
41
|
+
|
|
42
|
+
/** 角色目录里允许出现的子目录:片段与示例。 */
|
|
43
|
+
export const ROLE_PARTS_DIR = "parts";
|
|
44
|
+
|
|
45
|
+
/** 组装文档文件名(与 v1 一致,靠 kind 区分版本)。 */
|
|
46
|
+
export const ROSTER2_FILE_NAMES = ["roster.yaml", "roster.yml", "roster.json"];
|
|
47
|
+
|
|
48
|
+
/** 上限。与 v1 的 `LIMITS` 分开:这些数字属于 v2 的语义,不是 v1 的放宽版。 */
|
|
49
|
+
export const LIMITS2 = {
|
|
50
|
+
roleNameChars: 64,
|
|
51
|
+
roleChars: 120,
|
|
52
|
+
titleChars: 120,
|
|
53
|
+
descriptionChars: 500,
|
|
54
|
+
notesChars: 500,
|
|
55
|
+
tagsPerRole: 16,
|
|
56
|
+
maintainers: 16,
|
|
57
|
+
skillsPerRole: 16,
|
|
58
|
+
/** 一个角色正文(AGENT.md 的 markdown 部分)的上限。 */
|
|
59
|
+
bodyChars: 8000,
|
|
60
|
+
/** 一个片段的字符上限。 */
|
|
61
|
+
partChars: 6000,
|
|
62
|
+
/** 一个角色最多拆成几个片段。 */
|
|
63
|
+
parts: 16,
|
|
64
|
+
/** 拼装后 executionPrompt 的硬上限——这是运行时字段的上限,不是建议值。 */
|
|
65
|
+
promptChars: 12_000,
|
|
66
|
+
/** roster 层可以追加的字符上限。 */
|
|
67
|
+
appendChars: 6000,
|
|
68
|
+
members: 32,
|
|
69
|
+
stages: 12,
|
|
70
|
+
gates: 12,
|
|
71
|
+
/** 一条验收判据的字符上限(它是句子,不是引用)。 */
|
|
72
|
+
gateCriterionChars: 160,
|
|
73
|
+
/** 一个门最多带几条可执行判据,以及一条命令的长度上限。 */
|
|
74
|
+
gateVerifyCommands: 8,
|
|
75
|
+
gateVerifyCommandChars: 500,
|
|
76
|
+
maxRounds: 5,
|
|
77
|
+
/** 一次编译最多解析多少个角色文件(防把仓库根当角色库扫)。 */
|
|
78
|
+
roleFiles: 512,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** 角色 frontmatter 允许的键(未知键一律拒绝)。 */
|
|
82
|
+
export const ROLE_FIELDS = [
|
|
83
|
+
"$schema",
|
|
84
|
+
"kind",
|
|
85
|
+
"name",
|
|
86
|
+
"version",
|
|
87
|
+
"role",
|
|
88
|
+
"title",
|
|
89
|
+
"description",
|
|
90
|
+
"tags",
|
|
91
|
+
"maintainers",
|
|
92
|
+
"license",
|
|
93
|
+
"route",
|
|
94
|
+
"tools",
|
|
95
|
+
"prompt",
|
|
96
|
+
"skills",
|
|
97
|
+
"examples",
|
|
98
|
+
"requires",
|
|
99
|
+
"onMissing",
|
|
100
|
+
"notes",
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
/** 角色必填键。`prompt` 可省略(表示正文即全部)。 */
|
|
104
|
+
export const ROLE_REQUIRED = ["kind", "name", "version", "role"];
|
|
105
|
+
|
|
106
|
+
/** `route` 允许的键:与 v1 的 direct 路由字段一一对应,只是嵌套了一层。 */
|
|
107
|
+
export const ROUTE_FIELDS = ["provider", "model", "reasoningEffort"];
|
|
108
|
+
|
|
109
|
+
/** `tools` 允许的键。`policy` 是 direct,`scope` 是 declaration。 */
|
|
110
|
+
export const TOOLS_FIELDS = ["policy", "scope"];
|
|
111
|
+
|
|
112
|
+
/** `tools.scope` 允许的键。 */
|
|
113
|
+
export const TOOL_SCOPE_FIELDS = ["allow", "deny"];
|
|
114
|
+
|
|
115
|
+
/** `prompt` 允许的键。`parts` 是有序片段,`budget` 是字符预算。 */
|
|
116
|
+
export const PROMPT_FIELDS = ["parts", "budget"];
|
|
117
|
+
|
|
118
|
+
/** `requires` 允许的键:只收有证据源的两类。 */
|
|
119
|
+
export const REQUIRES_FIELDS = ["skills", "toolPolicies"];
|
|
120
|
+
|
|
121
|
+
/** `onMissing` 的枚举。 */
|
|
122
|
+
export const ON_MISSING = ["refuse", "degraded"];
|
|
123
|
+
export const DEFAULT_ON_MISSING = "refuse";
|
|
124
|
+
|
|
125
|
+
/** 组装文档允许的顶层键。 */
|
|
126
|
+
export const ROSTER2_FIELDS = [
|
|
127
|
+
"$schema",
|
|
128
|
+
"kind",
|
|
129
|
+
"id",
|
|
130
|
+
"version",
|
|
131
|
+
"title",
|
|
132
|
+
"description",
|
|
133
|
+
"license",
|
|
134
|
+
"maintainers",
|
|
135
|
+
"tags",
|
|
136
|
+
"defaults",
|
|
137
|
+
"targets",
|
|
138
|
+
"members",
|
|
139
|
+
"captain",
|
|
140
|
+
"orchestration",
|
|
141
|
+
"requires",
|
|
142
|
+
"onMissing",
|
|
143
|
+
"notes",
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
/** 组装文档必填键。 */
|
|
147
|
+
export const ROSTER2_REQUIRED = ["kind", "id", "version", "members"];
|
|
148
|
+
|
|
149
|
+
/** `defaults` 允许的键:只放能被成员继承的**桥接字段**。 */
|
|
150
|
+
export const DEFAULTS2_FIELDS = ["route", "tools"];
|
|
151
|
+
|
|
152
|
+
/** 一个成员引用允许的键。 */
|
|
153
|
+
export const MEMBER2_FIELDS = ["as", "role", "route", "tools", "prompt", "requires", "notes"];
|
|
154
|
+
|
|
155
|
+
/** 成员 `prompt` 允许的键:只能 prepend / append,不能替换角色片段。 */
|
|
156
|
+
export const MEMBER_PROMPT_FIELDS = ["prepend", "append"];
|
|
157
|
+
|
|
158
|
+
/** 队长段允许的键。 */
|
|
159
|
+
export const CAPTAIN2_FIELDS = ["role", "route", "persona", "notes"];
|
|
160
|
+
|
|
161
|
+
/** 编排段允许的键。 */
|
|
162
|
+
export const ORCHESTRATION_FIELDS = ["shape", "stages", "gates"];
|
|
163
|
+
|
|
164
|
+
/** 一个阶段允许的键。 */
|
|
165
|
+
export const STAGE_FIELDS = ["id", "parallel", "join", "on", "role", "maxRounds", "serial"];
|
|
166
|
+
|
|
167
|
+
/** 一个门允许的键。 */
|
|
168
|
+
/**
|
|
169
|
+
* 门的字段。
|
|
170
|
+
*
|
|
171
|
+
* `after` 是「等哪些阶段结束」——机器可校验(必须是存在的 stage)。
|
|
172
|
+
* `requires` 是「验收判据」——是给人读的句子,编译器只做长度与条数上限,不假装能验它。
|
|
173
|
+
* `by` 是「谁来判」,必须是这个组装里的成员名。
|
|
174
|
+
*/
|
|
175
|
+
export const GATE_FIELDS = ["id", "after", "requires", "verify", "by"];
|
|
176
|
+
|
|
177
|
+
/** 门的一条可执行判据:跑什么、期望退出码是什么。 */
|
|
178
|
+
export const VERIFY_FIELDS = ["command", "expectExit"];
|
|
179
|
+
|
|
180
|
+
/** 编排形态。只有一种被实现;其余留给未来而不是写一个假开关。 */
|
|
181
|
+
export const ORCHESTRATION_SHAPES = ["gate-pipeline"];
|
|
182
|
+
|
|
183
|
+
/** 编译目标:决定字段落到哪个面。 */
|
|
184
|
+
export const TARGETS = [
|
|
185
|
+
"agent-teams/members",
|
|
186
|
+
"agent-teams/orchestration",
|
|
187
|
+
"profile/agent-preset",
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
/** 角色引用:`name@1.2.3`。 */
|
|
191
|
+
export const ROLE_REF = new RegExp(`^(${id.source.slice(1, -1)})@(${semver.source.slice(1, -1)})$`);
|
|
192
|
+
|
|
193
|
+
/** 拼装分隔符——写进格式,不留在代码里。 */
|
|
194
|
+
export const PROMPT_SEPARATOR = "\n\n";
|
|
195
|
+
|
|
196
|
+
export const PATTERNS2 = { id, semver, provider, model, tag, skill, handle, roleRef: ROLE_REF };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown + YAML frontmatter 解析:把一份 `AGENT.md` 拆成「参数」(frontmatter)与「人设」(正文)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不用一种方言装两种内容:散文需要高亮、diff、lint;参数需要机器校验。
|
|
5
|
+
* 混在一个 YAML 块标量里,缩进错一格内容就静默变形(v1 的真实痛点,见 issue #3)。
|
|
6
|
+
*
|
|
7
|
+
* 解析只回答「这段文本能否拆成两块」,语义校验交给 `role.js`;失败一律转诊断,不抛异常。
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-agent-roster/frontmatter
|
|
10
|
+
*/
|
|
11
|
+
import { diagnostic } from "./parse.js";
|
|
12
|
+
|
|
13
|
+
const FENCE = /^---[ \t]*$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 拆分 frontmatter 与正文。
|
|
17
|
+
*
|
|
18
|
+
* 规则(写死在这里,作为格式的一部分):
|
|
19
|
+
* 1. 第一行必须是 `---`(允许前面有空行与 BOM,但只有这一种围栏);
|
|
20
|
+
* 2. 结束围栏是**独占一行**的 `---`;
|
|
21
|
+
* 3. 正文只去掉开头连续的空白行,其余原样保留(不做 dedent、不改缩进)。
|
|
22
|
+
*
|
|
23
|
+
* @param {string} text 文件全文
|
|
24
|
+
* @param {{ file?: string }} [options]
|
|
25
|
+
* @returns {{ ok: boolean, frontmatter: string | null, body: string, diagnostics: object[] }}
|
|
26
|
+
*/
|
|
27
|
+
export function splitFrontmatter(text, options = {}) {
|
|
28
|
+
const file = String(options.file ?? "");
|
|
29
|
+
const source = String(text ?? "").replace(/^\uFEFF/, "");
|
|
30
|
+
const diagnostics = [];
|
|
31
|
+
|
|
32
|
+
if (source.trim() === "") {
|
|
33
|
+
diagnostics.push(diagnostic("error", "empty-document", "", "文档为空", file));
|
|
34
|
+
return { ok: false, frontmatter: null, body: "", diagnostics };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lines = source.split("\n");
|
|
38
|
+
let index = 0;
|
|
39
|
+
while (index < lines.length && lines[index].trim() === "") index += 1;
|
|
40
|
+
|
|
41
|
+
if (index >= lines.length || !FENCE.test(lines[index])) {
|
|
42
|
+
diagnostics.push(
|
|
43
|
+
diagnostic(
|
|
44
|
+
"error",
|
|
45
|
+
"missing-frontmatter",
|
|
46
|
+
"",
|
|
47
|
+
"AGENT.md 必须以独占一行的 --- 开头,紧接着 YAML frontmatter;正文放在第二个 --- 之后",
|
|
48
|
+
file,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
return { ok: false, frontmatter: null, body: "", diagnostics };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const start = index + 1;
|
|
55
|
+
let end = -1;
|
|
56
|
+
for (let cursor = start; cursor < lines.length; cursor += 1) {
|
|
57
|
+
if (FENCE.test(lines[cursor])) {
|
|
58
|
+
end = cursor;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (end === -1) {
|
|
64
|
+
diagnostics.push(
|
|
65
|
+
diagnostic("error", "unterminated-frontmatter", "", "frontmatter 没有结束围栏(缺少独占一行的 ---)", file),
|
|
66
|
+
);
|
|
67
|
+
return { ok: false, frontmatter: null, body: "", diagnostics };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const frontmatter = lines.slice(start, end).join("\n");
|
|
71
|
+
if (frontmatter.trim() === "") {
|
|
72
|
+
diagnostics.push(diagnostic("error", "empty-frontmatter", "", "frontmatter 为空:至少要有 kind / name / version / role", file));
|
|
73
|
+
return { ok: false, frontmatter: null, body: "", diagnostics };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let bodyStart = end + 1;
|
|
77
|
+
while (bodyStart < lines.length && lines[bodyStart].trim() === "") bodyStart += 1;
|
|
78
|
+
const body = lines.slice(bodyStart).join("\n").replace(/\s+$/, "");
|
|
79
|
+
|
|
80
|
+
if (body === "") {
|
|
81
|
+
diagnostics.push(
|
|
82
|
+
diagnostic("error", "empty-body", "body", "正文为空:人设写在第二个 --- 之后;空人设不会被当成“用默认值”", file),
|
|
83
|
+
);
|
|
84
|
+
return { ok: false, frontmatter, body: "", diagnostics };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { ok: true, frontmatter, body, diagnostics };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 把正文与片段按格式规定的语义拼成最终 executionPrompt。
|
|
92
|
+
*
|
|
93
|
+
* 拼装规则是格式的一部分:`join("\n\n", [body, ...parts])`。
|
|
94
|
+
* 除了 roster 层允许的 prepend / append,没有第二个改变顺序或分隔符的入口。
|
|
95
|
+
*
|
|
96
|
+
* @param {string} body 角色正文
|
|
97
|
+
* @param {string[]} parts 片段正文(按 frontmatter 里的顺序)
|
|
98
|
+
* @param {{ prepend?: string, append?: string }} [extra] roster 层追加
|
|
99
|
+
* @returns {{ text: string, parts: string[] }}
|
|
100
|
+
*/
|
|
101
|
+
export function assemblePrompt(body, parts, extra = {}) {
|
|
102
|
+
const blocks = [];
|
|
103
|
+
if (typeof extra.prepend === "string" && extra.prepend !== "") blocks.push(extra.prepend);
|
|
104
|
+
if (body !== "") blocks.push(body);
|
|
105
|
+
for (const part of parts ?? []) if (typeof part === "string" && part !== "") blocks.push(part);
|
|
106
|
+
if (typeof extra.append === "string" && extra.append !== "") blocks.push(extra.append);
|
|
107
|
+
return { text: blocks.join("\n\n"), parts: blocks.map((block) => block.length) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 字符数(人设预算按字符计,不按字节,也不猜 token)。 */
|
|
111
|
+
export function charLength(text) {
|
|
112
|
+
return [...String(text ?? "")].length;
|
|
113
|
+
}
|
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v1 档案 → v2(角色 / 组装 / 编译)的原地迁移。
|
|
3
|
+
*
|
|
4
|
+
* 迁移只做形状改写,不做语义发明:
|
|
5
|
+
*
|
|
6
|
+
* - 成员原来内嵌的 `systemPrompt` 变成 `roles/<roster>-<member>/AGENT.md` 的正文;
|
|
7
|
+
* - 原来写在成员上的路由与工具策略搬进角色 frontmatter;
|
|
8
|
+
* - 组装只留引用(`role: <roster>-<member>@1.0.0`),成员名、标题、描述保持原样;
|
|
9
|
+
* - 队长从「成员之外的一个字段」变成真正的角色文件,`captain.role` 指向它;
|
|
10
|
+
* - 能力要求(`capabilities.yaml`)跟着档案走,内容不动。
|
|
11
|
+
*
|
|
12
|
+
* 角色名带档案 id 前缀,是因为 v2 要求**同一座库里角色名唯一**,而仓库里的 21 套业务档案
|
|
13
|
+
* 全都叫 `lead`;不加前缀就无法共存,加了前缀至少能追溯「这份人设属于谁」。
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-agent-roster/migrate
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
18
|
+
import nodePath from "node:path";
|
|
19
|
+
|
|
20
|
+
import { CAPABILITIES_FILE_NAMES, validateCapabilities } from "./capabilities.js";
|
|
21
|
+
import { compileRoster, scanRoles } from "./compile2.js";
|
|
22
|
+
import { MEMBER_FILE_NAMES } from "./format.js";
|
|
23
|
+
import { parseDocumentText } from "./parse.js";
|
|
24
|
+
import { compilePlan } from "./plan.js";
|
|
25
|
+
import { loadRosterAt } from "./registry.js";
|
|
26
|
+
|
|
27
|
+
export const ROLES_DIR = "roles";
|
|
28
|
+
export const ROSTERS_DIR = "rosters";
|
|
29
|
+
export const DIST_DIR = "dist";
|
|
30
|
+
|
|
31
|
+
const ROLE_VERSION = "1.0.0";
|
|
32
|
+
const EMPTY_PERSONA = "(迁移时原档案没有写人设正文;请补上这个角色的职责与边界。)";
|
|
33
|
+
|
|
34
|
+
/** 把值写成 YAML(块风格)。字符串一律 JSON 转义,避免冒号、井号被误读。 */
|
|
35
|
+
export function emitYaml(value, indent = 0) {
|
|
36
|
+
const pad = " ".repeat(indent);
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
if (value.length === 0) return "[]";
|
|
39
|
+
return value
|
|
40
|
+
.map((item) => {
|
|
41
|
+
if (item !== null && typeof item === "object") {
|
|
42
|
+
const nested = emitYaml(item, indent + 2);
|
|
43
|
+
// `- key: v` 的形式更像人写的 YAML:第一行跟在短横线后,其余按缩进续行。
|
|
44
|
+
const [first, ...rest] = nested.split("\n");
|
|
45
|
+
return [`${pad}- ${first.trimStart()}`, ...rest].join("\n");
|
|
46
|
+
}
|
|
47
|
+
return `${pad}- ${inline(item)}`;
|
|
48
|
+
})
|
|
49
|
+
.join("\n");
|
|
50
|
+
}
|
|
51
|
+
if (value !== null && typeof value === "object") {
|
|
52
|
+
if (Object.keys(value).length === 0) return "{}";
|
|
53
|
+
const lines = [];
|
|
54
|
+
for (const [key, item] of Object.entries(value)) {
|
|
55
|
+
if (item === undefined) continue;
|
|
56
|
+
if (Array.isArray(item)) {
|
|
57
|
+
lines.push(item.length === 0 ? `${pad}${key}: []` : `${pad}${key}:\n${emitYaml(item, indent + 2)}`);
|
|
58
|
+
} else if (item !== null && typeof item === "object") {
|
|
59
|
+
const body = emitYaml(item, indent + 2);
|
|
60
|
+
lines.push(Object.keys(item).length === 0 ? `${pad}${key}: {}` : `${pad}${key}:\n${body}`);
|
|
61
|
+
} else {
|
|
62
|
+
lines.push(`${pad}${key}: ${inline(item)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
|
67
|
+
return `${pad}${inline(value)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function inline(value) {
|
|
71
|
+
if (value === null) return "null";
|
|
72
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
73
|
+
if (Array.isArray(value)) return `[${value.map(inline).join(", ")}]`;
|
|
74
|
+
if (value !== null && typeof value === "object") return JSON.stringify(value);
|
|
75
|
+
return JSON.stringify(String(value));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 角色文件的内容:frontmatter + 空行 + 正文。 */
|
|
79
|
+
export function roleDocument(role, body) {
|
|
80
|
+
const text = String(body ?? "").trim().length > 0 ? String(body).trimEnd() : EMPTY_PERSONA;
|
|
81
|
+
return `---\n${emitYaml(role)}\n---\n\n${text}\n`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function routeOf(source) {
|
|
85
|
+
if (source === null || source === undefined) return null;
|
|
86
|
+
const route = {};
|
|
87
|
+
if (source.provider) route.provider = source.provider;
|
|
88
|
+
if (source.model) route.model = source.model;
|
|
89
|
+
if (source.reasoningEffort) route.reasoningEffort = source.reasoningEffort;
|
|
90
|
+
return Object.keys(route).length > 0 ? route : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function listOf(value) {
|
|
94
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 把一份 v1 档案转成 v2 的两类文件(角色文件 + 组装)。
|
|
99
|
+
* 编译产物在写盘之后由 `compileRoster` 生成,这里不重复拼装。
|
|
100
|
+
*/
|
|
101
|
+
export function convertRoster(roster, { capabilities = null } = {}) {
|
|
102
|
+
// v1 的 id 字段叫 `roster`;校验后的对象会补一个 `id`,但单测里可能直接传原文。
|
|
103
|
+
const rosterId = roster.id ?? roster.roster;
|
|
104
|
+
const version = roster.version ?? "1.0.0";
|
|
105
|
+
const files = [];
|
|
106
|
+
const members = [];
|
|
107
|
+
const roleNames = [];
|
|
108
|
+
const skills = new Set();
|
|
109
|
+
const policies = new Set();
|
|
110
|
+
|
|
111
|
+
for (const member of roster.members ?? []) {
|
|
112
|
+
const roleName = `${rosterId}-${member.name}`;
|
|
113
|
+
roleNames.push(roleName);
|
|
114
|
+
const memberSkills = listOf(member.skills);
|
|
115
|
+
const policy = member.toolPolicy ?? roster.defaults?.toolPolicy ?? null;
|
|
116
|
+
for (const skill of memberSkills) skills.add(skill);
|
|
117
|
+
if (policy) policies.add(policy);
|
|
118
|
+
const role = {
|
|
119
|
+
kind: "dsh-agent-roster/role@1",
|
|
120
|
+
name: roleName,
|
|
121
|
+
version: ROLE_VERSION,
|
|
122
|
+
role: member.role ?? member.name,
|
|
123
|
+
};
|
|
124
|
+
if (member.title) role.title = member.title;
|
|
125
|
+
if (member.description) role.description = member.description;
|
|
126
|
+
if (listOf(member.tags).length > 0) role.tags = listOf(member.tags);
|
|
127
|
+
const route = routeOf(member);
|
|
128
|
+
if (route) role.route = route;
|
|
129
|
+
if (member.toolPolicy) role.tools = { policy: member.toolPolicy };
|
|
130
|
+
if (memberSkills.length > 0) role.requires = { skills: memberSkills };
|
|
131
|
+
files.push({ path: nodePath.join(ROLES_DIR, roleName, "AGENT.md"), text: roleDocument(role, member.systemPrompt) });
|
|
132
|
+
members.push({ as: member.name, role: `${roleName}@${ROLE_VERSION}` });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let captain = null;
|
|
136
|
+
if (roster.captain) {
|
|
137
|
+
// v1 里队长常常与某个成员同名(`lead` 既是成员又是 captain);两者人设不同,
|
|
138
|
+
// 所以重名时给队长加后缀,而不是悄悄合并成同一个角色。
|
|
139
|
+
const base = `${rosterId}-${roster.captain.name ?? "captain"}`;
|
|
140
|
+
const captainName = roleNames.includes(base) ? `${base}-captain` : base;
|
|
141
|
+
roleNames.push(captainName);
|
|
142
|
+
const role = {
|
|
143
|
+
kind: "dsh-agent-roster/role@1",
|
|
144
|
+
name: captainName,
|
|
145
|
+
version: ROLE_VERSION,
|
|
146
|
+
role: roster.captain.role ?? "队长",
|
|
147
|
+
};
|
|
148
|
+
if (roster.captain.description) role.description = roster.captain.description;
|
|
149
|
+
const route = routeOf(roster.captain);
|
|
150
|
+
if (route) role.route = route;
|
|
151
|
+
files.push({
|
|
152
|
+
path: nodePath.join(ROLES_DIR, captainName, "AGENT.md"),
|
|
153
|
+
text: roleDocument(role, roster.captain.personaPrefix ?? roster.captain.description ?? ""),
|
|
154
|
+
});
|
|
155
|
+
captain = { role: `${captainName}@${ROLE_VERSION}` };
|
|
156
|
+
const captainRoute = routeOf(roster.captain);
|
|
157
|
+
if (captainRoute) captain.route = captainRoute;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const item of capabilities?.requires?.skills ?? []) skills.add(typeof item === "string" ? item : item.name);
|
|
161
|
+
for (const item of capabilities?.requires?.toolPolicies ?? []) policies.add(typeof item === "string" ? item : item.name);
|
|
162
|
+
|
|
163
|
+
const defaults = {};
|
|
164
|
+
const defaultRoute = routeOf(roster.defaults ?? null);
|
|
165
|
+
if (defaultRoute) defaults.route = defaultRoute;
|
|
166
|
+
if (roster.defaults?.toolPolicy) defaults.tools = { policy: roster.defaults.toolPolicy };
|
|
167
|
+
|
|
168
|
+
const targets = ["agent-teams/members"];
|
|
169
|
+
if (captain !== null) targets.push("profile/agent-preset");
|
|
170
|
+
|
|
171
|
+
const roster2 = { kind: "dsh-agent-roster/roster@2", id: rosterId, version };
|
|
172
|
+
if (roster.title) roster2.title = roster.title;
|
|
173
|
+
if (roster.description) roster2.description = roster.description;
|
|
174
|
+
if (roster.license) roster2.license = roster.license;
|
|
175
|
+
if (listOf(roster.maintainers).length > 0) roster2.maintainers = listOf(roster.maintainers);
|
|
176
|
+
if (listOf(roster.tags).length > 0) roster2.tags = listOf(roster.tags);
|
|
177
|
+
if (Object.keys(defaults).length > 0) roster2.defaults = defaults;
|
|
178
|
+
roster2.targets = targets;
|
|
179
|
+
roster2.members = members;
|
|
180
|
+
if (captain !== null) roster2.captain = captain;
|
|
181
|
+
if (skills.size > 0 || policies.size > 0) {
|
|
182
|
+
roster2.requires = {};
|
|
183
|
+
if (skills.size > 0) roster2.requires.skills = [...skills];
|
|
184
|
+
if (policies.size > 0) roster2.requires.toolPolicies = [...policies];
|
|
185
|
+
}
|
|
186
|
+
if (capabilities?.onMissing) roster2.onMissing = capabilities.onMissing;
|
|
187
|
+
|
|
188
|
+
files.push({ path: nodePath.join(ROSTERS_DIR, rosterId, "roster.yaml"), text: `${emitYaml(roster2)}\n` });
|
|
189
|
+
return { roster: roster2, files, members, roleNames, memberCount: members.length, hasCaptain: captain !== null };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function loadCapabilitiesOf(dir) {
|
|
193
|
+
for (const name of CAPABILITIES_FILE_NAMES) {
|
|
194
|
+
const file = nodePath.join(dir, name);
|
|
195
|
+
if (!existsSync(file)) continue;
|
|
196
|
+
const parsed = parseDocumentText(readFileSync(file, "utf8"), name);
|
|
197
|
+
if (!parsed.ok) return null;
|
|
198
|
+
const outcome = validateCapabilities(parsed.value, { file: name, expectedId: null });
|
|
199
|
+
return outcome.ok ? outcome.capabilities : null;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 迁移一座 v1 档案库。
|
|
206
|
+
*
|
|
207
|
+
* 不写盘时只返回计划;`write: true` 才落盘、编译、写出 `dist/`,并在最后删掉旧的 `<id>/` 目录。
|
|
208
|
+
*/
|
|
209
|
+
export function migrateLibrary(rootDir, { write = false, knownToolPolicies = null, source = {} } = {}) {
|
|
210
|
+
const report = { rootDir, write, entries: [], diagnostics: [], written: [], removed: [], compiled: [] };
|
|
211
|
+
let children = [];
|
|
212
|
+
try {
|
|
213
|
+
children = readdirSync(rootDir, { withFileTypes: true });
|
|
214
|
+
} catch (error) {
|
|
215
|
+
report.diagnostics.push({ severity: "error", code: "unreadable-dir", path: "", file: null, reason: `档案库不可读:${error?.message ?? error}` });
|
|
216
|
+
return report;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const seenRoles = new Map();
|
|
220
|
+
for (const child of children) {
|
|
221
|
+
if (!child.isDirectory() || child.isSymbolicLink()) continue;
|
|
222
|
+
if (child.name.startsWith(".") || child.name === ROLES_DIR || child.name === ROSTERS_DIR || child.name === DIST_DIR) continue;
|
|
223
|
+
const dir = nodePath.join(rootDir, child.name);
|
|
224
|
+
// v1 的档案文件名可以是 yaml / yml / json——迁移只认内容,不认扩展名。
|
|
225
|
+
if (!MEMBER_FILE_NAMES.some((name) => existsSync(nodePath.join(dir, name)))) continue;
|
|
226
|
+
const loaded = loadRosterAt(dir, { knownToolPolicies });
|
|
227
|
+
if (!loaded.ok) {
|
|
228
|
+
report.diagnostics.push({
|
|
229
|
+
severity: "warn",
|
|
230
|
+
code: "migrate-skipped",
|
|
231
|
+
path: "",
|
|
232
|
+
file: `${child.name}/roster.yaml`,
|
|
233
|
+
reason: `跳过:${loaded.diagnostics.map((item) => item.reason).join(";") || "读不出档案"}`,
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const capabilities = loadCapabilitiesOf(dir);
|
|
238
|
+
const converted = convertRoster(loaded.roster, { capabilities });
|
|
239
|
+
for (const roleName of converted.roleNames) {
|
|
240
|
+
if (seenRoles.has(roleName)) {
|
|
241
|
+
report.diagnostics.push({
|
|
242
|
+
severity: "error",
|
|
243
|
+
code: "duplicate-role",
|
|
244
|
+
path: "",
|
|
245
|
+
file: `${child.name}/roster.yaml`,
|
|
246
|
+
reason: `角色名 "${roleName}" 与 ${seenRoles.get(roleName)} 重复;v2 要求同库唯一`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
seenRoles.set(roleName, child.name);
|
|
250
|
+
}
|
|
251
|
+
report.entries.push({
|
|
252
|
+
id: loaded.roster.id,
|
|
253
|
+
version: loaded.roster.version,
|
|
254
|
+
from: `${child.name}/roster.yaml`,
|
|
255
|
+
files: converted.files,
|
|
256
|
+
memberCount: converted.memberCount,
|
|
257
|
+
hasCaptain: converted.hasCaptain,
|
|
258
|
+
capabilities: capabilities !== null,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!write) return report;
|
|
263
|
+
|
|
264
|
+
for (const entry of report.entries) {
|
|
265
|
+
for (const file of entry.files) {
|
|
266
|
+
const target = nodePath.join(rootDir, file.path);
|
|
267
|
+
mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
268
|
+
writeFileSync(target, file.text, "utf8");
|
|
269
|
+
report.written.push(file.path);
|
|
270
|
+
}
|
|
271
|
+
for (const name of CAPABILITIES_FILE_NAMES) {
|
|
272
|
+
const source = nodePath.join(rootDir, entry.id, name);
|
|
273
|
+
if (!existsSync(source)) continue;
|
|
274
|
+
const relative = nodePath.join(ROSTERS_DIR, entry.id, name);
|
|
275
|
+
const target = nodePath.join(rootDir, relative);
|
|
276
|
+
mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
277
|
+
writeFileSync(target, readFileSync(source, "utf8"), "utf8");
|
|
278
|
+
report.written.push(relative);
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const scanned = scanRoles(nodePath.join(rootDir, ROLES_DIR), { knownToolPolicies });
|
|
284
|
+
report.diagnostics.push(...scanned.diagnostics);
|
|
285
|
+
for (const entry of report.entries) {
|
|
286
|
+
const compiled = compileRoster({
|
|
287
|
+
rosterDir: nodePath.join(rootDir, ROSTERS_DIR, entry.id),
|
|
288
|
+
roles: scanned.roles,
|
|
289
|
+
expectedId: entry.id,
|
|
290
|
+
knownToolPolicies,
|
|
291
|
+
source: {},
|
|
292
|
+
});
|
|
293
|
+
if (!compiled.ok) {
|
|
294
|
+
report.diagnostics.push({
|
|
295
|
+
severity: "error",
|
|
296
|
+
code: "migrate-compile-failed",
|
|
297
|
+
path: "",
|
|
298
|
+
file: `${ROSTERS_DIR}/${entry.id}/roster.yaml`,
|
|
299
|
+
reason: compiled.diagnostics.map((item) => item.reason).join(";"),
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const name = `${compiled.compiled.source.roster}-${compiled.compiled.source.version}`;
|
|
304
|
+
const planResult = compilePlan(compiled.compiled);
|
|
305
|
+
const outputs = [{ path: nodePath.join(DIST_DIR, `${name}.json`), text: `${JSON.stringify(compiled.compiled, null, 2)}\n` }];
|
|
306
|
+
if (planResult.plan !== null) {
|
|
307
|
+
outputs.push({ path: nodePath.join(DIST_DIR, `${name}.tasks.json`), text: `${JSON.stringify(planResult.plan, null, 2)}\n` });
|
|
308
|
+
}
|
|
309
|
+
for (const output of outputs) {
|
|
310
|
+
const target = nodePath.join(rootDir, output.path);
|
|
311
|
+
mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
312
|
+
writeFileSync(target, output.text, "utf8");
|
|
313
|
+
report.written.push(output.path);
|
|
314
|
+
}
|
|
315
|
+
report.compiled.push({ id: entry.id, members: compiled.compiled.members.length, plan: planResult.plan !== null });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const entry of report.entries) {
|
|
319
|
+
const legacy = nodePath.join(rootDir, entry.id);
|
|
320
|
+
if (!existsSync(legacy) || !statSync(legacy).isDirectory()) continue;
|
|
321
|
+
// 只删「迁移前的那份目录」:v2 布局里档案在 rosters/ 下,root 上不该再有同名目录。
|
|
322
|
+
rmSync(legacy, { recursive: true, force: true });
|
|
323
|
+
report.removed.push(entry.id);
|
|
324
|
+
}
|
|
325
|
+
return report;
|
|
326
|
+
}
|