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,348 @@
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
+
18
+ // Gates:Tier3 层间门禁(设计 §3.3/§四/§五/§15)——entry(consume 前置)/ exit(outputs/produce 前置 + Plan 契约 checkPlanContract)/ complete(audit 验收前置)
19
+ // 纯函数闭包工厂:createGates(root) 注入 root;门禁函数签名 (sessionId, batchId, batch, lane)——batch 对象显式传入,不持有状态
20
+ // Phase 2 类型化:wavePlan 任务属性访问经 taskOf 单点断言(findTask 保持 JS,返回 object|null → WavePlanTask,
21
+ // 断言依据:findTask 语义即"按 id 定位 buildWavePlan 持久形态任务");Batch/批事件判别联合收窄(零断言消解 TS2339);
22
+ // gate 函数返回类型沿用结构推断(消费方 store.js 等为 JS,无类型约束)——运行期语义零变更(断言纯类型层)。
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import * as schema from '../schema.js';
26
+ import { runCommand } from './command-exec.js';
27
+ // P1-07 单点收敛:SESSION_RE/isAbsPath 定义迁至 constants.js;isAbsPath 保持导出
28
+ // (store.js:24 等既有消费方 import { isAbsPath } from './gates.js' 不受影响)。
29
+ import { SESSION_RE, isAbsPath } from './constants.js';
30
+ // P1-04 单点:findTask 收敛至 task-utils.js(原 :108 本地定义删除,调用点不变)
31
+ import { findTask } from './task-utils.js';
32
+ // R-07 读端收敛:e.type 比较改引 EVT 常量单点(member.settled 读端 :117)
33
+ import * as EVT from './event-types.js';
34
+ import type { Batch, Layer, WavePlanTask } from '../types/contracts.js';
35
+ export { isAbsPath };
36
+
37
+ // 局部类型化包装:findTask(task-utils.js,保持 JS)返回 object|null → 属性访问全报"属性不存在"。
38
+ // taskOf 为唯一断言点:findTask 遍历 batch.wavePlan[].tasks 按 t.id === lane 返回 buildWavePlan 持久形态任务
39
+ // → WavePlanTask 类型成立(运行期零变化,断言纯类型层)。
40
+ function taskOf(batch: Batch | null, lane: string): WavePlanTask | null {
41
+ return findTask(batch, lane) as WavePlanTask | null;
42
+ }
43
+
44
+ // 任务数组字段(gates 只读 consume/produce/outputs 三类,t[field] 索引合法化)
45
+ type TaskArrayField = 'consume' | 'produce' | 'outputs';
46
+
47
+ // targets 门禁(O2)marker 逃生声明标记:目标文件含独立行 `targets-claimed: true` 即视为已变更(跳过 mtime 比对)。
48
+ // 行首锚定独立行正则(仿 needHuman 独立行模式)——内嵌/注释/非行首不误判(NFR3)。
49
+ export const TARGETS_CLAIMED_RE = /^targets-claimed:\s*true$/m;
50
+
51
+ // needHuman:audit 产物人工裁决声明检测(纯函数,与 checkExitGate 同源文件解析)——
52
+ // 产物含独立行 `needHuman: true`(正则 /^needHuman:\s*true$/m)即声明人工裁决需求;
53
+ // 缺失/空文件/目录跳过(零感知:缺产物由 exit gate 既有语义拒,本函数不补刀)
54
+ export function detectNeedHuman(artifactsDir: string, producePaths: string[]) {
55
+ if (!Array.isArray(producePaths) || producePaths.length === 0) return { declared: false, path: null };
56
+ for (const p of producePaths) {
57
+ const abs = isAbsPath(p) ? p : path.join(artifactsDir, p);
58
+ let content;
59
+ try {
60
+ const st = fs.statSync(abs);
61
+ if (st.isDirectory() || st.size === 0) continue;
62
+ content = fs.readFileSync(abs, 'utf8');
63
+ } catch { continue; }
64
+ if (/^needHuman:\s*true$/m.test(content)) return { declared: true, path: p };
65
+ }
66
+ return { declared: false, path: null };
67
+ }
68
+
69
+ // 命令 gate(V1):产物独立行声明 `gate: <命令>`(行首锚定正则,与 needHuman 独立行模式同源)
70
+ // 内嵌/注释/非行首不误判;`gate: false` 视为显式禁用声明不计入(spec G1);空命令(gate: 后无内容)不计入(spec G8)
71
+ export const GATE_LINE_RE = /^gate:\s*(.+)$/gm;
72
+
73
+ // 声明解析纯函数(同 detectNeedHuman 模式:缺失/空/目录跳过、零感知):
74
+ // 遍历 produce/outputs 并集 → 收集所有命中行命令(保序)→ { declared, commands, path }
75
+ // 全部命中行为空/禁用 → declared=false(零感知,C2/G8)
76
+ export function detectGate(artifactsDir: string, paths: string[]) {
77
+ if (!Array.isArray(paths) || paths.length === 0) return { declared: false, commands: [], path: null };
78
+ const commands = [];
79
+ let declaredPath: string | null = null;
80
+ for (const p of paths) {
81
+ const abs = isAbsPath(p) ? p : path.join(artifactsDir, p);
82
+ let content;
83
+ try {
84
+ const st = fs.statSync(abs);
85
+ if (st.isDirectory() || st.size === 0) continue;
86
+ content = fs.readFileSync(abs, 'utf8');
87
+ } catch { continue; }
88
+ GATE_LINE_RE.lastIndex = 0; // /g 正则复用防 lastIndex 泄漏
89
+ let m;
90
+ while ((m = GATE_LINE_RE.exec(content)) !== null) {
91
+ const cmd = m[1].trim();
92
+ if (cmd.length === 0 || cmd === 'false') continue; // 空命令 / gate: false(禁用声明)不计入
93
+ commands.push(cmd);
94
+ if (!declaredPath) declaredPath = p;
95
+ }
96
+ }
97
+ return { declared: commands.length > 0, commands, path: declaredPath };
98
+ }
99
+
100
+ export function createGates(root: string) {
101
+ const sessionsDir = path.join(root, 'sessions');
102
+
103
+ function sessionDir(sessionId: string) {
104
+ if (!SESSION_RE.test(sessionId)) throw new Error('invalid sessionId: ' + sessionId);
105
+ return path.join(sessionsDir, sessionId);
106
+ }
107
+ function artifactsDirOf(sessionId: string, batchId: string) {
108
+ return path.join(sessionDir(sessionId), 'artifacts', batchId);
109
+ }
110
+ function batchFile(sessionId: string, batchId: string) {
111
+ if (!SESSION_RE.test(batchId)) throw new Error('invalid batchId');
112
+ return path.join(sessionDir(sessionId), 'batches', batchId + '.json');
113
+ }
114
+ function readBatch(sessionId: string, batchId: string): Batch | null {
115
+ const file = batchFile(sessionId, batchId);
116
+ if (!fs.existsSync(file)) return null;
117
+ try {
118
+ // 单点断言:批次 JSON 由 store.createBatch 写入(Batch 契约同源形态),JSON.parse 产物断言为 Batch
119
+ return JSON.parse(fs.readFileSync(file, 'utf8')) as Batch;
120
+ } catch {
121
+ // 损坏批次隔离(v2-node-robustness ②,AC-1 读路径不 throw):损坏 → null(登记在 store 旁路清单,本文件不重复)
122
+ return null;
123
+ }
124
+ }
125
+
126
+ // ---- Tier3 门禁辅助 ----
127
+ // lane 启动时间(O2 targets 门禁基准):events 末尾向前反查最近一条 `member.settled` 且 lane 匹配且
128
+ // to==='running' 的事件 ts(ISO)——返工(review→running)会 push 新的 running 结算事件 → 基准重置(重新计时);
129
+ // 无 → 回退 batch.createdAt(防御;正常 merged 必有 running 事件)。遍历模式仿 store.js lastActiveAtOf。
130
+ // e.type === EVT 常量值('member.settled')在 BatchEvent 判别联合下收窄;兜底分支字段 unknown 与 === 比较不受影响
131
+ function laneStartedAt(batch: Batch, lane: string): string {
132
+ const evs = batch.events ?? [];
133
+ for (let i = evs.length - 1; i >= 0; i--) {
134
+ const e = evs[i];
135
+ if (e && e.type === EVT.EVT_MEMBER_SETTLED && e.lane === lane && e.to === 'running' && e.ts) return e.ts;
136
+ }
137
+ return batch.createdAt;
138
+ }
139
+ function resolveArtifact(sessionId: string, batchId: string, rel: string) {
140
+ return isAbsPath(rel) ? rel : path.join(artifactsDirOf(sessionId, batchId), rel);
141
+ }
142
+ function fileExistsNonEmpty(p: string) {
143
+ try {
144
+ const st = fs.statSync(p);
145
+ return st.isDirectory() || st.size > 0;
146
+ } catch { return false; }
147
+ }
148
+ // Entry Gate(设计 §四):exec 与 audit lane 派发前 consume 必须全部存在且非空
149
+ // P2-03:条件覆盖 audit 层(原仅 exec——audit lane 声明 consume 时缺产物同样应拒派 GATE_ENTRY_MISSING)
150
+ function checkEntryGate(sessionId: string, batchId: string, batch: Batch, lane: string) {
151
+ const t = taskOf(batch, lane);
152
+ if (!t || (t.layer !== 'exec' && t.layer !== 'audit') || !Array.isArray(t.consume) || t.consume.length === 0) return { ok: true };
153
+ const missing = t.consume.filter((p: string) => !fileExistsNonEmpty(resolveArtifact(sessionId, batchId, p)));
154
+ return missing.length ? { ok: false, code: 'GATE_ENTRY_MISSING', missing } : { ok: true };
155
+ }
156
+ // Plan 契约产物结构校验:仅 plan 产物——spec 必填章节 + task-tree JSON 可解析
157
+ function checkPlanContract(sessionId: string, batchId: string, batch: Batch, lane: string) {
158
+ const t = taskOf(batch, lane);
159
+ if (!t || t.layer !== 'plan' || !Array.isArray(t.produce)) return { ok: true };
160
+ const problems = [];
161
+ for (const p of t.produce) {
162
+ const abs = resolveArtifact(sessionId, batchId, p);
163
+ if (!fileExistsNonEmpty(abs)) { problems.push(p + ' missing'); continue; }
164
+ const content = fs.readFileSync(abs, 'utf8');
165
+ if (p.endsWith('spec.md')) {
166
+ if (!content.includes('## 验收标准')) problems.push(p + ' lacks "## 验收标准"');
167
+ if (!content.includes('## 约束')) problems.push(p + ' lacks "## 约束"');
168
+ } else if (p.endsWith('.json')) {
169
+ try { JSON.parse(content); } catch { problems.push(p + ' invalid JSON'); }
170
+ }
171
+ }
172
+ return problems.length ? { ok: false, code: 'GATE_PLAN_CONTRACT', problems } : { ok: true };
173
+ }
174
+ // Exit Gate(设计 §五):exec→outputs 存在;audit→produce 存在;plan→Plan 契约
175
+ function checkExitGate(sessionId: string, batchId: string, batch: Batch, lane: string) {
176
+ const t = taskOf(batch, lane);
177
+ if (!t || !t.layer) return { ok: true };
178
+ if (t.layer === 'plan') return checkPlanContract(sessionId, batchId, batch, lane);
179
+ const field = t.layer === 'exec' ? 'outputs' : (t.layer === 'audit' ? 'produce' : null);
180
+ if (!field || !Array.isArray(t[field]) || t[field].length === 0) return { ok: true };
181
+ const missing = t[field].filter((p: string) => !fileExistsNonEmpty(resolveArtifact(sessionId, batchId, p)));
182
+ return missing.length ? { ok: false, code: 'GATE_EXIT_MISSING_' + t.layer.toUpperCase(), missing } : { ok: true };
183
+ }
184
+ // needHuman Gate(复用 review 态挂起):audit lane 产物声明 needHuman →
185
+ // review→merged 前置人工裁决证据(note 契约 `human:<裁决人>:<时间>:<结论>`,如 human:user@2026-08-21:accept)
186
+ // 缺证据 → GATE_NEEDHUMAN_PENDING;conflict 驳回不强制(评审驳回/人工否决语义);
187
+ // 未声明/非 audit lane → 零感知({ ok: true, declared: false })
188
+ function checkNeedHumanGate(sessionId: string, batchId: string, batch: Batch, lane: string, note?: string | null) {
189
+ const t = taskOf(batch, lane);
190
+ if (!t || t.layer !== 'audit' || !Array.isArray(t.produce) || t.produce.length === 0) {
191
+ return { ok: true, declared: false, path: null };
192
+ }
193
+ const det = detectNeedHuman(artifactsDirOf(sessionId, batchId), t.produce);
194
+ if (!det.declared) return { ok: true, declared: false, path: null };
195
+ const evidence = typeof note === 'string' ? (note.match(/^human:.+/m) ?? [null])[0] : null;
196
+ if (evidence) return { ok: true, declared: true, path: det.path, evidence };
197
+ return {
198
+ ok: false, code: 'GATE_NEEDHUMAN_PENDING', declared: true, path: det.path,
199
+ message: 'audit lane 声明 needHuman,须 Manager 转达人工裁决(merged 需 note 含 human: 证据 / conflict 驳回)',
200
+ };
201
+ }
202
+ // 命令 gate(V1,设计 §组件 2 / spec G2-G10、C5-C7):exec 层产物声明行 `gate: <命令>` → merged 前置确定性执行 → 退出码判定
203
+ // 签名与既有门禁一致(batch 显式传入,不持有状态);执行器经 deps.runCommand 注入(DI,默认真实执行器,测试可注入 mock)
204
+ // cwd 契约(C7):lane worktree 根(若已建)→ GATE_REPO_ROOT(批次 repo 根配置,V1 env 注入)→ artifacts 根兜底
205
+ function commandCwd(sessionId: string, batchId: string, lane: string) {
206
+ const wt = path.join(sessionDir(sessionId), 'worktrees', batchId, lane);
207
+ try { if (fs.statSync(wt).isDirectory()) return wt; } catch { /* 未建 worktree,继续 */ }
208
+ const repoRoot = process.env.GATE_REPO_ROOT;
209
+ if (repoRoot) { try { if (fs.statSync(repoRoot).isDirectory()) return repoRoot; } catch { /* 无效则跳过 */ } }
210
+ return artifactsDirOf(sessionId, batchId);
211
+ }
212
+ function checkCommandGate(
213
+ sessionId: string, batchId: string, batch: Batch, lane: string,
214
+ deps: { runCommand?: typeof runCommand } = {},
215
+ ) {
216
+ // 总开关(GATE_ENABLED=false → 全部零感知,应急逃生阀)
217
+ if (String(process.env.GATE_ENABLED).toLowerCase() === 'false') return { ok: true, declared: false };
218
+ const t = taskOf(batch, lane);
219
+ if (!t || t.layer !== 'exec') return { ok: true, declared: false }; // 仅约束 exec 层验证声明(D-005)
220
+ // produce ∪ outputs 并集(去重保序)
221
+ const fields = [...new Set([...(t.produce ?? []), ...(t.outputs ?? [])])];
222
+ const det = detectGate(artifactsDirOf(sessionId, batchId), fields);
223
+ if (!det.declared) return { ok: true, declared: false }; // 未声明 gate → 零感知(C2/G10)
224
+ if (det.commands.length === 0) {
225
+ return { ok: false, code: 'GATE_EXIT_NO_COMMAND', command: null, exitCode: null, declared: true, needHumanEscalation: false, detail: 'gate 行命中但命令解析为空(防空命令假通过)' };
226
+ }
227
+ const run = deps.runCommand ?? runCommand;
228
+ const results: Array<{ command: string; exitCode: number | null; durationMs: number }> = [];
229
+ let failed: { code: string; command: string; exitCode: number | null; detail: string } | null = null;
230
+ let outputTruncated = false;
231
+ for (const command of det.commands) {
232
+ const r = run({ command, cwd: commandCwd(sessionId, batchId, lane) });
233
+ if (r.truncated) outputTruncated = true;
234
+ const res = { command, exitCode: r.exitCode ?? null, durationMs: r.durationMs ?? 0 };
235
+ results.push(res);
236
+ if (r.forbidden) { failed = { code: 'GATE_EXIT_FORBIDDEN', command, exitCode: null, detail: '黑名单命中(只读守卫,不执行)' }; break; }
237
+ if (r.timedOut) { failed = { code: 'GATE_EXIT_TIMEOUT', command, exitCode: res.exitCode, detail: '命令超时(重试后仍超时)' }; break; }
238
+ if (r.error && r.error.startsWith('GATE_EXIT_SPAWN_FAIL')) { failed = { code: 'GATE_EXIT_SPAWN_FAIL', command, exitCode: null, detail: r.error }; break; }
239
+ if (r.error === 'GATE_EXIT_NO_COMMAND') { failed = { code: 'GATE_EXIT_NO_COMMAND', command, exitCode: null, detail: r.error }; break; }
240
+ if (!r.ok) { failed = { code: 'GATE_EXIT_NONZERO', command, exitCode: res.exitCode, detail: '退出码 ' + res.exitCode + '(非 0,重试后仍失败)' }; break; }
241
+ }
242
+ if (failed) {
243
+ // C5/G9:失败且产物同时声明 needHuman → 转人工闸(不抛错,store 接线转 checkNeedHumanGate 语义)
244
+ const nh = detectNeedHuman(artifactsDirOf(sessionId, batchId), fields);
245
+ return { ok: false, ...failed, declared: true, needHumanEscalation: nh.declared, path: det.path };
246
+ }
247
+ return { ok: true, declared: true, commands: det.commands, results, outputTruncated, path: det.path };
248
+ }
249
+ // targets 门禁(O2,设计 §1.2):exec 层 lane 声明 targets(批次产物根外目标文件绝对路径)→ merged 前置校验
250
+ // 每个 target:①存在性(statSync 为文件;缺失/目录 → missing);②变更性——mtime 晚于 lane 启动时间
251
+ // (默认 mtime 路径),或 marker 逃生(任务级 targetsMarker 非空 或 env GATE_TARGETS_MODE==='marker' →
252
+ // 目标文件内容含独立行 `targets-claimed: true` 即视为已变更,跳过 mtime 比对,两模式不叠加);
253
+ // 未声明 targets / 非 exec 层 / GATE_ENABLED=false → 零感知({ ok: true, declared: false },与 checkCommandGate D-005 对称)。
254
+ // 判定:missing 非空 → { ok:false, code:'GATE_TARGET_MISSING', missing };unchanged 非空 →
255
+ // { ok:false, code:'GATE_TARGET_UNCHANGED', unchanged };全过 → { ok:true, declared:true, mode:'mtime'|'marker' }。
256
+ // 诚实披露:mtime/marker 均为低成本实证(非密码学证据),防「口头声称零成本通过」;恶意伪造归 O5 人审。
257
+ function checkTargetsGate(sessionId: string, batchId: string, batch: Batch, lane: string) {
258
+ // 总开关(GATE_ENABLED=false → 全批零感知,应急逃生阀,与 checkCommandGate 同源)
259
+ if (String(process.env.GATE_ENABLED).toLowerCase() === 'false') return { ok: true, declared: false };
260
+ const t = taskOf(batch, lane);
261
+ if (!t || t.layer !== 'exec' || !Array.isArray(t.targets) || t.targets.length === 0) {
262
+ return { ok: true, declared: false }; // 未声明 / 非 exec 层 → 零感知
263
+ }
264
+ const startAt = laneStartedAt(batch, lane);
265
+ const startMs = Date.parse(startAt);
266
+ const markerMode = (typeof t.targetsMarker === 'string' && t.targetsMarker.length > 0)
267
+ || String(process.env.GATE_TARGETS_MODE).toLowerCase() === 'marker';
268
+ const missing: string[] = [];
269
+ const unchanged: string[] = [];
270
+ for (const target of t.targets) {
271
+ let st;
272
+ try { st = fs.statSync(target); } catch { missing.push(target); continue; }
273
+ if (!st.isFile()) { missing.push(target); continue; } // 目录/非文件视同缺失(与 detectNeedHuman 文件判定同源)
274
+ if (markerMode) {
275
+ let claimed = false;
276
+ try {
277
+ claimed = TARGETS_CLAIMED_RE.test(fs.readFileSync(target, 'utf8'));
278
+ } catch { /* 读失败 → 视为未声明(fail-closed,落到 unchanged) */ }
279
+ if (claimed) continue; // 标记命中 → 视为已变更(跳过 mtime 比对)
280
+ unchanged.push(target);
281
+ continue;
282
+ }
283
+ // mtime 主路径(默认):目标文件 mtime 必须晚于 lane 最近 running 启动时间
284
+ const mtimeMs = new Date(st.mtime).getTime();
285
+ if (!(Number.isFinite(mtimeMs) && Number.isFinite(startMs) && mtimeMs > startMs)) unchanged.push(target);
286
+ }
287
+ if (missing.length > 0) {
288
+ return { ok: false, declared: true, code: 'GATE_TARGET_MISSING', missing, unchanged: [], mode: markerMode ? 'marker' : 'mtime', targets: [...t.targets] };
289
+ }
290
+ if (unchanged.length > 0) {
291
+ return { ok: false, declared: true, code: 'GATE_TARGET_UNCHANGED', missing: [], unchanged, mode: markerMode ? 'marker' : 'mtime', targets: [...t.targets] };
292
+ }
293
+ return { ok: true, declared: true, missing: [], unchanged: [], mode: markerMode ? 'marker' : 'mtime', targets: [...t.targets] };
294
+ }
295
+ // Complete Gate(设计 §5.2):audit 层必须存在且全部 settled 且无 failed/conflict;exec 层全部 settled
296
+ function checkCompleteGate(batch: Batch) {
297
+ const layers: Record<Layer, string[]> = { plan: [], exec: [], audit: [] };
298
+ for (const w of batch.wavePlan ?? []) {
299
+ for (const t of w.tasks) if (t.layer && layers[t.layer]) layers[t.layer].push(t.id);
300
+ }
301
+ if (layers.exec.length === 0 && layers.audit.length === 0) return { ok: true }; // generic 批次
302
+ const laneState = (id: string) => batch.lanes[id];
303
+ const allTerminal = (ids: string[]) => ids.every((id: string) => schema.isMemberTerminal(laneState(id)));
304
+ if (layers.audit.length === 0) return { ok: false, code: 'GATE_COMPLETE_NO_AUDIT' };
305
+ if (!allTerminal(layers.audit)) return { ok: false, code: 'GATE_EXIT_PENDING_AUDIT', pending: layers.audit.filter((id: string) => !schema.isMemberTerminal(laneState(id))) };
306
+ if (layers.audit.some((id: string) => ['failed', 'conflict'].includes(laneState(id)))) return { ok: false, code: 'GATE_COMPLETE_AUDIT_FAILED' };
307
+ if (layers.exec.length && !allTerminal(layers.exec)) return { ok: false, code: 'GATE_COMPLETE_EXEC_PENDING', pending: layers.exec.filter((id: string) => !schema.isMemberTerminal(laneState(id))) };
308
+ return { ok: true };
309
+ }
310
+ // 门禁状态查询(gate_status 工具用,设计 §8 M1):lane 的 layer/契约字段/缺失清单/plan 契约问题
311
+ function gateStatus(sessionId: string, batchId: string, lane: string) {
312
+ const batch = readBatch(sessionId, batchId);
313
+ if (!batch) {
314
+ // 损坏批次(文件存在但解析失败)→ 返回 corrupt 视图不 throw(AC-1 读路径不 throw);不存在 → throw
315
+ if (fs.existsSync(batchFile(sessionId, batchId))) {
316
+ return { lane, layer: null, state: null, team: null, corrupt: true, consume: [], produce: [], outputs: [], consumeMissing: [], outputsMissing: [], produceMissing: [], contractProblems: null, targets: [], targetsMissing: [], targetsUnchanged: [] };
317
+ }
318
+ throw new Error('batch not found: ' + batchId);
319
+ }
320
+ const t = taskOf(batch, lane);
321
+ if (!t) return { lane, layer: null, state: batch.lanes[lane], gates: 'generic', team: batch.team };
322
+ const missing = (field: TaskArrayField) => (Array.isArray(t[field]) ? t[field] : []).filter((p: string) => !fileExistsNonEmpty(resolveArtifact(sessionId, batchId, p)));
323
+ const contract = t.layer === 'plan' ? checkPlanContract(sessionId, batchId, batch, lane) : null;
324
+ // targets 探测(O2,设计 §1.5):声明清单 + 缺失清单 + 未变更清单——仅 stat(存在性 + mtime 路径),
325
+ // 不读文件正文(marker 命中判定留给门禁执行时,避免 gate_status 读文件成本,见 design Open Question 3)
326
+ const targets: string[] = Array.isArray(t.targets) ? [...t.targets] : [];
327
+ const startAt = laneStartedAt(batch, lane);
328
+ const startMs = Date.parse(startAt);
329
+ const statFile = (p: string): fs.Stats | null => { try { const st = fs.statSync(p); return st.isFile() ? st : null; } catch { return null; } };
330
+ const targetsMissing = targets.filter((p: string) => statFile(p) === null);
331
+ const targetsUnchanged = targets.filter((p: string) => {
332
+ if (targetsMissing.includes(p)) return false;
333
+ const st = statFile(p);
334
+ if (!st) return true; // 探测竞态:stat 失败视同未变更(保守)
335
+ const mtimeMs = new Date(st.mtime).getTime();
336
+ return !(Number.isFinite(mtimeMs) && Number.isFinite(startMs) && mtimeMs > startMs);
337
+ });
338
+ return {
339
+ lane, layer: t.layer ?? null, state: batch.lanes[lane], team: batch.team,
340
+ consume: t.consume ?? [], produce: t.produce ?? [], outputs: t.outputs ?? [],
341
+ consumeMissing: missing('consume'), outputsMissing: missing('outputs'), produceMissing: missing('produce'),
342
+ contractProblems: contract && !contract.ok ? contract.problems : null,
343
+ targets, targetsMissing, targetsUnchanged,
344
+ };
345
+ }
346
+
347
+ return { checkEntryGate, checkPlanContract, checkExitGate, checkNeedHumanGate, checkCommandGate, checkTargetsGate, checkCompleteGate, gateStatus };
348
+ }
@@ -0,0 +1,14 @@
1
+ import type { BatchTransitionTable, TransitionTable } from '../types/contracts.js';
2
+ export declare const DEFAULT_MEMBER_RULES: TransitionTable;
3
+ export declare const DEFAULT_BATCH_RULES: BatchTransitionTable;
4
+ export declare function loadRules(config: {
5
+ ratchet?: {
6
+ memberRules?: unknown;
7
+ batchRules?: unknown;
8
+ allowRelax?: boolean;
9
+ };
10
+ } | null | undefined): {
11
+ memberRules: TransitionTable;
12
+ batchRules: BatchTransitionTable;
13
+ source: 'default' | 'config';
14
+ };
@@ -0,0 +1,76 @@
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
+ // MachineRules:状态迁移规则表(棘轮形式化)
18
+ // 单一事实源:引用 lib/schema.ts 的 MEMBER_TRANSITIONS / BATCH_TRANSITIONS(不拷贝,schema.ts 零改动)
19
+ // 棘轮语义:默认规则 = 现行强约束(行为不变);配置覆盖只许删(收紧)不许增(放宽)——
20
+ // 覆盖表出现默认表中不存在的迁移 → loadRules throw(启动即失败,fail-closed,棘轮不可绕过);
21
+ // allowRelax: true 为显式逃生门(默认 false,仅在部署侧明确授权时可用)。
22
+ // 纯逻辑零依赖(无 IO),可独立单测。
23
+ // Phase 2 类型化:validateOverride 泛型化(T = MemberState|BatchPhase,覆盖表 Record<T, readonly T[]>);
24
+ // override 为配置输入 unknown(运行期校验兜底,fail-closed 语义零变更);棘轮 throw 逻辑一字不动。
25
+ import { MEMBER_TRANSITIONS, BATCH_TRANSITIONS } from '../schema.js';
26
+ // 默认 = 现行强约束(与 schema 常量同引用,非拷贝)
27
+ // 显式标注通用形态:satisfies 字面量(8 键/5 键)经泛型 Record 匹配时推断不稳定(空数组元素 never 污染),
28
+ // 标注后 validateOverride<T> 稳定推断 T=MemberState/BatchPhase(运行期零变更,纯类型层)
29
+ export const DEFAULT_MEMBER_RULES = MEMBER_TRANSITIONS;
30
+ export const DEFAULT_BATCH_RULES = BATCH_TRANSITIONS;
31
+ // 棘轮校验:覆盖表 must be 默认表的子集(按 from 键逐项:值必须是字符串数组且 ⊆ 默认 to 列表)。
32
+ // allowRelax=true 时仅做结构校验(键存在、值数组),放行新增迁移(显式逃生门)。
33
+ function validateOverride(defaultTable, override, kind, allowRelax) {
34
+ if (override == null)
35
+ return null;
36
+ if (typeof override !== 'object' || Array.isArray(override)) {
37
+ throw new Error('ratchet ' + kind + ' must be an object of { from: [to...] }');
38
+ }
39
+ const next = { ...defaultTable };
40
+ for (const [from, tos] of Object.entries(override)) {
41
+ if (!(from in defaultTable)) {
42
+ throw new Error('ratchet ' + kind + ': unknown state "' + from + '" (ratchet cannot add states)');
43
+ }
44
+ const fromKey = from; // 单点断言:'from in defaultTable' 守卫后 from 必为 T 键(运行期守卫语义不变)
45
+ if (!Array.isArray(tos) || tos.some((t) => typeof t !== 'string' || !t)) {
46
+ throw new Error('ratchet ' + kind + '[' + from + '] must be an array of non-empty strings');
47
+ }
48
+ if (new Set(tos).size !== tos.length) {
49
+ throw new Error('ratchet ' + kind + '[' + from + '] has duplicate targets');
50
+ }
51
+ if (!allowRelax) {
52
+ for (const to of tos) {
53
+ if (!defaultTable[fromKey].includes(to)) {
54
+ throw new Error('ratchet ' + kind + ': relaxing transition "' + from + ' -> ' + to + '" is not allowed (ratchet; set allowRelax: true to override)');
55
+ }
56
+ }
57
+ }
58
+ next[fromKey] = [...tos];
59
+ }
60
+ return next;
61
+ }
62
+ // 加载规则表:config.ratchet = { memberRules?, batchRules?, allowRelax: false }(可选键,schema.ts Config 说明)
63
+ // 未配置 → 默认表(= 现行约束,行为不变);配置覆盖 → 棘轮校验(非法 throw,fail-closed)。
64
+ // 返回 { memberRules, batchRules, source: 'default'|'config' }。
65
+ export function loadRules(config) {
66
+ const ratchet = config && typeof config === 'object' ? config.ratchet : undefined;
67
+ const allowRelax = ratchet?.allowRelax === true;
68
+ const memberOverride = ratchet?.memberRules;
69
+ const batchOverride = ratchet?.batchRules;
70
+ if (memberOverride == null && batchOverride == null) {
71
+ return { memberRules: DEFAULT_MEMBER_RULES, batchRules: DEFAULT_BATCH_RULES, source: 'default' };
72
+ }
73
+ const memberRules = validateOverride(DEFAULT_MEMBER_RULES, memberOverride, 'memberRules', allowRelax) ?? DEFAULT_MEMBER_RULES;
74
+ const batchRules = validateOverride(DEFAULT_BATCH_RULES, batchOverride, 'batchRules', allowRelax) ?? DEFAULT_BATCH_RULES;
75
+ return { memberRules, batchRules, source: 'config' };
76
+ }
@@ -0,0 +1,88 @@
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
+
18
+ // MachineRules:状态迁移规则表(棘轮形式化)
19
+ // 单一事实源:引用 lib/schema.ts 的 MEMBER_TRANSITIONS / BATCH_TRANSITIONS(不拷贝,schema.ts 零改动)
20
+ // 棘轮语义:默认规则 = 现行强约束(行为不变);配置覆盖只许删(收紧)不许增(放宽)——
21
+ // 覆盖表出现默认表中不存在的迁移 → loadRules throw(启动即失败,fail-closed,棘轮不可绕过);
22
+ // allowRelax: true 为显式逃生门(默认 false,仅在部署侧明确授权时可用)。
23
+ // 纯逻辑零依赖(无 IO),可独立单测。
24
+ // Phase 2 类型化:validateOverride 泛型化(T = MemberState|BatchPhase,覆盖表 Record<T, readonly T[]>);
25
+ // override 为配置输入 unknown(运行期校验兜底,fail-closed 语义零变更);棘轮 throw 逻辑一字不动。
26
+ import { MEMBER_TRANSITIONS, BATCH_TRANSITIONS } from '../schema.js';
27
+ import type { MemberState, BatchPhase } from '../schema.js';
28
+ import type { BatchTransitionTable, TransitionTable } from '../types/contracts.js';
29
+
30
+ // 默认 = 现行强约束(与 schema 常量同引用,非拷贝)
31
+ // 显式标注通用形态:satisfies 字面量(8 键/5 键)经泛型 Record 匹配时推断不稳定(空数组元素 never 污染),
32
+ // 标注后 validateOverride<T> 稳定推断 T=MemberState/BatchPhase(运行期零变更,纯类型层)
33
+ export const DEFAULT_MEMBER_RULES: TransitionTable = MEMBER_TRANSITIONS;
34
+ export const DEFAULT_BATCH_RULES: BatchTransitionTable = BATCH_TRANSITIONS;
35
+
36
+ // 棘轮校验:覆盖表 must be 默认表的子集(按 from 键逐项:值必须是字符串数组且 ⊆ 默认 to 列表)。
37
+ // allowRelax=true 时仅做结构校验(键存在、值数组),放行新增迁移(显式逃生门)。
38
+ function validateOverride<T extends MemberState | BatchPhase>(
39
+ defaultTable: Record<T, readonly T[]>,
40
+ override: unknown,
41
+ kind: string,
42
+ allowRelax: boolean,
43
+ ): Record<T, readonly T[]> | null {
44
+ if (override == null) return null;
45
+ if (typeof override !== 'object' || Array.isArray(override)) {
46
+ throw new Error('ratchet ' + kind + ' must be an object of { from: [to...] }');
47
+ }
48
+ const next = { ...defaultTable };
49
+ for (const [from, tos] of Object.entries(override)) {
50
+ if (!(from in defaultTable)) {
51
+ throw new Error('ratchet ' + kind + ': unknown state "' + from + '" (ratchet cannot add states)');
52
+ }
53
+ const fromKey = from as T; // 单点断言:'from in defaultTable' 守卫后 from 必为 T 键(运行期守卫语义不变)
54
+ if (!Array.isArray(tos) || tos.some((t: string) => typeof t !== 'string' || !t)) {
55
+ throw new Error('ratchet ' + kind + '[' + from + '] must be an array of non-empty strings');
56
+ }
57
+ if (new Set(tos).size !== tos.length) {
58
+ throw new Error('ratchet ' + kind + '[' + from + '] has duplicate targets');
59
+ }
60
+ if (!allowRelax) {
61
+ for (const to of tos) {
62
+ if (!defaultTable[fromKey].includes(to)) {
63
+ throw new Error('ratchet ' + kind + ': relaxing transition "' + from + ' -> ' + to + '" is not allowed (ratchet; set allowRelax: true to override)');
64
+ }
65
+ }
66
+ }
67
+ next[fromKey] = [...tos];
68
+ }
69
+ return next;
70
+ }
71
+
72
+ // 加载规则表:config.ratchet = { memberRules?, batchRules?, allowRelax: false }(可选键,schema.ts Config 说明)
73
+ // 未配置 → 默认表(= 现行约束,行为不变);配置覆盖 → 棘轮校验(非法 throw,fail-closed)。
74
+ // 返回 { memberRules, batchRules, source: 'default'|'config' }。
75
+ export function loadRules(
76
+ config: { ratchet?: { memberRules?: unknown; batchRules?: unknown; allowRelax?: boolean } } | null | undefined,
77
+ ): { memberRules: TransitionTable; batchRules: BatchTransitionTable; source: 'default' | 'config' } {
78
+ const ratchet = config && typeof config === 'object' ? config.ratchet : undefined;
79
+ const allowRelax = ratchet?.allowRelax === true;
80
+ const memberOverride = ratchet?.memberRules;
81
+ const batchOverride = ratchet?.batchRules;
82
+ if (memberOverride == null && batchOverride == null) {
83
+ return { memberRules: DEFAULT_MEMBER_RULES, batchRules: DEFAULT_BATCH_RULES, source: 'default' };
84
+ }
85
+ const memberRules = validateOverride(DEFAULT_MEMBER_RULES, memberOverride, 'memberRules', allowRelax) ?? DEFAULT_MEMBER_RULES;
86
+ const batchRules = validateOverride(DEFAULT_BATCH_RULES, batchOverride, 'batchRules', allowRelax) ?? DEFAULT_BATCH_RULES;
87
+ return { memberRules, batchRules, source: 'config' };
88
+ }
@@ -0,0 +1,68 @@
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
+
18
+ // Machine:状态迁移判定 + 棘轮规则消费 + lane 派发条件校验
19
+ // 本文件只含迁移判定 + 派发条件——
20
+ // archive 逻辑归 archive.js、needHuman 逻辑归 gates.js/store.js,不得写入本文件。
21
+ // 纯逻辑(无 IO):fileExists 经 DI 注入(store.js 接线时绑定 artifactsDir),可独立单测。
22
+ import { DEFAULT_MEMBER_RULES, DEFAULT_BATCH_RULES } from './machine-rules.js';
23
+ // P1-04 单点:findTask 收敛至 task-utils.js(原 :69 本地定义删除——原「避免 machine→gates 依赖」由零依赖单点承担)
24
+ import { findTask } from './task-utils.js';
25
+
26
+ // 缺省规则 = 默认表(与 schema 常量同引用,行为不变)
27
+ export const RATCHET_RULES = DEFAULT_MEMBER_RULES;
28
+
29
+ // 成员迁移判定:查规则表(rules 可注入;缺省 = 默认强约束)
30
+ // 返回 { ok: true } | { ok: false, code: 'INVALID_MEMBER_TRANSITION' }
31
+ export function applyMemberTransition(from, to, { rules } = {}) {
32
+ const table = rules?.memberRules ?? DEFAULT_MEMBER_RULES;
33
+ if (!table || typeof table !== 'object' || !Array.isArray(table[from])) {
34
+ return { ok: false, code: 'INVALID_MEMBER_TRANSITION' };
35
+ }
36
+ return table[from].includes(to)
37
+ ? { ok: true }
38
+ : { ok: false, code: 'INVALID_MEMBER_TRANSITION' };
39
+ }
40
+
41
+ // 批次阶段迁移判定:同上(batchRules 表)
42
+ export function applyBatchTransition(from, to, { rules } = {}) {
43
+ const table = rules?.batchRules ?? DEFAULT_BATCH_RULES;
44
+ if (!table || typeof table !== 'object' || !Array.isArray(table[from])) {
45
+ return { ok: false, code: 'INVALID_BATCH_TRANSITION' };
46
+ }
47
+ return table[from].includes(to)
48
+ ? { ok: true }
49
+ : { ok: false, code: 'INVALID_BATCH_TRANSITION' };
50
+ }
51
+
52
+ // 派发条件校验:读任务 condition(对象数组 {path, exists},建批时已规范化)→ 逐个 fileExists → AND 求值。
53
+ // fileExists(path) 由 store.js 注入(相对路径解析在注入侧,本函数保持纯逻辑)。
54
+ // 无 condition → 恒 { ok: true }。返回 { ok: true } | { ok: false, missing: [path...] }。
55
+ export function checkDispatchCondition(sessionId, batchId, batch, lane, { fileExists }) {
56
+ const task = findTask(batch, lane);
57
+ const condition = task?.condition ?? null;
58
+ if (condition == null) return { ok: true };
59
+ if (typeof fileExists !== 'function') {
60
+ throw new Error('checkDispatchCondition: fileExists must be injected (DI)');
61
+ }
62
+ const missing = [];
63
+ for (const c of condition) {
64
+ if (!c || typeof c.path !== 'string' || !c.path) continue; // 防御:非法元素按恒满足跳过(建批时已静态校验)
65
+ if (!fileExists(c.path)) missing.push(c.path);
66
+ }
67
+ return missing.length ? { ok: false, missing } : { ok: true };
68
+ }