dsh-punky-swarm 0.3.6

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 (101) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +661 -0
  3. package/README.en.md +281 -0
  4. package/README.md +281 -0
  5. package/cordis.patch.yml +59 -0
  6. package/lib/acps/certs.js +310 -0
  7. package/lib/acps/discovery-client.js +283 -0
  8. package/lib/acps/registry-client.js +527 -0
  9. package/lib/acps/server.js +302 -0
  10. package/lib/aip/agent-descriptor.js +170 -0
  11. package/lib/aip/identity.js +469 -0
  12. package/lib/aip/tool-descriptor.js +93 -0
  13. package/lib/api.js +266 -0
  14. package/lib/artifact-types.js +48 -0
  15. package/lib/assembly/audit-blind-review.js +170 -0
  16. package/lib/assembly/schema.js +253 -0
  17. package/lib/assembly.js +41 -0
  18. package/lib/assets.js +103 -0
  19. package/lib/bridge/trajectory.js +202 -0
  20. package/lib/client.js +704 -0
  21. package/lib/comms/acps-bridge.js +219 -0
  22. package/lib/comms/aip-format.js +141 -0
  23. package/lib/comms/budget.js +124 -0
  24. package/lib/comms/mailbox.js +221 -0
  25. package/lib/comms/topic-runtime.js +76 -0
  26. package/lib/comms/topic.js +115 -0
  27. package/lib/discovery/filter.js +199 -0
  28. package/lib/discovery/schema.js +157 -0
  29. package/lib/discovery/service.js +251 -0
  30. package/lib/hot/config-watch.js +252 -0
  31. package/lib/index.js +433 -0
  32. package/lib/lock.js +69 -0
  33. package/lib/panel/batch-detail.js +163 -0
  34. package/lib/panel/batch-list.js +84 -0
  35. package/lib/panel/locales.js +93 -0
  36. package/lib/panel/main.js +270 -0
  37. package/lib/panel/stream.js +348 -0
  38. package/lib/panel/theme.js +103 -0
  39. package/lib/panel/widgets.js +63 -0
  40. package/lib/schema.d.ts +192 -0
  41. package/lib/schema.js +259 -0
  42. package/lib/schema.ts +398 -0
  43. package/lib/state/archive.js +174 -0
  44. package/lib/state/command-exec.js +148 -0
  45. package/lib/state/constants.js +37 -0
  46. package/lib/state/corrupt-registry.js +103 -0
  47. package/lib/state/event-types.js +65 -0
  48. package/lib/state/gates.d.ts +226 -0
  49. package/lib/state/gates.js +428 -0
  50. package/lib/state/gates.ts +348 -0
  51. package/lib/state/machine-rules.d.ts +14 -0
  52. package/lib/state/machine-rules.js +76 -0
  53. package/lib/state/machine-rules.ts +88 -0
  54. package/lib/state/machine.js +68 -0
  55. package/lib/state/resume.js +201 -0
  56. package/lib/state/schema-v3.d.ts +10 -0
  57. package/lib/state/schema-v3.js +57 -0
  58. package/lib/state/schema-v3.ts +80 -0
  59. package/lib/state/store.js +607 -0
  60. package/lib/state/task-utils.js +40 -0
  61. package/lib/tools/core.js +323 -0
  62. package/lib/tools/git-utils.js +44 -0
  63. package/lib/tools/lane-tools.js +455 -0
  64. package/lib/tools/log-tools.js +173 -0
  65. package/lib/tools/mailbox-tools.js +121 -0
  66. package/lib/tools/merge-agent.js +127 -0
  67. package/lib/tools/register.js +85 -0
  68. package/lib/tools/shared.js +32 -0
  69. package/lib/types/contracts.d.ts +277 -0
  70. package/lib/types/contracts.js +17 -0
  71. package/lib/types/contracts.ts +279 -0
  72. package/lib/verify/evidence.js +249 -0
  73. package/lib/verify/gate.js +161 -0
  74. package/lib/verify/mount.js +48 -0
  75. package/lib/verify/selector.js +101 -0
  76. package/lib/watch/lane-heartbeat.js +333 -0
  77. package/lib/wave-plan.d.ts +48 -0
  78. package/lib/wave-plan.js +473 -0
  79. package/lib/wave-plan.ts +465 -0
  80. package/package.json +71 -0
  81. package/presets/jiufeng/NOTICE +5 -0
  82. package/presets/jiufeng/agent.cordis.yml +275 -0
  83. package/presets/jiufeng/preset.yml +3 -0
  84. package/skills/jiufeng-team/SKILL.md +214 -0
  85. package/skills/jiufeng-team/references/constitution.md +62 -0
  86. package/skills/jiufeng-team/references/roles/coder.md +17 -0
  87. package/skills/jiufeng-team/references/roles/coordinator.md +13 -0
  88. package/skills/jiufeng-team/references/roles/designer.md +14 -0
  89. package/skills/jiufeng-team/references/roles/doc-manager.md +13 -0
  90. package/skills/jiufeng-team/references/roles/manager.md +22 -0
  91. package/skills/jiufeng-team/references/roles/reviewer.md +19 -0
  92. package/skills/jiufeng-team/references/roles/supervisor.md +13 -0
  93. package/skills/jiufeng-team/references/roles/tester.md +13 -0
  94. package/skills/jiufeng-team/references/templates/call-chain-matrix-template.md +121 -0
  95. package/skills/jiufeng-team/references/templates/endpoint-behavior-template.md +60 -0
  96. package/skills/jiufeng-team/references/templates/gap-list.json +33 -0
  97. package/skills/jiufeng-team/references/templates/leader-decision-pack.md +92 -0
  98. package/skills/jiufeng-team/references/templates/plan-template.md +61 -0
  99. package/skills/jiufeng-team/references/templates/success-pattern-seeds.md +126 -0
  100. package/skills/jiufeng-team/references/workflow.md +98 -0
  101. package/skills/jiufeng-team/scripts/check-v3-density.mjs +98 -0
@@ -0,0 +1,48 @@
1
+ import type { ConditionClause, ConditionInput, Layer, WavePlanDoc, WavePlanTask, WavePlanTaskInput } from './types/contracts.js';
2
+ type WaveTask = WavePlanTaskInput | WavePlanTask;
3
+ export declare const LAYERS: readonly ["plan", "exec", "audit"];
4
+ export declare const VALID_ROLES: readonly ["coordinator", "manager", "designer", "coder", "tester", "reviewer", "supervisor", "doc-manager"];
5
+ export declare const ROLE_EXTENSIONS: string[];
6
+ export declare const ROLE_WHITELIST: Set<string>;
7
+ export declare function normalizeRole(role: unknown): string | null;
8
+ export declare function defaultRoleForLayer(layer: Layer | null | undefined): string | null;
9
+ export declare const PLAN_LEAD_ROLES: Set<string>;
10
+ export declare const AUDIT_LEAD_ROLES: Set<string>;
11
+ export declare function isCClassBatch(tasks: WaveTask[], waves: string[][]): boolean;
12
+ export declare function collectRoleCompletenessWarnings(tasks: WaveTask[], waves: string[][]): {
13
+ code: string;
14
+ layer: string;
15
+ missing: string;
16
+ message: string;
17
+ }[];
18
+ export declare function topoWaves(tasks: WaveTask[]): {
19
+ waves: string[][];
20
+ order: string[];
21
+ };
22
+ export declare function normalizeCondition(cond: ConditionInput): ConditionClause[] | null;
23
+ export declare function normalizeResumeContract(t: WavePlanTaskInput): {
24
+ checkpoint: {
25
+ steps: number;
26
+ } | null;
27
+ resume: boolean;
28
+ };
29
+ export declare function normalizeTargetsContract(t: WavePlanTaskInput): {
30
+ targets: string[] | null;
31
+ targetsMarker: string | null;
32
+ };
33
+ export declare const RESUME_CLAUSE = "\u82E5\u672C lane \u5B58\u5728 checkpoint\uFF08lane_checkpoint_status \u53EF\u67E5\uFF09\uFF0C\u987B\u5148\u67E5\u8BE2 checkpoint \u5386\u53F2\uFF0C\u4ECE\u6700\u540E\u5DF2 checkpoint \u7684\u6B65\u9AA4\u4E4B\u540E\u7EE7\u7EED\uFF0C\u7981\u6B62\u91CD\u505A\u5DF2\u5B8C\u6210\u6B65\u9AA4\uFF1B\u6BCF\u5B8C\u6210\u4E00\u4E2A\u5B50\u6B65\u9AA4\u7ACB\u5373 lane_checkpoint\uFF08\u643A\u5E26 progress\uFF09\uFF0C\u7981\u6B62\u6512\u6279\u3002";
34
+ export declare function resumeClauseFor(task: WavePlanTask | null): string | null;
35
+ export declare function assembleCmd(role: string | null, skills: string[] | null | undefined, cmd: string): string;
36
+ export declare function buildWavePlan({ batchId, tasks, concurrency, team, assembly }: {
37
+ batchId: string;
38
+ tasks: WavePlanTaskInput[];
39
+ concurrency?: number;
40
+ team?: string;
41
+ assembly?: {
42
+ layers?: Record<string, {
43
+ skills?: Record<string, string[]>;
44
+ }>;
45
+ } | null;
46
+ }): WavePlanDoc;
47
+ export declare function validateWavePlan(plan: WavePlanDoc): boolean;
48
+ export {};
@@ -0,0 +1,473 @@
1
+ /*
2
+ Copyright (C) 2025-2026 Punky
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ */
17
+ // wavePlan 固定语义:DAG 拓扑分层,启动时确定并持久化,绝不在中途重算
18
+ // Tier3(dsh-punky-swarm 三层门禁):任务可声明 layer/consume/produce/outputs/role/skills,
19
+ // 建批时做三层契约静态校验(跨层引用 / 有 exec 必有 audit / 路径契约一致性 / skill 声明);
20
+ // cmd 由引擎注入 role/skill 前缀(装配可插拔,不绑定 jiufeng,见设计 §12.1/§14.2/§15.3)。
21
+ // resume 契约:task 可声明 checkpoint:{steps}(总步数)与 resume:boolean
22
+ // (崩溃后新 worker 允许参考 checkpoint 跳过已完成步骤)——校验放行 + 字段透传(与 condition 同模式,
23
+ // 默认关场景仅存元数据,消费方按 capabilities.worktree.enabled 开关生效);resumeClauseFor(task) 供
24
+ // 派发侧注入固定任务包条款(RESUME_CLAUSE)。
25
+ // Phase 2 类型化:输入面 WavePlanTaskInput(id 必填其余可选)/ 持久面 WavePlanTask(缺省落 null/[]/false),
26
+ // buildWavePlan 是两形态间的唯一规范化桥;topoWaves 产物 id 必命中 tasks(find 单点断言);
27
+ // G4:PLAN_LEAD_ROLES.has(effectiveRole(t)) 经显式空值守卫(Set.has(null) 运行期即 false,非行为缺陷)——
28
+ // 全部消解路径均为类型适配,运行期语义零变更。
29
+ import { BLIND_REVIEW_ROLES } from './assembly/schema.js';
30
+ import { isAbsPath } from './state/constants.js'; // P1-07 单点(原 :149 自有实现删除,改 import)
31
+ const SCHEMA_VERSION = 1;
32
+ export const LAYERS = ['plan', 'exec', 'audit'];
33
+ // 合法角色集合(jiufeng-team 8 角色,任务权威;大小写兼容,内部归一化小写)
34
+ export const VALID_ROLES = ['coordinator', 'manager', 'designer', 'coder', 'tester', 'reviewer', 'supervisor', 'doc-manager'];
35
+ // 装配扩展角色(盲审三角色,与 assembly/schema.js BLIND_REVIEW_ROLES 同源;装配可插拔扩展点)
36
+ export const ROLE_EXTENSIONS = BLIND_REVIEW_ROLES;
37
+ // 校验白名单 = 8 角色 ∪ 装配扩展(既有合法装配角色不误报)
38
+ export const ROLE_WHITELIST = new Set([...VALID_ROLES, ...ROLE_EXTENSIONS]);
39
+ // role 归一化:合法角色(大小写不敏感)→ 小写规范名;非法/未声明 → null
40
+ export function normalizeRole(role) {
41
+ if (role == null)
42
+ return null;
43
+ if (typeof role !== 'string')
44
+ return null;
45
+ const norm = role.trim().toLowerCase();
46
+ return ROLE_WHITELIST.has(norm) ? norm : null;
47
+ }
48
+ // layer → 缺省 role(task 未显式声明 role 时):plan→designer / audit→supervisor / exec→coder;
49
+ // 未声明 layer(generic 任务)→ null(保持现状:不注入 role 前缀,不改变既有 generic 语义)
50
+ // 参数含 undefined:调用点(effectiveRole/buildWavePlan)传 t.layer 可能为 undefined——宽松输入面,运行期语义零变更
51
+ export function defaultRoleForLayer(layer) {
52
+ if (layer === 'plan')
53
+ return 'designer';
54
+ if (layer === 'audit')
55
+ return 'supervisor';
56
+ if (layer === 'exec')
57
+ return 'coder';
58
+ return null;
59
+ }
60
+ // C 类批次角色齐备门禁(GATE_ROLE_MISSING,warning 语义:事件留痕、不阻断建批,与 GATE_ROLE_INVALID 一致;后续可配 enforce)——
61
+ // 背景:实跑证实 C 类批次(多 wave/多 lane/跨层)常缺 plan 层 designer 与 audit 层 supervisor(被 planner/auditor 或 Leader 代劳)。
62
+ // C 类形态判定(复用 wavePlan 拓扑信息):wave 数 >1 或 lane 数 >1 或存在跨 layer 依赖;单 lane 批次(非 C 类形态)不触发本门禁。
63
+ export const PLAN_LEAD_ROLES = new Set(['designer', 'coordinator', 'manager']); // plan 层牵头角色(manager 为 continuable subagent 常驻牵头)
64
+ export const AUDIT_LEAD_ROLES = new Set(['supervisor', 'doc-manager']); // audit 层牵头角色
65
+ // 跨 layer 依赖:任一任务的 deps 中存在 layer 与自身不同的依赖(DAG 跨层编排)
66
+ function hasCrossLayerDep(tasks) {
67
+ const byId = new Map(tasks.map((t) => [t.id, t]));
68
+ return tasks.some((t) => {
69
+ if (!t.layer)
70
+ return false;
71
+ return (t.deps ?? []).some((d) => {
72
+ const dep = byId.get(d);
73
+ return dep && dep.layer && dep.layer !== t.layer;
74
+ });
75
+ });
76
+ }
77
+ export function isCClassBatch(tasks, waves) {
78
+ return waves.length > 1 || tasks.length > 1 || hasCrossLayerDep(tasks);
79
+ }
80
+ // 任务有效角色(归一化):未声明 → 按 layer 缺省(plan→designer / audit→supervisor / exec→coder);
81
+ // 显式声明非法角色 → null(归一化失败,不满足齐备;由 GATE_ROLE_INVALID 另行告警)
82
+ function effectiveRole(t) {
83
+ return normalizeRole(t.role ?? defaultRoleForLayer(t.layer));
84
+ }
85
+ // C 类批次角色齐备检查:plan 层 lane 需至少一个 designer/coordinator;audit 层 lane 需至少一个 supervisor/doc-manager;
86
+ // 层不存在(无该层 lane)不检查(validateLayerContract 既有语义不变);非 C 类形态(单 lane 批次)不触发
87
+ export function collectRoleCompletenessWarnings(tasks, waves) {
88
+ if (!isCClassBatch(tasks, waves))
89
+ return [];
90
+ const warnings = [];
91
+ const planLanes = tasks.filter((t) => t.layer === 'plan');
92
+ if (planLanes.length > 0 && !planLanes.some((t) => {
93
+ // G4 空值守卫:effectiveRole 返回 string|null;Set.has(null) 运行期恒 false,显式守卫类型适配、判定结果不变
94
+ const r = effectiveRole(t);
95
+ return r !== null && PLAN_LEAD_ROLES.has(r);
96
+ })) {
97
+ warnings.push({
98
+ code: 'GATE_ROLE_MISSING',
99
+ layer: 'plan',
100
+ missing: 'designer|coordinator|manager',
101
+ message: 'C-class batch requires at least one plan lane with role designer, coordinator or manager (effective: ' + planLanes.map((t) => effectiveRole(t) ?? t.role ?? '(default)').join('/') + ')',
102
+ });
103
+ }
104
+ const auditLanes = tasks.filter((t) => t.layer === 'audit');
105
+ if (auditLanes.length > 0 && !auditLanes.some((t) => {
106
+ const r = effectiveRole(t);
107
+ return r !== null && AUDIT_LEAD_ROLES.has(r);
108
+ })) {
109
+ warnings.push({
110
+ code: 'GATE_ROLE_MISSING',
111
+ layer: 'audit',
112
+ missing: 'supervisor|doc-manager',
113
+ message: 'C-class batch requires at least one audit lane with role supervisor or doc-manager (effective: ' + auditLanes.map((t) => effectiveRole(t) ?? t.role ?? '(default)').join('/') + ')',
114
+ });
115
+ }
116
+ return warnings;
117
+ }
118
+ export function topoWaves(tasks) {
119
+ if (!Array.isArray(tasks) || tasks.length === 0) {
120
+ throw new Error('tasks must be a non-empty array');
121
+ }
122
+ const ids = new Set();
123
+ for (const t of tasks) {
124
+ if (!t || typeof t.id !== 'string' || !t.id)
125
+ throw new Error('task id required');
126
+ if (ids.has(t.id))
127
+ throw new Error('duplicate task id: ' + t.id);
128
+ ids.add(t.id);
129
+ }
130
+ const deps = new Map();
131
+ for (const t of tasks) {
132
+ const d = Array.isArray(t.deps) ? t.deps : [];
133
+ for (const dep of d) {
134
+ if (!ids.has(dep))
135
+ throw new Error('task ' + t.id + ' depends on unknown id: ' + dep);
136
+ }
137
+ deps.set(t.id, new Set(d));
138
+ }
139
+ const indegree = new Map();
140
+ for (const id of ids) {
141
+ const d = deps.get(id); // 单点断言:ids 内每 id 均经上方 deps.set(t.id, ...) 建项,get 必命中(断言纯类型层)
142
+ indegree.set(id, d.size);
143
+ }
144
+ const waves = [];
145
+ let remaining = new Set(ids);
146
+ const order = [];
147
+ let guard = ids.size * ids.size + 1;
148
+ while (remaining.size > 0) {
149
+ if (--guard < 0)
150
+ throw new Error('cycle detected in task deps');
151
+ const ready = [...remaining].filter((id) => {
152
+ const d = deps.get(id); // 单点断言:remaining ⊆ ids,get 必命中(断言纯类型层)
153
+ for (const dep of d)
154
+ if (remaining.has(dep))
155
+ return false;
156
+ return true;
157
+ });
158
+ if (ready.length === 0)
159
+ throw new Error('cycle detected in task deps');
160
+ waves.push(ready);
161
+ order.push(...ready);
162
+ for (const id of ready)
163
+ remaining.delete(id);
164
+ }
165
+ return { waves, order };
166
+ }
167
+ // condition 规范化(建批静态声明,两种等价形态 → 统一对象数组):
168
+ // condition: [{ path, exists: true }](推荐主形态)/ condition: ['plan/spec.md'](字符串简写 = 存在即满足)
169
+ // 非法(非数组 / 元素非法 / path 非空字符串 / exists 非 true)→ throw(fail-closed 拒建批)
170
+ // 谓词唯一:exists(MVP 只做存在性判定);exists: false 反向断言超出 MVP,拒建批。
171
+ export function normalizeCondition(cond) {
172
+ if (cond == null)
173
+ return null;
174
+ if (!Array.isArray(cond))
175
+ throw new Error('condition must be an array (of {path, exists} objects or path strings)');
176
+ const out = [];
177
+ // 单点断言:Array.isArray 后 cond 为 ConditionClause[] | string[] 之一(元素并集形态),运行期遍历不变
178
+ for (const c of cond) {
179
+ if (typeof c === 'string') {
180
+ if (!c.trim())
181
+ throw new Error('condition path must be a non-empty string');
182
+ out.push({ path: c, exists: true });
183
+ }
184
+ else if (c && typeof c === 'object' && !Array.isArray(c)) {
185
+ if (typeof c.path !== 'string' || !c.path.trim())
186
+ throw new Error('condition path must be a non-empty string');
187
+ if (c.exists !== true)
188
+ throw new Error('condition exists must be true (MVP supports existence predicate only)');
189
+ out.push({ path: c.path, exists: true });
190
+ }
191
+ else {
192
+ throw new Error('condition entries must be {path, exists} objects or path strings');
193
+ }
194
+ }
195
+ return out.length ? out : null; // 空数组 = 无条件(恒满足)
196
+ }
197
+ // condition 路径契约(与 consume 同源):相对路径必须在本批次产物根内(plan/|exec/|audit/ 前缀);artifacts/ 跨批次先禁;绝对路径放行
198
+ // 对所有声明 condition 的任务生效(含 generic 批次),建批入口统一校验(fail-closed 拒建批)
199
+ function checkConditionPaths(t, cond) {
200
+ for (const c of cond ?? []) {
201
+ const p = c.path;
202
+ if (isAbsPath(p))
203
+ continue;
204
+ if (p.startsWith('artifacts/'))
205
+ throw new Error('task ' + t.id + ' condition cross-batch reference is disabled in MVP (N6): ' + p);
206
+ if (!/^(plan|exec|audit)\//.test(p))
207
+ throw new Error('task ' + t.id + ' condition must be under plan/|exec/|audit/ or absolute: ' + p);
208
+ }
209
+ }
210
+ // resume 契约字段规范化(建批静态声明,与 condition 同模式):
211
+ // checkpoint: { steps: number }(声明本 lane 总步数,供 progress 校验与任务包注入;缺省 null)
212
+ // resume: boolean(声明"崩溃后新 worker 允许参考 checkpoint 跳过已完成步骤";缺省 false = 现状,行为不变)
213
+ // 非法(steps 非正整数 / resume 非 boolean)→ throw(fail-closed 拒建批)
214
+ export function normalizeResumeContract(t) {
215
+ let checkpoint = null;
216
+ if (t.checkpoint != null) {
217
+ if (typeof t.checkpoint !== 'object' || Array.isArray(t.checkpoint)) {
218
+ throw new Error('task ' + t.id + ' checkpoint must be an object { steps: number }');
219
+ }
220
+ const steps = t.checkpoint.steps;
221
+ if (!Number.isInteger(steps) || steps < 1) {
222
+ throw new Error('task ' + t.id + ' checkpoint.steps must be a positive integer');
223
+ }
224
+ checkpoint = { steps };
225
+ }
226
+ const resume = t.resume;
227
+ if (resume != null && typeof resume !== 'boolean') {
228
+ throw new Error('task ' + t.id + ' resume must be a boolean');
229
+ }
230
+ return { checkpoint, resume: resume === true };
231
+ }
232
+ // targets/targetsMarker 契约规范化(O2 targets 声明契约,与 condition/resume 同模式):
233
+ // targets: string[]——批次产物根外目标文件(exec worker 承诺「修改/生成」的既有文件)的绝对路径数组;
234
+ // 相对路径建批拒绝(fail-closed,防把产物相对路径误当 targets);
235
+ // targetsMarker: string|null——可选内容声明标记(缺省 null = 纯 mtime 校验;非空时目标文件含独立行
236
+ // `targets-claimed: true` 即视为已变更,见 gates.ts checkTargetsGate marker 逃生路径)。
237
+ // 非法(targets 非 string 数组 / 空 / 含非绝对路径 / targetsMarker 非 string|null)→ throw(fail-closed 拒建批)。
238
+ export function normalizeTargetsContract(t) {
239
+ let targets = null;
240
+ if (t.targets != null) {
241
+ if (!Array.isArray(t.targets) || t.targets.length === 0) {
242
+ throw new Error('task ' + t.id + ' targets must be a non-empty string array');
243
+ }
244
+ for (const p of t.targets) {
245
+ if (typeof p !== 'string' || !p.trim()) {
246
+ throw new Error('task ' + t.id + ' targets must be non-empty strings');
247
+ }
248
+ if (!isAbsPath(p)) {
249
+ throw new Error('task ' + t.id + ' targets must be absolute paths (fail-closed, got: ' + p + ')');
250
+ }
251
+ }
252
+ targets = [...t.targets];
253
+ }
254
+ let targetsMarker = null;
255
+ if (t.targetsMarker != null) {
256
+ if (typeof t.targetsMarker !== 'string') {
257
+ throw new Error('task ' + t.id + ' targetsMarker must be a string or null');
258
+ }
259
+ targetsMarker = t.targetsMarker;
260
+ }
261
+ return { targets, targetsMarker };
262
+ }
263
+ // 任务包 resume 契约固定条款:resume: true 时注入 worker 派发提示词——
264
+ // 新 worker 先查 checkpoint 历史(lane_checkpoint_status),从最后已 checkpoint 步骤之后继续,禁止重做;
265
+ // 每完成一个子步骤立即 lane_checkpoint(携带 progress),禁止攒批。
266
+ export const RESUME_CLAUSE = '若本 lane 存在 checkpoint(lane_checkpoint_status 可查),须先查询 checkpoint 历史,从最后已 checkpoint 的步骤之后继续,禁止重做已完成步骤;每完成一个子步骤立即 lane_checkpoint(携带 progress),禁止攒批。';
267
+ // 任务包 resume 条款注入:task.resume === true → 返回固定条款文本(注入派发提示词);否则 null(不注入,现状)
268
+ export function resumeClauseFor(task) {
269
+ return task?.resume === true ? RESUME_CLAUSE : null;
270
+ }
271
+ // 三层契约静态校验(仅当任一 lane 声明 layer 时启用;generic 批次跳过)
272
+ function validateLayerContract(tasks) {
273
+ const plan = tasks.filter((t) => t.layer === 'plan');
274
+ const exec = tasks.filter((t) => t.layer === 'exec');
275
+ const audit = tasks.filter((t) => t.layer === 'audit');
276
+ const used = tasks.some((t) => t.layer != null && LAYERS.includes(t.layer));
277
+ if (!used)
278
+ return;
279
+ if (exec.length === 0 && audit.length === 0 && plan.length === 0) {
280
+ throw new Error('three-tier: layer must be one of plan/exec/audit');
281
+ }
282
+ // 有 exec 必有 audit(设计 §3.3/§5.2)
283
+ if (exec.length > 0 && audit.length === 0) {
284
+ throw new Error('three-tier: exec layers require at least one audit lane');
285
+ }
286
+ // 路径契约一致性(N6/§15.3):相对路径必须在本批次产物根内(plan/exec/audit 前缀);跨批次 artifacts/ 先禁;绝对路径放行(N2 自由开放,由运行时 gate 校验存在性)
287
+ const checkPaths = (t, field) => {
288
+ for (const p of t[field] ?? []) {
289
+ if (typeof p !== 'string' || !p.trim())
290
+ throw new Error('task ' + t.id + ' ' + field + ' must be non-empty strings');
291
+ if (isAbsPath(p))
292
+ continue;
293
+ if (p.startsWith('artifacts/'))
294
+ throw new Error('task ' + t.id + ' ' + field + ' cross-batch reference is disabled in MVP (N6): ' + p);
295
+ if (!/^(plan|exec|audit)\//.test(p))
296
+ throw new Error('task ' + t.id + ' ' + field + ' must be under plan/|exec/|audit/ or absolute: ' + p);
297
+ }
298
+ };
299
+ for (const t of tasks) {
300
+ if (t.layer != null && LAYERS.includes(t.layer)) {
301
+ checkPaths(t, 'consume');
302
+ checkPaths(t, 'produce');
303
+ checkPaths(t, 'outputs');
304
+ // condition 结构/路径校验在建批入口对所有任务统一做(见 buildWavePlan),此处不重复
305
+ }
306
+ }
307
+ // 跨层引用:exec.consume 的相对 plan/ 路径必须由 plan 层 produce 提供(设计 §3.3 建批静态校验)
308
+ const planProduces = new Set(plan.flatMap((t) => t.produce ?? []));
309
+ for (const t of exec) {
310
+ for (const p of t.consume ?? []) {
311
+ if (p.startsWith('plan/') && !planProduces.has(p)) {
312
+ throw new Error('task ' + t.id + ' consumes "' + p + '" which is not produced by any plan lane');
313
+ }
314
+ }
315
+ }
316
+ // skill 声明校验(N4/§15.3):非空字符串;真实存在性由装配/技能库在注册侧保证
317
+ for (const t of tasks) {
318
+ if (t.skills != null) {
319
+ if (!Array.isArray(t.skills) || t.skills.length === 0 || t.skills.some((s) => typeof s !== 'string' || !s.trim())) {
320
+ throw new Error('task ' + t.id + ' skills must be a non-empty string array');
321
+ }
322
+ }
323
+ if (t.role != null && (typeof t.role !== 'string' || !t.role.trim())) {
324
+ throw new Error('task ' + t.id + ' role must be a non-empty string');
325
+ }
326
+ }
327
+ }
328
+ // 引擎注入 cmd 前缀:按 role 契约 + 装配的 skill 能力(设计 §12.1/§14.2),Leader 只写任务内容
329
+ export function assembleCmd(role, skills, cmd) {
330
+ const parts = [];
331
+ if (role)
332
+ parts.push('[role=' + role + ']');
333
+ if (Array.isArray(skills) && skills.length)
334
+ parts.push('[skills=' + skills.join(',') + ']');
335
+ return parts.length ? parts.join(' ') + ' ' + (cmd ?? '') : (cmd ?? '');
336
+ }
337
+ export function buildWavePlan({ batchId, tasks, concurrency = 5, team = 'generic', assembly }) {
338
+ if (!batchId || typeof batchId !== 'string')
339
+ throw new Error('batchId required');
340
+ const { waves } = topoWaves(tasks);
341
+ validateLayerContract(tasks);
342
+ // role 集合校验(GATE_ROLE_INVALID,warning 语义:事件留痕、不阻断建批、保持兼容)——
343
+ // 仅「显式声明且非空、但不在合法集合」的 role 触发告警;未声明(走默认值)与归一化后合法的角色不告警
344
+ const warnings = [];
345
+ for (const t of tasks) {
346
+ if (typeof t.role === 'string' && t.role.trim() && normalizeRole(t.role) === null) {
347
+ warnings.push({
348
+ code: 'GATE_ROLE_INVALID',
349
+ task: t.id,
350
+ role: t.role,
351
+ message: 'task ' + t.id + ' role "' + t.role + '" is not a valid role (' + [...VALID_ROLES, ...ROLE_EXTENSIONS].join('/') + '); kept as-is for compatibility, layer default applies only when role is omitted',
352
+ });
353
+ }
354
+ }
355
+ // C 类批次角色齐备门禁(GATE_ROLE_MISSING,warning 语义)——并入同一收集循环/同一返回结构;
356
+ // 仅 C 类形态(多 wave/多 lane/跨层依赖)且对应层存在时检查;单 lane 批次不触发
357
+ warnings.push(...collectRoleCompletenessWarnings(tasks, waves));
358
+ const wavePlan = waves.map((ids, idx) => ({
359
+ wave: idx + 1,
360
+ tasks: ids.map((id) => {
361
+ // 单点断言:ids 来自 topoWaves(tasks)(同一 tasks 的拓扑序),find 必命中(断言纯类型层)
362
+ const t = tasks.find((x) => x.id === id);
363
+ const layer = t.layer ?? undefined;
364
+ // 默认值修正:task 未显式声明 role → 按 layer 取缺省(plan→designer / audit→supervisor / exec→coder;generic 无 layer → null 现状)
365
+ const rawRole = t.role ?? defaultRoleForLayer(layer);
366
+ // 大小写归一化:合法角色(Designer→designer)存小写规范名;非法角色保留原值(兼容,GATE_ROLE_INVALID 告警暴露)
367
+ const role = normalizeRole(rawRole) ?? rawRole;
368
+ let skills = t.skills;
369
+ // 装配补全(可插拔):未显式声明 skills 时,按 team 装配表的 role → skills 补全(assembly 可选)
370
+ // layer 为 Layer|undefined:undefined 时索引装配表 miss(JS 属性键字符串化语义一致),断言仅类型层
371
+ if (assembly && role && skills === undefined) {
372
+ const entry = assembly?.layers?.[layer]?.skills?.[role];
373
+ if (entry)
374
+ skills = entry;
375
+ }
376
+ // condition 结构 + 路径校验(对所有任务,含 generic),fail-closed 拒建批
377
+ const condition = normalizeCondition(t.condition);
378
+ checkConditionPaths(t, condition);
379
+ // resume 契约字段校验 + 透传(checkpoint{steps}/resume 布尔;默认关场景仅存元数据,消费方按开关生效)
380
+ const resumeContract = normalizeResumeContract(t);
381
+ // targets/targetsMarker 契约校验 + 透传(O2:绝对路径 fail-closed 拒建批;未声明 = null = 零感知,
382
+ // 仿 condition 可选字段模式,不升 batch schema 版本)
383
+ const targetsContract = normalizeTargetsContract(t);
384
+ return {
385
+ id: t.id,
386
+ cmd: assembleCmd(role, skills, t.cmd ?? ''),
387
+ deps: Array.isArray(t.deps) ? [...t.deps] : [],
388
+ model: t.model ?? null,
389
+ tools: Array.isArray(t.tools) ? [...t.tools] : null,
390
+ layer: t.layer ?? null,
391
+ role: role ?? null,
392
+ skills: skills ? [...skills] : null,
393
+ consume: Array.isArray(t.consume) ? [...t.consume] : null,
394
+ produce: Array.isArray(t.produce) ? [...t.produce] : null,
395
+ outputs: Array.isArray(t.outputs) ? [...t.outputs] : null,
396
+ condition, // lane 条件(统一对象数组形态;缺省 null = 恒满足)
397
+ checkpoint: resumeContract.checkpoint, // { steps } | null(总步数声明,供 progress 校验与任务包注入)
398
+ resume: resumeContract.resume, // boolean(缺省 false = 现状,行为不变)
399
+ targets: targetsContract.targets, // string[] | null(绝对路径目标文件声明;未声明 null = 零感知)
400
+ targetsMarker: targetsContract.targetsMarker, // string | null(内容声明标记;缺省 null = 纯 mtime 校验)
401
+ };
402
+ }),
403
+ }));
404
+ const concurrencyN = Number.isInteger(concurrency) && concurrency > 0 ? concurrency : 5;
405
+ return {
406
+ schema: SCHEMA_VERSION,
407
+ batchId,
408
+ team: team || 'generic',
409
+ wavePlan,
410
+ concurrency: concurrencyN,
411
+ warnings, // role 校验告警(GATE_ROLE_INVALID,warning 语义:不阻断建批;事件留痕由调用方落批次)
412
+ };
413
+ }
414
+ export function validateWavePlan(plan) {
415
+ if (!plan || typeof plan !== 'object')
416
+ throw new Error('wavePlan must be an object');
417
+ if (plan.schema !== SCHEMA_VERSION)
418
+ throw new Error('unsupported wavePlan schema: ' + String(plan.schema));
419
+ if (!plan.batchId || typeof plan.batchId !== 'string')
420
+ throw new Error('wavePlan.batchId required');
421
+ if (!Array.isArray(plan.wavePlan) || plan.wavePlan.length === 0)
422
+ throw new Error('wavePlan.wavePlan required');
423
+ const seen = new Set();
424
+ for (const w of plan.wavePlan) {
425
+ if (!Number.isInteger(w.wave) || w.wave < 1)
426
+ throw new Error('wave number invalid');
427
+ if (!Array.isArray(w.tasks))
428
+ throw new Error('wave tasks must be an array');
429
+ for (const t of w.tasks) {
430
+ if (!t || typeof t.id !== 'string' || seen.has(t.id))
431
+ throw new Error('task id invalid/duplicate');
432
+ seen.add(t.id);
433
+ if (t.model !== null && typeof t.model !== 'string')
434
+ throw new Error('task model must be string or null');
435
+ if (t.cmd !== undefined && typeof t.cmd !== 'string')
436
+ throw new Error('task cmd must be string');
437
+ if (t.tools !== undefined && t.tools !== null && (!Array.isArray(t.tools) || t.tools.some((x) => typeof x !== 'string')))
438
+ throw new Error('task tools must be string array or null');
439
+ if (t.layer != null && !LAYERS.includes(t.layer))
440
+ throw new Error('task layer invalid: ' + t.layer);
441
+ for (const f of ['consume', 'produce', 'outputs', 'skills']) {
442
+ if (t[f] != null && (!Array.isArray(t[f]) || t[f].some((x) => typeof x !== 'string')))
443
+ throw new Error('task ' + f + ' must be string array');
444
+ }
445
+ // condition 规范化形态校验(建批后 wavePlan JSON 应为对象数组或 null)
446
+ if (t.condition != null) {
447
+ if (!Array.isArray(t.condition) || t.condition.some((c) => !c || typeof c !== 'object' || Array.isArray(c) || typeof c.path !== 'string' || c.exists !== true)) {
448
+ throw new Error('task condition must be [{path, exists: true}, ...] array or null');
449
+ }
450
+ checkConditionPaths(t, t.condition);
451
+ }
452
+ // checkpoint/resume 形态校验(建批后 wavePlan JSON 应为 {steps} | null / boolean)
453
+ if (t.checkpoint != null && (typeof t.checkpoint !== 'object' || Array.isArray(t.checkpoint) || !Number.isInteger(t.checkpoint.steps) || t.checkpoint.steps < 1)) {
454
+ throw new Error('task checkpoint must be { steps: positive int } or null');
455
+ }
456
+ if (t.resume != null && typeof t.resume !== 'boolean') {
457
+ throw new Error('task resume must be boolean');
458
+ }
459
+ // targets/targetsMarker 形态校验(O2,与建批规范化同语义,防伪造/篡改):
460
+ // targets 为绝对路径 string 数组(相对路径 fail-closed 拒);targetsMarker 为 string|null
461
+ if (t.targets != null && (!Array.isArray(t.targets) || t.targets.length === 0 || t.targets.some((x) => typeof x !== 'string' || !x.trim() || !isAbsPath(x)))) {
462
+ throw new Error('task targets must be a non-empty string array of absolute paths');
463
+ }
464
+ if (t.targetsMarker != null && typeof t.targetsMarker !== 'string') {
465
+ throw new Error('task targetsMarker must be string or null');
466
+ }
467
+ }
468
+ }
469
+ const flat = plan.wavePlan.flatMap((w) => w.tasks);
470
+ topoWaves(flat);
471
+ validateLayerContract(flat);
472
+ return true;
473
+ }