agentic-flow-kit 0.9.1 → 1.0.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/README.md +93 -93
- package/modules/hosts/claude/commands/wf-build.md +3 -3
- package/modules/hosts/claude/commands/wf-design.md +3 -3
- package/modules/hosts/claude/commands/wf-new-task.md +14 -2
- package/modules/hosts/claude/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/claude/commands/wf-plan.md +3 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-build.md +3 -3
- package/modules/hosts/codex/commands/wf-design.md +3 -3
- package/modules/hosts/codex/commands/wf-new-task.md +14 -2
- package/modules/hosts/codex/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/codex/commands/wf-plan.md +3 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-build.md +3 -3
- package/modules/hosts/cursor/commands/wf-design.md +3 -3
- package/modules/hosts/cursor/commands/wf-new-task.md +14 -2
- package/modules/hosts/cursor/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/cursor/commands/wf-plan.md +3 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +3 -3
- package/modules/hosts/opencode/commands/wf-design.md +3 -3
- package/modules/hosts/opencode/commands/wf-new-task.md +14 -2
- package/modules/hosts/opencode/commands/wf-pipeline-run.md +35 -0
- package/modules/hosts/opencode/commands/wf-plan.md +3 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +3 -3
- package/modules/hosts/trae/commands/wf-design.md +3 -3
- package/modules/hosts/trae/commands/wf-new-task.md +14 -2
- package/modules/hosts/trae/commands/wf-pipeline-run.md +36 -0
- package/modules/hosts/trae/commands/wf-plan.md +3 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/modules/hosts/trae/rules/project-workflow.md +1 -1
- package/package.json +45 -45
- package/templates/_agents/commands/build.md +2 -2
- package/templates/_agents/commands/design.md +1 -1
- package/templates/_agents/commands/new-task.md +1 -0
- package/templates/_agents/commands/pipeline-run.md +43 -0
- package/templates/_agents/commands/plan.md +3 -3
- package/templates/_agents/commands/sync-hosts.md +1 -1
- package/templates/_agents/scripts/check-lane-surface.mjs +20 -7
- package/templates/_agents/scripts/check-lane-surface.test.mjs +24 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +135 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +161 -0
- package/templates/_agents/scripts/check-loop.mjs +70 -18
- package/templates/_agents/scripts/check-loop.test.mjs +155 -8
- package/templates/_agents/scripts/confirm-doc.mjs +19 -7
- package/templates/_agents/scripts/confirm-doc.test.mjs +31 -0
- package/templates/_agents/scripts/fill-intent.mjs +1 -1
- package/templates/_agents/scripts/fill-plan.test.mjs +4 -3
- package/templates/_agents/scripts/gate-checklist.mjs +1 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +75 -5
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +104 -2
- package/templates/_agents/scripts/pipeline-run.mjs +999 -0
- package/templates/_agents/scripts/pipeline-run.test.mjs +537 -0
- package/templates/_agents/scripts/policy.mjs +1 -0
- package/templates/_agents/scripts/pre-push.test.mjs +8 -1
- package/templates/_agents/scripts/solidify-task.test.mjs +5 -2
- package/templates/_agents/scripts/stage-gates.mjs +29 -6
- package/templates/_agents/scripts/trust-mode.test.mjs +110 -0
- package/templates/_agents/scripts-test-exempt.txt +10 -0
- package/templates/_githooks/pre-commit +12 -0
- package/templates/workflow/README.md +3 -2
- package/templates/workflow/plans/_TEMPLATE.md +1 -1
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pipeline-run — 跨宿主全自动闭环执行器(2026-10-02 pipeline-run;spec workflow/specs/2026-10-02-pipeline-run.md)
|
|
3
|
+
// 用途:把六阶段闭环的确定性部分代码化为状态机——start/next/status/watch/abort 五子命令;
|
|
4
|
+
// AI 槽位经工单协议交宿主执行,人件门到点即停、next --delegated "<原话>" 逐份过 confirm-doc。
|
|
5
|
+
// 红线:绝不 spawn AI 会话 / 零依赖 / git 永不 --no-verify / 永不伪造原话 / 永不直改文档状态(唯一入口 confirm-doc)。
|
|
6
|
+
// 契约:工单协议 + run 事件流 schema 见 spec §功能行为(宿主面契约,变更须走变更流程)。
|
|
7
|
+
// 存储:.agents/cache/pipeline-runs/<runId>.json(gitignored 缓存态,非权威——workflow/ 文档 + confirmations.jsonl 是唯一真相源)。
|
|
8
|
+
// 注入(测试钩子,spec §测试注入契约):PIPELINE_RUN_ROOT(仓库根,默认 cwd)/
|
|
9
|
+
// PIPELINE_RUN_BIN(门脚本目录,默认 <root>/.agents/scripts)/ PIPELINE_RUN_GIT_BIN(git 命令前缀,空格分隔,默认 "git")/
|
|
10
|
+
// PIPELINE_RUN_TODAY(文档日期锚定,默认当天——测试固定日期消除时钟依赖,2026-10-03 实证:测试硬编码日期跨日即红)。
|
|
11
|
+
// npm 不直接调用——测试门统一走 verify.mjs(其内部自跑 npm test)。
|
|
12
|
+
// 用法:
|
|
13
|
+
// node .agents/scripts/pipeline-run.mjs start "<需求原文>" [--hint "kind=require level=L1 module=wiki"]
|
|
14
|
+
// node .agents/scripts/pipeline-run.mjs next [--run <id>] [--triage "kind=|level=|module=|topic="] [--delegated "<原话>"] [--files "a,b"]
|
|
15
|
+
// node .agents/scripts/pipeline-run.mjs status [--run <id>] [--adopt] [--json]
|
|
16
|
+
// node .agents/scripts/pipeline-run.mjs watch [--run <id>] [--interval 2]
|
|
17
|
+
// node .agents/scripts/pipeline-run.mjs abort --run <id> [--to superseded|cancelled] [--delegated "<原话>"]
|
|
18
|
+
// 测试:node templates/_agents/scripts/pipeline-run.test.mjs(夹具假门 + 假 git,不触真实仓库)
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const KINDS = ['require', 'fix', 'verify', 'review', 'deploy'];
|
|
26
|
+
const LEVELS = ['L0', 'L1', 'L2', 'L3'];
|
|
27
|
+
const TERMINAL = ['done', 'superseded', 'cancelled', 'closed'];
|
|
28
|
+
const FIX_CAP = 3;
|
|
29
|
+
const GIT_NEVER_FLAGS = ['--no-verify', '-n']; // 不变量:任何 git 调用不得携带(防御性断言,正常路径不会拼出)
|
|
30
|
+
|
|
31
|
+
function fail(msg, code = 1) { console.error('❌ ' + msg); process.exit(code); }
|
|
32
|
+
const nowIso = () => new Date().toISOString();
|
|
33
|
+
const today = () => process.env.PIPELINE_RUN_TODAY || nowIso().slice(0, 10);
|
|
34
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
35
|
+
|
|
36
|
+
// ── 环境解析(测试注入点) ─────────────────────────────────────────────
|
|
37
|
+
function ctxEnv(overrideRoot) {
|
|
38
|
+
const root = path.resolve(overrideRoot || process.env.PIPELINE_RUN_ROOT || process.cwd());
|
|
39
|
+
const bin = path.resolve(process.env.PIPELINE_RUN_BIN || path.join(root, '.agents', 'scripts'));
|
|
40
|
+
const gitPrefix = (process.env.PIPELINE_RUN_GIT_BIN || 'git').split(/\s+/).filter(Boolean);
|
|
41
|
+
return { root, bin, gitPrefix, runsDir: path.join(root, '.agents', 'cache', 'pipeline-runs') };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function moduleList(ctx) {
|
|
45
|
+
try {
|
|
46
|
+
return fs.readFileSync(path.join(ctx.root, '.agents', 'workflow-modules.txt'), 'utf8')
|
|
47
|
+
.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
|
|
48
|
+
} catch { return ['backend', 'frontend', 'ui', 'infra', 'integration', 'pipeline', 'wiki']; } // 夹具缺表时的回退默认(warning 由调用方打)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── run 文件(原子写;事件 append-only) ──────────────────────────────
|
|
52
|
+
function runFileOf(ctx, runId) { return path.join(ctx.runsDir, `${runId}.json`); }
|
|
53
|
+
|
|
54
|
+
function loadRun(ctx, runId) {
|
|
55
|
+
const f = runFileOf(ctx, runId);
|
|
56
|
+
if (!fs.existsSync(f)) return null;
|
|
57
|
+
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function saveRun(ctx, run) {
|
|
61
|
+
fs.mkdirSync(ctx.runsDir, { recursive: true });
|
|
62
|
+
run.updatedAt = nowIso();
|
|
63
|
+
const f = runFileOf(ctx, run.runId);
|
|
64
|
+
const tmp = f + '.tmp';
|
|
65
|
+
fs.writeFileSync(tmp, JSON.stringify(run, null, 2) + '\n');
|
|
66
|
+
fs.renameSync(tmp, f);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function emit(run, ev) { run.events.push({ t: nowIso(), ...ev }); }
|
|
70
|
+
|
|
71
|
+
function latestRunId(ctx) {
|
|
72
|
+
if (!fs.existsSync(ctx.runsDir)) return null;
|
|
73
|
+
const ids = fs.readdirSync(ctx.runsDir).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5)).sort();
|
|
74
|
+
for (let i = ids.length - 1; i >= 0; i--) {
|
|
75
|
+
const r = loadRun(ctx, ids[i]);
|
|
76
|
+
if (r && !['done', 'aborted'].includes(r.stopType)) return ids[i];
|
|
77
|
+
}
|
|
78
|
+
return ids[ids.length - 1] || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function newRunId(requirement) {
|
|
82
|
+
const d = new Date();
|
|
83
|
+
const ascii = (requirement.toLowerCase().match(/[a-z0-9]{2,}/g) || []).slice(0, 3).join('-') || 'task';
|
|
84
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
85
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}-${ascii}-${rand}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── 门执行(唯一子进程出口;记录真实退出码) ──────────────────────────
|
|
89
|
+
function runNode(ctx, run, script, args) {
|
|
90
|
+
const scriptPath = path.join(ctx.bin, script);
|
|
91
|
+
const cmd = `node .agents/scripts/${script} ${args.join(' ')}`;
|
|
92
|
+
const t0 = Date.now();
|
|
93
|
+
const p = spawnSync(process.execPath, [scriptPath, ...args], { cwd: ctx.root, encoding: 'utf8', windowsHide: true });
|
|
94
|
+
const ms = Date.now() - t0;
|
|
95
|
+
const exit = p.status ?? 1;
|
|
96
|
+
emit(run, { type: 'gate', cmd, exit, ms });
|
|
97
|
+
return { exit, stdout: p.stdout || '', stderr: p.stderr || '' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function runGit(ctx, run, args) {
|
|
101
|
+
if (args.some((a) => GIT_NEVER_FLAGS.includes(a))) fail('不变量破坏:git 调用携带禁用旗标', 1);
|
|
102
|
+
const cmd = `git ${args.join(' ')}`;
|
|
103
|
+
const t0 = Date.now();
|
|
104
|
+
const p = spawnSync(ctx.gitPrefix[0], [...ctx.gitPrefix.slice(1), ...args], { cwd: ctx.root, encoding: 'utf8', windowsHide: true });
|
|
105
|
+
const ms = Date.now() - t0;
|
|
106
|
+
const exit = p.status ?? 1;
|
|
107
|
+
emit(run, { type: 'gate', cmd, exit, ms });
|
|
108
|
+
return { exit, stdout: p.stdout || '', stderr: p.stderr || '' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 文档读取/校验 ──────────────────────────────────────────────────────
|
|
112
|
+
function readDoc(ctx, rel) {
|
|
113
|
+
try { return fs.readFileSync(path.join(ctx.root, rel), 'utf8'); } catch { return null; }
|
|
114
|
+
}
|
|
115
|
+
function fmGet(text, key) {
|
|
116
|
+
const m = text.split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
|
|
117
|
+
return m ? m.slice(key.length + 1).trim() : null;
|
|
118
|
+
}
|
|
119
|
+
function docState(ctx, rel) { const t = readDoc(ctx, rel); return t ? fmGet(t, '状态') : null; }
|
|
120
|
+
|
|
121
|
+
// 占位符判定(复核 P1-3 收窄,2026-10-02):先剔除 code span(`…`)与引号串("…")——
|
|
122
|
+
// 正文合法的命令语法占位(如 `next --delegated "<原话>"`)几乎总在 code span 内,
|
|
123
|
+
// 模板实占位符(如 <为什么做;写明需求来源…>)从不包 code span;再匹配剩余裸尖括号。
|
|
124
|
+
export function placeholdersIn(text) {
|
|
125
|
+
const stripped = String(text)
|
|
126
|
+
.replace(/^```[\s\S]*?^```$/gm, '') // 复核 P2-B:跨行 code fence 整块剔除(围栏内命令示例占位不属模板占位符)
|
|
127
|
+
.replace(/`[^`\n]*`/g, '')
|
|
128
|
+
.replace(/"[^"\n]*"/g, '');
|
|
129
|
+
const hits = [];
|
|
130
|
+
for (const re of [/<[^>\n]{1,60}>/g, /YYYY-MM-DD 用户/g]) {
|
|
131
|
+
let m; while ((m = re.exec(stripped))) hits.push(m[0]);
|
|
132
|
+
}
|
|
133
|
+
return [...new Set(hits)].slice(0, 5);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function sectionBody(text, title) {
|
|
137
|
+
const lines = text.split(/\r?\n/);
|
|
138
|
+
const i = lines.findIndex((l) => l.trim() === `## ${title}` || l.trim() === `## ${title}(L1 微改动无实质内容可删本节,不硬填)` || l.trim().startsWith(`## ${title}`));
|
|
139
|
+
if (i < 0) return null;
|
|
140
|
+
const body = [];
|
|
141
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
142
|
+
if (/^##\s/.test(lines[j])) break;
|
|
143
|
+
body.push(lines[j]);
|
|
144
|
+
}
|
|
145
|
+
return body.join('\n').trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function validateDraftContent(text, kind, level) {
|
|
149
|
+
const problems = [];
|
|
150
|
+
if (!text) return ['文件不存在或为空'];
|
|
151
|
+
const state = fmGet(text, '状态');
|
|
152
|
+
if (!state) problems.push('frontmatter 缺「状态:」行');
|
|
153
|
+
if (kind !== 'incident' && !fmGet(text, '级别')) problems.push('frontmatter 缺「级别:」行');
|
|
154
|
+
const ph = placeholdersIn(text);
|
|
155
|
+
if (ph.length) problems.push(`模板占位符残留:${ph.join('、')}`);
|
|
156
|
+
const required = {
|
|
157
|
+
intent: level === 'L1' ? ['背景与问题', '目标', '验收标准'] : ['背景与问题', '历史教训/防复发', '目标', '非目标', '约束', '验收标准'],
|
|
158
|
+
spec: ['功能行为', '数据流', '系统改动', '约束遵守映射'],
|
|
159
|
+
plan: level === 'L1' ? ['改动方案', '约束与风险', '验证计划'] : ['改动方案', '任务拆解', '执行顺序', '验证计划'],
|
|
160
|
+
incident: ['时间线', '根因', '复盘三件套'],
|
|
161
|
+
}[kind] || [];
|
|
162
|
+
for (const sec of required) {
|
|
163
|
+
const b = sectionBody(text, sec);
|
|
164
|
+
if (b === null) { if (!(level === 'L1' && ['非目标', '约束', '任务拆解', '执行顺序'].includes(sec))) problems.push(`缺节:## ${sec}`); }
|
|
165
|
+
else if (!b) problems.push(`节为空:## ${sec}`);
|
|
166
|
+
}
|
|
167
|
+
return problems;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function closeoutComplete(text) {
|
|
171
|
+
const body = sectionBody(text, '验收标准');
|
|
172
|
+
if (body === null) return { ok: false, problems: ['缺「## 验收标准」节'] };
|
|
173
|
+
const lines = body.split(/\r?\n/);
|
|
174
|
+
const open = lines.filter((l) => /^\s*-\s\[\s\]\s/.test(l));
|
|
175
|
+
// 复核 P2-6:证据冒号后须直接跟内容(非空白、非收括号)——「(证据:)」空值不算过
|
|
176
|
+
const checkedNoEv = lines.filter((l) => /^\s*-\s\[x\]\s/.test(l) && !/证据[::][^)\s]/.test(l));
|
|
177
|
+
const problems = [];
|
|
178
|
+
if (open.length) problems.push(`未勾验收 ${open.length} 条`);
|
|
179
|
+
if (checkedNoEv.length) problems.push(`已勾缺证据 ${checkedNoEv.length} 条`);
|
|
180
|
+
return { ok: problems.length === 0, problems };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function summarizeDoc(text) {
|
|
184
|
+
const body = text.split(/\r?\n/).filter((l) => l.trim() && !l.startsWith('---') && !/^状态:|^级别:|^日期:|^模块:|^备注:|^risk_level:/.test(l));
|
|
185
|
+
return body.slice(0, 3).join(' / ').slice(0, 200);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── 工单构建 ───────────────────────────────────────────────────────────
|
|
189
|
+
function wo(run, o) {
|
|
190
|
+
const order = { id: (run.events.filter((e) => e.type === 'work-order').length + 1), ...o };
|
|
191
|
+
return order;
|
|
192
|
+
}
|
|
193
|
+
function issueWO(ctx, run, order, nextStage) {
|
|
194
|
+
run.workOrder = order;
|
|
195
|
+
run.stopType = 'work-order';
|
|
196
|
+
run.stage = nextStage;
|
|
197
|
+
emit(run, { type: 'work-order', id: order.id, kind: order.kind });
|
|
198
|
+
emit(run, { type: 'stop', stopType: 'work-order', workOrderId: order.id });
|
|
199
|
+
saveRun(ctx, run);
|
|
200
|
+
printRun(ctx, run);
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
function stopConfirm(ctx, run, docRel, points, ledger) {
|
|
204
|
+
run.workOrder = null;
|
|
205
|
+
run.stopType = 'await-confirm';
|
|
206
|
+
run.awaitConfirm = { doc: docRel, points, ledger: !!ledger };
|
|
207
|
+
emit(run, { type: 'stop', stopType: 'await-confirm', doc: docRel });
|
|
208
|
+
saveRun(ctx, run);
|
|
209
|
+
printRun(ctx, run);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
function stopGateFail(ctx, run, stage, problems, exitHint) {
|
|
213
|
+
run.workOrder = null;
|
|
214
|
+
run.stopType = 'gate-fail';
|
|
215
|
+
run.stage = stage;
|
|
216
|
+
run.gateFail = { stage, problems };
|
|
217
|
+
emit(run, { type: 'stop', stopType: 'gate-fail', stage });
|
|
218
|
+
saveRun(ctx, run);
|
|
219
|
+
printRun(ctx, run, problems);
|
|
220
|
+
process.exit(2);
|
|
221
|
+
}
|
|
222
|
+
function stopDone(ctx, run) {
|
|
223
|
+
run.workOrder = null;
|
|
224
|
+
run.stopType = 'done';
|
|
225
|
+
run.stage = 'done';
|
|
226
|
+
emit(run, { type: 'done' });
|
|
227
|
+
emit(run, { type: 'stop', stopType: 'done' });
|
|
228
|
+
saveRun(ctx, run);
|
|
229
|
+
printRun(ctx, run);
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── 输出(人类可读 + 机器标记) ────────────────────────────────────────
|
|
234
|
+
function stageBar(run) {
|
|
235
|
+
const s = run.stage;
|
|
236
|
+
const mk = (name, pos) => (pos === 'cur' ? '[●' + name + ']' : pos === 'past' ? '[✓' + name + ']' : '[…' + name + ']');
|
|
237
|
+
const seq = run.triage?.kind === 'verify' ? [['Verify', s === 'verify' || s === 'done']]
|
|
238
|
+
: run.triage?.kind === 'review' ? [['Review', s === 'review-only' || s === 'done']]
|
|
239
|
+
: run.triage?.kind === 'deploy' ? [['Checklist', ['deploy-check', 'deploy-prep'].includes(s)], ['Authorize', s === 'deploy-await' || s === 'done']]
|
|
240
|
+
: [['Plan', ['intent-fill', 'intent-draft', 'intent-confirm', 'incident-fill', 'incident-draft', 'incident-confirm', 'triage'].includes(s)],
|
|
241
|
+
['Design', ['spec-fill', 'spec-draft', 'spec-review', 'spec-confirm'].includes(s)],
|
|
242
|
+
['Build', ['plan-fill', 'plan-draft', 'plan-confirm', 'changes-confirm', 'implement', 'fix', 'changes-verify'].includes(s)],
|
|
243
|
+
['Test', ['verify', 'review', 'closeout', 'done-confirm', 'incident-fixed-confirm', 'incident-closed-confirm', 'direct-commit', 'direct-edit'].includes(s) || s === 'done']];
|
|
244
|
+
const curIdx = seq.findIndex(([, cur]) => cur);
|
|
245
|
+
return seq.map(([name], i) => mk(name, i < curIdx ? 'past' : i === curIdx ? 'cur' : 'future')).join(' ');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function printRun(ctx, run, problems) {
|
|
249
|
+
const lv = run.triage ? ` ${run.triage.level}·${run.triage.kind}` : '';
|
|
250
|
+
console.log(`\n▶ pipeline-run ${run.runId}${lv} ${stageBar(run)}`);
|
|
251
|
+
if (run.stopType === 'work-order' && run.workOrder) {
|
|
252
|
+
const w = run.workOrder;
|
|
253
|
+
console.log(`\n== 工单 #${w.id}(${w.kind})${w.title} ==`);
|
|
254
|
+
console.log(`目标:${w.goal}`);
|
|
255
|
+
for (const s of w.instructions) console.log(` ${s}`);
|
|
256
|
+
if (w.context?.kbHits?.length) console.log(`查重命中:\n ${w.context.kbHits.slice(0, 6).join('\n ')}`);
|
|
257
|
+
if (w.context?.stderrTail) console.log(`上次门失败输出(尾 20 行):\n ${w.context.stderrTail}`);
|
|
258
|
+
console.log('约束:');
|
|
259
|
+
for (const c of w.constraints) console.log(` - ${c}`);
|
|
260
|
+
console.log('验收(脚本机器校验):');
|
|
261
|
+
for (const a of w.acceptance) console.log(` - ${a}`);
|
|
262
|
+
console.log(`\n完成工单后回跑:node .agents/scripts/pipeline-run.mjs next${run.docs?.entry ? '' : ''}`);
|
|
263
|
+
} else if (run.stopType === 'await-confirm' && run.awaitConfirm) {
|
|
264
|
+
const c = run.awaitConfirm;
|
|
265
|
+
console.log(`\n== 等待确认 ==${c.ledger ? `(文档:${c.doc},将经 confirm-doc 代录入台账)` : '(对话级确认,记 run 事件不进台账)'}`);
|
|
266
|
+
console.log(`要点:${c.points}`);
|
|
267
|
+
console.log(`用户确认后回跑:next --delegated "<用户原话>"`);
|
|
268
|
+
} else if (run.stopType === 'gate-fail') {
|
|
269
|
+
console.log(`\n== 门拒绝(exit 2,不绕不吞——修复后原路重跑 next)==`);
|
|
270
|
+
for (const p of problems || run.gateFail?.problems || []) console.log(` - ${p}`);
|
|
271
|
+
} else if (run.stopType === 'done') {
|
|
272
|
+
console.log(`\n== 完成 ==`);
|
|
273
|
+
const gates = run.events.filter((e) => e.type === 'gate');
|
|
274
|
+
const fails = gates.filter((e) => e.exit !== 0).length;
|
|
275
|
+
console.log(` 需求:${run.requirement}`);
|
|
276
|
+
console.log(` 门执行 ${gates.length} 道(非零 ${fails});确认 ${run.events.filter((e) => e.type === 'confirm').length} 次;提交 ${run.events.filter((e) => e.type === 'commit').length} 笔`);
|
|
277
|
+
for (const d of [run.docs?.entry, run.docs?.spec, run.docs?.plan].filter(Boolean)) console.log(` 文档:${d}(${docState(ctx, d) || '?'})`);
|
|
278
|
+
} else if (run.stopType === 'aborted') {
|
|
279
|
+
console.log(`\n== 已放弃(${run.abort?.to || ''})== ${run.abort?.note || ''}`);
|
|
280
|
+
}
|
|
281
|
+
const stopLine = run.stopType === 'work-order' ? `PIPELINE-STOP work-order ${run.runId} ${run.workOrder?.id}`
|
|
282
|
+
: run.stopType === 'await-confirm' ? `PIPELINE-STOP await-confirm ${run.runId} ${run.awaitConfirm?.doc || ''}`
|
|
283
|
+
: run.stopType === 'gate-fail' ? `PIPELINE-STOP gate-fail ${run.runId} ${run.stage}`
|
|
284
|
+
: run.stopType === 'aborted' ? `PIPELINE-STOP aborted ${run.runId}`
|
|
285
|
+
: `PIPELINE-STOP done ${run.runId}`;
|
|
286
|
+
console.log(stopLine.trim());
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── triage / 路径 ──────────────────────────────────────────────────────
|
|
290
|
+
export function parseKV(s) {
|
|
291
|
+
const out = {};
|
|
292
|
+
for (const part of String(s || '').split(/\s+/).filter(Boolean)) {
|
|
293
|
+
const i = part.indexOf('=');
|
|
294
|
+
if (i > 0) out[part.slice(0, i)] = part.slice(i + 1);
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function validateTriage(ctx, kv) {
|
|
300
|
+
const problems = [];
|
|
301
|
+
if (!KINDS.includes(kv.kind)) problems.push(`kind 须为 ${KINDS.join('|')}`);
|
|
302
|
+
if (!LEVELS.includes(kv.level)) problems.push(`level 须为 ${LEVELS.join('|')}`);
|
|
303
|
+
const mods = moduleList(ctx);
|
|
304
|
+
if (kv.module && !mods.includes(kv.module)) problems.push(`module 须在词表内:${mods.join('|')}`);
|
|
305
|
+
if (kv.topic && !/^[a-z0-9][a-z0-9-]*$/.test(kv.topic)) problems.push('topic 须为英文 kebab-case');
|
|
306
|
+
return problems;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── 状态机(next 的全部合法推进) ──────────────────────────────────────
|
|
310
|
+
function confirmGate(ctx, run, docRel, quote, to) {
|
|
311
|
+
const args = [docRel];
|
|
312
|
+
if (to) args.push('--to', to);
|
|
313
|
+
if (quote) args.push('--delegated', quote);
|
|
314
|
+
const r = runNode(ctx, run, 'confirm-doc.mjs', args);
|
|
315
|
+
if (r.exit !== 0) {
|
|
316
|
+
const tail = (r.stderr || r.stdout).split(/\r?\n/).filter(Boolean).slice(-6).join(' | ');
|
|
317
|
+
return { ok: false, msg: `confirm-doc exit ${r.exit}:${tail}` };
|
|
318
|
+
}
|
|
319
|
+
emit(run, { type: 'confirm', doc: docRel, to: to || nextConfirmTarget(docState(ctx, docRel), docRel), source: quote ? 'chat-delegated' : 'tty', ...(quote ? { quote } : {}) });
|
|
320
|
+
return { ok: true };
|
|
321
|
+
}
|
|
322
|
+
// 语义上 confirm-doc 自行决定目标态(approved/done/fixed/closed);事件里记录结果态
|
|
323
|
+
function nextConfirmTarget(curState, docRel) { return curState || 'approved'; }
|
|
324
|
+
|
|
325
|
+
// managedFaceCheck(复核 P2-4 修复 A′,2026-10-02 用户拍板):提交集命中 kit.json 台账面或包源
|
|
326
|
+
// templates/_agents/ 时——包源环境(bin/flow-kit.mjs 可达)自动跑 sync 刷台账并把 kit.json 与 sync
|
|
327
|
+
// 产物追加进提交集(窗口闭合);装户环境降级为显式 WARN(提示手动补 sync)。返回须追加的文件。
|
|
328
|
+
function managedFaceCheck(ctx, run, files) {
|
|
329
|
+
let managedRels = null;
|
|
330
|
+
try {
|
|
331
|
+
const kit = JSON.parse(fs.readFileSync(path.join(ctx.root, '.agents', 'kit.json'), 'utf8'));
|
|
332
|
+
managedRels = new Set((kit.managed || []).map((e) => e && e.rel).filter(Boolean));
|
|
333
|
+
} catch { return []; } // 无台账(夹具/裸库)不处理
|
|
334
|
+
const hit = files.some((f) => f.startsWith('templates/_agents/') || managedRels.has(f));
|
|
335
|
+
if (!hit) return [];
|
|
336
|
+
const flowKit = path.join(ctx.root, 'bin', 'flow-kit.mjs');
|
|
337
|
+
if (!fs.existsSync(flowKit)) {
|
|
338
|
+
const msg = '触及 managed 面(kit.json 台账)——装户环境无 flow-kit:提交后须手动跑 flow-kit sync 刷新台账,否则克隆/CI doctor 报漂移';
|
|
339
|
+
console.log('⚠️ ' + msg);
|
|
340
|
+
emit(run, { type: 'warn', note: msg });
|
|
341
|
+
return []; // 复核 P2-D:装户无刷新路径,把未刷新的 kit.json 卷入提交是 no-op 甚至带入中间态——只 WARN 不扩大提交面
|
|
342
|
+
}
|
|
343
|
+
const cmd = 'node bin/flow-kit.mjs sync';
|
|
344
|
+
const t0 = Date.now();
|
|
345
|
+
const p = spawnSync(process.execPath, [flowKit, 'sync'], { cwd: ctx.root, encoding: 'utf8', windowsHide: true });
|
|
346
|
+
const exit = p.status ?? 1;
|
|
347
|
+
emit(run, { type: 'gate', cmd, exit, ms: Date.now() - t0 });
|
|
348
|
+
if (exit !== 0) {
|
|
349
|
+
console.log(`⚠️ flow-kit sync exit ${exit}——提交将带旧台账,须事后手动补 sync`);
|
|
350
|
+
return ['.agents/kit.json'];
|
|
351
|
+
}
|
|
352
|
+
// sync 产物(.agents/ 与 templates/ 下非运行态改动)+ 台账一并入提交
|
|
353
|
+
const extra = ['.agents/kit.json', ...(changedFiles(ctx, run) || [])].filter((c) =>
|
|
354
|
+
(c.startsWith('.agents/') || c.startsWith('templates/')) &&
|
|
355
|
+
!RUNTIME_OWNED.some((ro) => (ro.endsWith('/') ? c.startsWith(ro) : c === ro)));
|
|
356
|
+
emit(run, { type: 'managed-sync', extra: [...new Set(extra)] });
|
|
357
|
+
return [...new Set(extra)];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function docsCommit(ctx, run, files, subject) {
|
|
361
|
+
const commitFiles = [...new Set([...files, ...managedFaceCheck(ctx, run, files)])];
|
|
362
|
+
const add = runGit(ctx, run, ['add', ...commitFiles]);
|
|
363
|
+
if (add.exit !== 0) return { ok: false, msg: `git add 失败:${add.stderr.slice(0, 200)}` };
|
|
364
|
+
const c = runGit(ctx, run, ['commit', '-m', subject]);
|
|
365
|
+
if (c.exit !== 0) return { ok: false, msg: `git commit 失败:${(c.stderr || c.stdout).split(/\r?\n/).filter(Boolean).slice(-4).join(' | ')}` };
|
|
366
|
+
const sha = runGit(ctx, run, ['rev-parse', '--short', 'HEAD']);
|
|
367
|
+
emit(run, { type: 'commit', sha: sha.stdout.trim(), subject });
|
|
368
|
+
return { ok: true };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function changedFiles(ctx, run) {
|
|
372
|
+
const r = runGit(ctx, run, ['status', '--porcelain', '-u']); // -u:展开 untracked 目录为逐文件(防「src/」折叠误报越权)
|
|
373
|
+
if (r.exit !== 0) return null;
|
|
374
|
+
const out = [];
|
|
375
|
+
for (const line of r.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
376
|
+
let p = line.slice(3).trim();
|
|
377
|
+
if (p.includes(' -> ')) {
|
|
378
|
+
// 复核 P2-2:rename 行收录 old+new 两路径——旧路径的删除同样属改动面(防改名逃逸越权)
|
|
379
|
+
for (const q of p.split(' -> ')) {
|
|
380
|
+
const n = q.trim().replace(/^"|"$/g, '').replace(/\\/g, '/');
|
|
381
|
+
if (n) out.push(n);
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
p = p.replace(/^"|"$/g, '').replace(/\\/g, '/');
|
|
386
|
+
out.push(p);
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 脚本自身管理的运行态路径——confirm/gate/gen-index 副产物,不属宿主越权面(沙箱演练实证,2026-10-02)
|
|
392
|
+
const RUNTIME_OWNED = ['.agents/cache/', '.agents/confirmations.jsonl', 'workflow/INDEX.md'];
|
|
393
|
+
|
|
394
|
+
function checkAuthorized(ctx, run, filesCsv) {
|
|
395
|
+
const listed = String(filesCsv || '').split(/[,,]/).map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean);
|
|
396
|
+
if (!listed.length) return { ok: false, problems: ['须以 --files 回填本工单实际改动的文件清单(逗号分隔)'] };
|
|
397
|
+
const changed = changedFiles(ctx, run);
|
|
398
|
+
if (changed === null) return { ok: false, problems: ['git status 失败,无法核对改动面'] };
|
|
399
|
+
const beyond = changed.filter((c) =>
|
|
400
|
+
!listed.includes(c) && !RUNTIME_OWNED.some((ro) => (ro.endsWith('/') ? c.startsWith(ro) : c === ro)));
|
|
401
|
+
if (beyond.length) return { ok: false, problems: [`git 改动超出回填清单(偏离即停):${beyond.join('、')}`] };
|
|
402
|
+
run.lastFiles = listed;
|
|
403
|
+
return { ok: true };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// locateDeployTarget:deploy 对象定位——--triage topic 显式优先,否则取 workflow/{intents,incidents}
|
|
407
|
+
// 最新终态入口(文件名日期序);同名 spec 存在才纳入(L1 无 spec 合法),plan 恒须同名在位。
|
|
408
|
+
function locateDeployTarget(ctx, run) {
|
|
409
|
+
const all = [];
|
|
410
|
+
for (const dir of ['intents', 'incidents']) {
|
|
411
|
+
const d = path.join(ctx.root, 'workflow', dir);
|
|
412
|
+
if (!fs.existsSync(d)) continue;
|
|
413
|
+
for (const f of fs.readdirSync(d)) {
|
|
414
|
+
if (!f.endsWith('.md') || f.startsWith('_')) continue;
|
|
415
|
+
const rel = `workflow/${dir}/${f}`;
|
|
416
|
+
if (['done', 'closed'].includes(docState(ctx, rel))) all.push({ rel, base: f.replace(/\.md$/, '') });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!all.length) return { entry: null };
|
|
420
|
+
all.sort((a, b) => b.base.localeCompare(a.base));
|
|
421
|
+
const topic = run.triage?.topic && run.triage.topic !== 'task' ? run.triage.topic : null;
|
|
422
|
+
// 复核 P2-C 收紧:topic 须与去日期前缀后的主题段等值——子串匹配会误命中(topic=run 命中 pipeline-run)
|
|
423
|
+
const hit = topic ? all.find((x) => x.base.replace(/^\d{4}-\d{2}-\d{2}-/, '') === topic) : all[0];
|
|
424
|
+
if (!hit) return { entry: null };
|
|
425
|
+
const specRel = `workflow/specs/${hit.base}.md`;
|
|
426
|
+
return {
|
|
427
|
+
entry: hit.rel,
|
|
428
|
+
spec: fs.existsSync(path.join(ctx.root, specRel)) ? specRel : null,
|
|
429
|
+
plan: `workflow/plans/${hit.base}.md`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function briefReq(run) { return run.requirement.replace(/\s+/g, ' ').slice(0, 40); }
|
|
434
|
+
|
|
435
|
+
function kbGate(ctx, run, types) {
|
|
436
|
+
const words = (run.requirement.toLowerCase().match(/[a-z0-9]{2,}/g) || []).slice(0, 2);
|
|
437
|
+
const kw = words.length ? words : [run.requirement.slice(0, 6)];
|
|
438
|
+
const r = runNode(ctx, run, 'kb-search.mjs', [...kw, '--scope', 'workflow', '--type', types, '-n', '6']);
|
|
439
|
+
const hits = r.stdout.split(/\r?\n/).filter((l) => l.startsWith('📄')).slice(0, 6);
|
|
440
|
+
return { exit: r.exit, hits };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function advance(ctx, run, opt) {
|
|
444
|
+
const L = () => run.triage.level;
|
|
445
|
+
const K = () => run.triage.kind;
|
|
446
|
+
const entry = () => run.docs.entry;
|
|
447
|
+
const topic = () => run.triage.topic;
|
|
448
|
+
|
|
449
|
+
// 工单型停机点的复验(进入对应阶段时先校验上一工单产出)
|
|
450
|
+
while (true) {
|
|
451
|
+
const s = run.stage;
|
|
452
|
+
|
|
453
|
+
if (s === 'triage') {
|
|
454
|
+
if (!run.triage) {
|
|
455
|
+
if (!opt.triage) {
|
|
456
|
+
issueWO(ctx, run, wo(run, {
|
|
457
|
+
kind: 'triage', title: '判级与分类', goal: '读需求与 new-task.md 判级闭集,输出 kind/level/module/topic',
|
|
458
|
+
instructions: [
|
|
459
|
+
'1. 读 .agents/commands/new-task.md §级别判断(L2 触达面闭集 / L1 默认档 / L3 结构面)',
|
|
460
|
+
`2. 对需求定级:「${run.requirement}」${run.hint ? `(预判参考:${run.hint})` : ''}`,
|
|
461
|
+
'3. 回填:next --triage "kind=require|fix|verify|review|deploy level=L0-L3 module=<词表> topic=<英文kebab>"',
|
|
462
|
+
],
|
|
463
|
+
context: { docPath: '.agents/commands/new-task.md', kbHits: [], stderrTail: '', relatedDocs: ['workflow/INDEX.md'] },
|
|
464
|
+
constraints: ['判级就高不就低;勾红线必 L2+;module 取 .agents/workflow-modules.txt 词表'],
|
|
465
|
+
acceptance: ['--triage 三值枚举合法(脚本校验)', '红线词命中而 level<L2 会打 warning(回填者负责就高)'],
|
|
466
|
+
}), 'triage');
|
|
467
|
+
}
|
|
468
|
+
const kv = parseKV(opt.triage);
|
|
469
|
+
const problems = validateTriage(ctx, kv);
|
|
470
|
+
if (problems.length) stopGateFail(ctx, run, 'triage', problems);
|
|
471
|
+
if (/红线|契约|schema|公共接口/.test(run.requirement) && LEVELS.indexOf(kv.level) < LEVELS.indexOf('L2')) {
|
|
472
|
+
console.log('⚠️ warning:需求文本命中红线关键词而 level<L2——判级应就高(继续,但责任在回填者)');
|
|
473
|
+
}
|
|
474
|
+
run.triage = { kind: kv.kind, level: kv.level, module: kv.module || moduleList(ctx)[0], topic: kv.topic || 'task', decidedBy: run.hint ? 'hint+ai' : 'ai', decidedAt: nowIso() };
|
|
475
|
+
emit(run, { type: 'triage', ...run.triage });
|
|
476
|
+
run.docs = run.docs || { entry: null, spec: null, plan: null };
|
|
477
|
+
// 路由
|
|
478
|
+
run.stage = K() === 'verify' ? 'verify' : K() === 'review' ? 'review-only' : K() === 'deploy' ? 'deploy-check'
|
|
479
|
+
: K() === 'require' && L() === 'L0' ? 'direct-edit'
|
|
480
|
+
: K() === 'fix' ? 'incident-fill' : 'intent-fill';
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
run.stage = 'intent-fill'; continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (s === 'intent-fill' || s === 'incident-fill') {
|
|
487
|
+
const isInc = s === 'incident-fill';
|
|
488
|
+
const rel = isInc ? `workflow/incidents/${today()}-${topic()}.md` : `workflow/intents/${today()}-${topic()}.md`;
|
|
489
|
+
run.docs.entry = rel;
|
|
490
|
+
const st = docState(ctx, rel);
|
|
491
|
+
if (!st) {
|
|
492
|
+
const kb = kbGate(ctx, run, isInc ? 'incidents,plans' : 'intents,specs,plans');
|
|
493
|
+
run.lastKbHits = kb.hits;
|
|
494
|
+
if (isInc) {
|
|
495
|
+
// incident 无 fill-* 工具:从 _TEMPLATE.md 复制骨架(机器步骤,记事件)
|
|
496
|
+
const tplPath = path.join(ctx.root, 'workflow', 'incidents', '_TEMPLATE.md');
|
|
497
|
+
try {
|
|
498
|
+
fs.copyFileSync(tplPath, path.join(ctx.root, rel));
|
|
499
|
+
emit(run, { type: 'gate', cmd: `cp workflow/incidents/_TEMPLATE.md ${rel}`, exit: 0, ms: 0 });
|
|
500
|
+
} catch (e) { stopGateFail(ctx, run, s, [`incident 模板复制失败:${e.message}`]); }
|
|
501
|
+
} else {
|
|
502
|
+
const r = runNode(ctx, run, 'fill-intent.mjs', ['--module', run.triage.module, '--level', L(), '--topic', topic(), '--output', rel, '--notes', `pipeline-run ${run.runId}`]);
|
|
503
|
+
if (r.exit !== 0) stopGateFail(ctx, run, s, [`fill-intent exit ${r.exit}:${(r.stderr || r.stdout).split(/\r?\n/).slice(-3).join(' | ')}`]);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
run.stage = isInc ? 'incident-draft' : 'intent-draft';
|
|
507
|
+
issueWO(ctx, run, wo(run, {
|
|
508
|
+
kind: 'draft', title: `起草 ${isInc ? 'incident' : 'intent'}:${rel}`, goal: isInc ? '填实事故 4 节 + 复盘三件套' : `填实 intent 各节(${L()} 省节规则见模板注释)`,
|
|
509
|
+
instructions: isInc ? [
|
|
510
|
+
'1. 按模板填:时间线 / 根因 / 为什么没拦住 / 复盘三件套',
|
|
511
|
+
'2. 先查同类:kb-search 命中已附(如有)',
|
|
512
|
+
'3. 完成后回跑 next——脚本校验 frontmatter/节完整/占位符',
|
|
513
|
+
] : [
|
|
514
|
+
'1. 骨架已由 fill-intent 生成,填实各节正文(背景/教训/目标/非目标/约束/验收)',
|
|
515
|
+
'2. 验收标准逐条可测试(写不出判据=还没想清楚)',
|
|
516
|
+
'3. 查重命中(如有)在「历史教训/防复发」节引用',
|
|
517
|
+
'4. 完成后回跑 next——脚本校验 frontmatter/节完整/占位符',
|
|
518
|
+
],
|
|
519
|
+
context: { docPath: rel, kbHits: run.lastKbHits || [], stderrTail: '', relatedDocs: ['.agents/commands/plan.md'] },
|
|
520
|
+
constraints: ['只改该文档本身;命名英文 kebab-case;正文不写「状态:/级别:」行'],
|
|
521
|
+
acceptance: ['frontmatter 键完整', '必填节非空(L1 省节合法)', '无模板占位符残留'],
|
|
522
|
+
}), run.stage);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (s === 'intent-draft' || s === 'incident-draft') {
|
|
526
|
+
const kind = s === 'incident-draft' ? 'incident' : 'intent';
|
|
527
|
+
const problems = validateDraftContent(readDoc(ctx, entry()), kind, L());
|
|
528
|
+
if (problems.length) stopGateFail(ctx, run, s, problems);
|
|
529
|
+
const text = readDoc(ctx, entry());
|
|
530
|
+
run.stage = kind === 'incident' ? 'incident-confirm' : 'intent-confirm';
|
|
531
|
+
stopConfirm(ctx, run, entry(), summarizeDoc(text), kind !== 'incident');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (s === 'intent-confirm' || s === 'incident-confirm') {
|
|
535
|
+
const ledger = s !== 'incident-confirm';
|
|
536
|
+
if (!opt.delegated) stopConfirm(ctx, run, entry(), run.awaitConfirm?.points || summarizeDoc(readDoc(ctx, entry()) || ''), ledger);
|
|
537
|
+
if (ledger) {
|
|
538
|
+
const r = confirmGate(ctx, run, entry(), opt.delegated);
|
|
539
|
+
if (!r.ok) stopGateFail(ctx, run, s, [r.msg]);
|
|
540
|
+
} else {
|
|
541
|
+
emit(run, { type: 'confirm', doc: entry(), to: 'draft-ok', source: 'conversation', quote: opt.delegated });
|
|
542
|
+
}
|
|
543
|
+
run.stage = L() === 'L1' || K() === 'fix' ? 'plan-fill' : 'spec-fill';
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (s === 'spec-fill') {
|
|
548
|
+
const rel = `workflow/specs/${today()}-${topic()}.md`;
|
|
549
|
+
run.docs.spec = rel;
|
|
550
|
+
if (!docState(ctx, rel)) {
|
|
551
|
+
const r = runNode(ctx, run, 'fill-spec.mjs', ['--level', L(), '--topic', topic(), '--output', rel]);
|
|
552
|
+
if (r.exit !== 0) stopGateFail(ctx, run, s, [`fill-spec exit ${r.exit}(G1 前置门或参数):${(r.stderr || r.stdout).split(/\r?\n/).slice(-3).join(' | ')}`]);
|
|
553
|
+
}
|
|
554
|
+
run.stage = 'spec-draft';
|
|
555
|
+
issueWO(ctx, run, wo(run, {
|
|
556
|
+
kind: 'draft', title: `起草 spec:${rel}`, goal: '设计规格 4 节(功能行为/数据流/系统改动/约束遵守映射)',
|
|
557
|
+
instructions: [
|
|
558
|
+
'1. 骨架已由 fill-spec 生成,填实 4 节',
|
|
559
|
+
'2. 约束遵守映射逐条对照根 AGENTS.md 红线,逐条回应',
|
|
560
|
+
'3. 多方案取舍列选项交用户(spec 确认时定)',
|
|
561
|
+
'4. 完成后回跑 next——脚本机器校验',
|
|
562
|
+
],
|
|
563
|
+
context: { docPath: rel, kbHits: [], stderrTail: '', relatedDocs: ['.agents/commands/design.md'] },
|
|
564
|
+
constraints: ['只改该文档;页面/交互形态禁止自行推定'],
|
|
565
|
+
acceptance: ['4 节齐全非空', '无占位符残留'],
|
|
566
|
+
}), 'spec-draft');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (s === 'spec-draft') {
|
|
570
|
+
const problems = validateDraftContent(readDoc(ctx, run.docs.spec), 'spec', L());
|
|
571
|
+
if (problems.length) stopGateFail(ctx, run, s, problems);
|
|
572
|
+
run.stage = L() === 'L3' ? 'spec-review' : 'spec-confirm';
|
|
573
|
+
if (L() === 'L3') {
|
|
574
|
+
issueWO(ctx, run, wo(run, {
|
|
575
|
+
kind: 'review', title: `独立复核 spec:${run.docs.spec}`, goal: 'L3 强制:新上下文只读复核,结论写入 spec「确认与复核」节',
|
|
576
|
+
instructions: [
|
|
577
|
+
'1. 以独立视角读 spec 全文与入口 intent',
|
|
578
|
+
'2. 找会破坏方案的缺口(问「什么会坏」而非「好不好」)',
|
|
579
|
+
'3. 结论(P0/P1 清单或「无」)写入目标文档「## 确认与复核」节(无此节则在文末追加)',
|
|
580
|
+
'4. 完成后回跑 next——脚本校验结论在节',
|
|
581
|
+
],
|
|
582
|
+
context: { docPath: run.docs.spec, kbHits: [], stderrTail: '', relatedDocs: [] },
|
|
583
|
+
constraints: ['只读复核 + 仅在 spec 确认与复核节追加结论;不改其他内容'],
|
|
584
|
+
acceptance: ['spec「确认与复核」节含复核结论(P0/P1 或 无)'],
|
|
585
|
+
}), 'spec-review');
|
|
586
|
+
}
|
|
587
|
+
stopConfirm(ctx, run, run.docs.spec, summarizeDoc(readDoc(ctx, run.docs.spec)), true);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (s === 'spec-review') {
|
|
591
|
+
const body = sectionBody(readDoc(ctx, run.docs.spec) || '', '确认与复核') || '';
|
|
592
|
+
if (!(/P0\s*\/\s*P1/.test(body) || /P0/.test(body) || /P1/.test(body))) stopGateFail(ctx, run, s, ['spec「确认与复核」节缺复核结论(须含 P0/P1 清单或「无 P0/P1」)']);
|
|
593
|
+
run.stage = 'spec-confirm';
|
|
594
|
+
stopConfirm(ctx, run, run.docs.spec, summarizeDoc(readDoc(ctx, run.docs.spec)), true);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (s === 'spec-confirm') {
|
|
598
|
+
if (!opt.delegated) stopConfirm(ctx, run, run.docs.spec, run.awaitConfirm?.points || '', true);
|
|
599
|
+
const r = confirmGate(ctx, run, run.docs.spec, opt.delegated);
|
|
600
|
+
if (!r.ok) stopGateFail(ctx, run, s, [r.msg]);
|
|
601
|
+
run.stage = 'plan-fill'; continue;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (s === 'plan-fill') {
|
|
605
|
+
const rel = `workflow/plans/${today()}-${topic()}.md`;
|
|
606
|
+
run.docs.plan = rel;
|
|
607
|
+
if (!docState(ctx, rel)) {
|
|
608
|
+
const r = runNode(ctx, run, 'fill-plan.mjs', ['--level', L() === 'L0' ? 'L1' : L(), '--topic', topic(), '--output', rel]);
|
|
609
|
+
if (r.exit !== 0) stopGateFail(ctx, run, s, [`fill-plan exit ${r.exit}(G1 前置门或参数):${(r.stderr || r.stdout).split(/\r?\n/).slice(-3).join(' | ')}`]);
|
|
610
|
+
}
|
|
611
|
+
run.stage = 'plan-draft';
|
|
612
|
+
issueWO(ctx, run, wo(run, {
|
|
613
|
+
kind: 'draft', title: `起草 plan:${rel}`, goal: `计划文档(${L() === 'L1' ? 'Quick-Plan 三节' : '完整 4 节:改动方案/任务拆解/执行顺序/验证计划'})`,
|
|
614
|
+
instructions: [
|
|
615
|
+
'1. 骨架已由 fill-plan 生成,填实各节',
|
|
616
|
+
'2. 改动方案逐文件列「做什么+判据」——后续 implement 工单的授权面以此为准',
|
|
617
|
+
'3. 验证计划对齐入口验收标准',
|
|
618
|
+
'4. 完成后回跑 next——脚本机器校验',
|
|
619
|
+
],
|
|
620
|
+
context: { docPath: rel, kbHits: [], stderrTail: '', relatedDocs: ['.agents/commands/build.md'] },
|
|
621
|
+
constraints: ['只改该文档'],
|
|
622
|
+
acceptance: ['节齐全非空', '无占位符残留'],
|
|
623
|
+
}), 'plan-draft');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (s === 'plan-draft') {
|
|
627
|
+
const problems = validateDraftContent(readDoc(ctx, run.docs.plan), 'plan', L());
|
|
628
|
+
if (problems.length) stopGateFail(ctx, run, s, problems);
|
|
629
|
+
run.stage = 'plan-confirm';
|
|
630
|
+
stopConfirm(ctx, run, run.docs.plan, summarizeDoc(readDoc(ctx, run.docs.plan)), true);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (s === 'plan-confirm') {
|
|
634
|
+
if (!opt.delegated) stopConfirm(ctx, run, run.docs.plan, run.awaitConfirm?.points || '', true);
|
|
635
|
+
const r = confirmGate(ctx, run, run.docs.plan, opt.delegated);
|
|
636
|
+
if (!r.ok) stopGateFail(ctx, run, s, [r.msg]);
|
|
637
|
+
// 批次一:入口(+spec)+plan 全 approved → 一笔 docs 提交(配对门要求树内成对)
|
|
638
|
+
const files = [entry(), run.docs.spec, run.docs.plan].filter(Boolean);
|
|
639
|
+
const subject = `docs(workflow): ${topic()} ${K() === 'fix' ? 'incident+plan' : run.docs.spec ? '三件套' : '两件套'} approved——${briefReq(run)}`;
|
|
640
|
+
const c = docsCommit(ctx, run, files, subject);
|
|
641
|
+
if (!c.ok) stopGateFail(ctx, run, s, [c.msg]);
|
|
642
|
+
run.stage = 'changes-confirm';
|
|
643
|
+
stopConfirm(ctx, run, run.docs.plan, (sectionBody(readDoc(ctx, run.docs.plan) || '', '改动方案') || '').split(/\r?\n/).slice(0, 5).join(' / ').slice(0, 200), false);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (s === 'changes-confirm') {
|
|
647
|
+
if (!opt.delegated) stopConfirm(ctx, run, run.awaitConfirm?.doc || '', run.awaitConfirm?.points || '', false);
|
|
648
|
+
emit(run, { type: 'confirm', doc: '改动清单', to: 'ok', source: 'conversation', quote: opt.delegated });
|
|
649
|
+
run.stage = 'implement';
|
|
650
|
+
const planFiles = (readDoc(ctx, run.docs.plan).match(/[\w./-]+\.(?:mjs|md|json|txt|cjs|sh|yml|yaml)/g) || []);
|
|
651
|
+
issueWO(ctx, run, wo(run, {
|
|
652
|
+
kind: 'implement', title: `实现:${briefReq(run)}`, goal: '按 plan 改动方案实现 + 自验',
|
|
653
|
+
instructions: [
|
|
654
|
+
'1. 按 plan §改动方案逐文件实现(判据照条目)',
|
|
655
|
+
'2. 自验:跑 plan §验证计划所列静态门命令',
|
|
656
|
+
'3. 偏离即停:改公共接口/计划外文件/新依赖 → 不做,停机向用户上报',
|
|
657
|
+
'4. 完成后回填改动文件清单:next --files "a.mjs,b.md"',
|
|
658
|
+
],
|
|
659
|
+
context: { docPath: run.docs.plan, kbHits: [], stderrTail: '', relatedDocs: planFiles.slice(0, 8) },
|
|
660
|
+
constraints: [`授权文件以 plan 改动方案为准;回填 --files 清单必须与 git status 实际改动一致(脚本核对)`],
|
|
661
|
+
acceptance: ['git 改动 ⊆ 回填清单', '后续 verify 门(npm test + check-loop 经 verify.mjs)全绿'],
|
|
662
|
+
}), 'implement');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (s === 'implement' || s === 'fix') {
|
|
666
|
+
if (!opt.files) { printRun(ctx, run); process.exit(0); } // 无 --files 重入:重发当前工单(run.workOrder 保留)
|
|
667
|
+
const a = checkAuthorized(ctx, run, opt.files);
|
|
668
|
+
if (!a.ok) stopGateFail(ctx, run, s, a.problems);
|
|
669
|
+
run.stage = 'verify'; continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (s === 'verify') {
|
|
673
|
+
const r = runNode(ctx, run, 'verify.mjs', []);
|
|
674
|
+
if (K() === 'verify') { // verify-only 短路径:不带修复环,绿即收、败即停
|
|
675
|
+
if (r.exit === 0) stopDone(ctx, run);
|
|
676
|
+
stopGateFail(ctx, run, s, ['verify 未过(verify-only 不带修复环——人工处理后重跑,或立正式任务走闭环)']);
|
|
677
|
+
}
|
|
678
|
+
if (r.exit === 0) {
|
|
679
|
+
run.fixLoop = { count: 0, cap: FIX_CAP };
|
|
680
|
+
// 代码提交(沙箱演练实证的缺口,2026-10-02):verify 绿后、复核/关单前落一笔——
|
|
681
|
+
// 文件集 = 当前非运行态、非 workflow 文档的改动(均已过 --files 授权核对)
|
|
682
|
+
const codeFiles = (changedFiles(ctx, run) || []).filter((c) =>
|
|
683
|
+
!RUNTIME_OWNED.some((ro) => (ro.endsWith('/') ? c.startsWith(ro) : c === ro)) && !c.startsWith('workflow/'));
|
|
684
|
+
if (codeFiles.length) {
|
|
685
|
+
const verb = K() === 'fix' ? 'fix' : 'feat';
|
|
686
|
+
const c = docsCommit(ctx, run, codeFiles, `${verb}(${run.triage.module}): ${briefReq(run)}(pipeline-run ${run.runId})`);
|
|
687
|
+
if (!c.ok) stopGateFail(ctx, run, s, [c.msg]);
|
|
688
|
+
}
|
|
689
|
+
run.stage = (L() === 'L2' || L() === 'L3') ? 'review' : 'closeout'; continue;
|
|
690
|
+
}
|
|
691
|
+
run.fixLoop = run.fixLoop || { count: 0, cap: FIX_CAP };
|
|
692
|
+
run.fixLoop.count += 1;
|
|
693
|
+
const tail = (r.stderr || r.stdout).split(/\r?\n/).filter(Boolean).slice(-20).join('\n');
|
|
694
|
+
if (run.fixLoop.count >= run.fixLoop.cap) {
|
|
695
|
+
stopGateFail(ctx, run, s, [`修复环超限(${run.fixLoop.cap} 次)——人工介入或 abort`]);
|
|
696
|
+
}
|
|
697
|
+
run.stage = 'fix';
|
|
698
|
+
issueWO(ctx, run, wo(run, {
|
|
699
|
+
kind: 'fix', title: `修复环 ${run.fixLoop.count}/${run.fixLoop.cap}:verify 未过`, goal: '按失败输出修复后回跑',
|
|
700
|
+
instructions: [
|
|
701
|
+
'1. 读下方「上次门失败输出」定位失败项',
|
|
702
|
+
'2. 修复(授权面仍以 plan 为准,偏离即停)',
|
|
703
|
+
'3. 完成后回填:next --files "…"(脚本重核对改动面并重跑 verify)',
|
|
704
|
+
],
|
|
705
|
+
context: { docPath: run.docs.plan, kbHits: [], stderrTail: tail, relatedDocs: [] },
|
|
706
|
+
constraints: ['不绕门不改门;不 --no-verify'],
|
|
707
|
+
acceptance: ['git 改动 ⊆ 回填清单', 'verify.mjs 重跑全绿'],
|
|
708
|
+
}), 'fix');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (s === 'review') {
|
|
712
|
+
const target = run.docs.spec || entry();
|
|
713
|
+
const body = sectionBody(readDoc(ctx, target) || '', '确认与复核') || '';
|
|
714
|
+
if (!(/P0\s*\/\s*P1/.test(body) || /P0/.test(body) || /P1/.test(body))) {
|
|
715
|
+
issueWO(ctx, run, wo(run, {
|
|
716
|
+
kind: 'review', title: `独立复核(L2/L3):实现与文档对照`, goal: '读入口+spec+plan+diff,产出 P0/P1/P2 清单并把结论写入 spec「确认与复核」节',
|
|
717
|
+
instructions: [
|
|
718
|
+
'1. 独立读入口/spec/plan 与本次 diff(git diff + 相关文件)',
|
|
719
|
+
'2. 逐项对照验收标准找缺口(file:line 证据)',
|
|
720
|
+
'3. 结论写入目标文档「## 确认与复核」节(P0/P1 清单或「无」;无此节则在文末追加)',
|
|
721
|
+
'4. 定性与合入时机由用户拍板——复核只交证据',
|
|
722
|
+
],
|
|
723
|
+
context: { docPath: target, kbHits: [], stderrTail: '', relatedDocs: [entry(), run.docs.plan].filter(Boolean) },
|
|
724
|
+
constraints: ['只读 + 仅在 spec 确认与复核节追加结论'],
|
|
725
|
+
acceptance: ['spec「确认与复核」节含结论(P0/P1 或 无)'],
|
|
726
|
+
}), 'review');
|
|
727
|
+
}
|
|
728
|
+
run.stage = K() === 'fix' ? 'closeout' : 'closeout';
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (s === 'closeout') {
|
|
733
|
+
if (K() !== 'fix') {
|
|
734
|
+
const co = closeoutComplete(readDoc(ctx, entry()) || '');
|
|
735
|
+
if (!co.ok) {
|
|
736
|
+
issueWO(ctx, run, wo(run, {
|
|
737
|
+
kind: 'closeout', title: `勾验收补证据:${entry()}`, goal: '关单前逐条勾验入口文档验收标准并补证据',
|
|
738
|
+
instructions: [
|
|
739
|
+
'1. 逐条核验「## 验收标准」:达成改 [x] 并补「(证据:<commit SHA/测试名/输出>)」',
|
|
740
|
+
'2. 未达成的不得勾——如实留 [ ] 并在下方说明',
|
|
741
|
+
'3. 完成后回跑 next(脚本校验全勾+证据在)',
|
|
742
|
+
],
|
|
743
|
+
context: { docPath: entry(), kbHits: [], stderrTail: '', relatedDocs: [run.docs.plan].filter(Boolean) },
|
|
744
|
+
constraints: ['只改入口文档验收节;证据须真实(SHA/用例名/命令输出)'],
|
|
745
|
+
acceptance: ['验收行全 [x] 且每条含 证据:'],
|
|
746
|
+
}), 'closeout');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
run.stage = K() === 'fix' ? 'incident-fixed-confirm' : 'done-confirm';
|
|
750
|
+
stopConfirm(ctx, run, entry(), '关单确认(done)——逐份原话', true);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (s === 'done-confirm' || s === 'incident-fixed-confirm' || s === 'incident-closed-confirm') {
|
|
754
|
+
const finalize = () => {
|
|
755
|
+
runNode(ctx, run, 'gen-workflow-index.mjs', []);
|
|
756
|
+
const files = [entry(), run.docs.spec, run.docs.plan].filter(Boolean);
|
|
757
|
+
const c = docsCommit(ctx, run, [...files, 'workflow/INDEX.md'], `docs(workflow): ${topic()} 关单——终态落定(pipeline-run ${run.runId})`);
|
|
758
|
+
if (!c.ok) stopGateFail(ctx, run, s, [c.msg]);
|
|
759
|
+
stopDone(ctx, run);
|
|
760
|
+
};
|
|
761
|
+
const pendOf = () => {
|
|
762
|
+
const pend = [];
|
|
763
|
+
if (K() === 'fix') {
|
|
764
|
+
const st = docState(ctx, entry());
|
|
765
|
+
if (!['fixed', 'closed'].includes(st)) pend.push(entry());
|
|
766
|
+
if (st === 'fixed') pend.push(entry()); // 两跳:fixed 之后还需 closed
|
|
767
|
+
if (run.docs.plan && !TERMINAL.includes(docState(ctx, run.docs.plan))) pend.push(run.docs.plan);
|
|
768
|
+
} else {
|
|
769
|
+
for (const rel of [entry(), run.docs.spec, run.docs.plan].filter(Boolean)) {
|
|
770
|
+
if (!TERMINAL.includes(docState(ctx, rel))) pend.push(rel);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return pend;
|
|
774
|
+
};
|
|
775
|
+
const label = K() === 'fix' ? 'incident 两跳(open→fixed→closed)与 plan 关单' : 'done 关单';
|
|
776
|
+
const pend0 = pendOf();
|
|
777
|
+
if (!pend0.length) finalize();
|
|
778
|
+
if (!opt.delegated) stopConfirm(ctx, run, pend0[0], `${label}——逐份原话,一次一份(余 ${pend0.length} 份)`, true);
|
|
779
|
+
const r = confirmGate(ctx, run, pend0[0], opt.delegated);
|
|
780
|
+
if (!r.ok) stopGateFail(ctx, run, s, [r.msg]);
|
|
781
|
+
const rest = pendOf(); // 一次一份:确认成功即停机,下一份须新原话(confirm-gate-one-per-call 教训)
|
|
782
|
+
if (!rest.length) finalize();
|
|
783
|
+
stopConfirm(ctx, run, rest[0], `${label}——逐份原话,一次一份(余 ${rest.length} 份)`, true);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (s === 'direct-edit') {
|
|
787
|
+
if (!opt.files) {
|
|
788
|
+
issueWO(ctx, run, wo(run, {
|
|
789
|
+
kind: 'implement', title: `L0 直改:${briefReq(run)}`, goal: 'docs/样式微调,无行为影响——直接改后提交',
|
|
790
|
+
instructions: ['1. 按需求直接修改目标文件', '2. 完成后回填:next --files "…"(脚本核对改动面并走 pre-commit 提交)'],
|
|
791
|
+
context: { docPath: '', kbHits: run.lastKbHits || [], stderrTail: '', relatedDocs: [] },
|
|
792
|
+
constraints: ['L0 语义:无行为影响;越此范围=判级错误,停机上报改判'],
|
|
793
|
+
acceptance: ['git 改动 ⊆ 回填清单', 'pre-commit 门通过'],
|
|
794
|
+
}), 'direct-edit');
|
|
795
|
+
}
|
|
796
|
+
const a = checkAuthorized(ctx, run, opt.files);
|
|
797
|
+
if (!a.ok) stopGateFail(ctx, run, s, a.problems);
|
|
798
|
+
const c = docsCommit(ctx, run, run.lastFiles, `docs(${run.triage.module}): ${briefReq(run)}(L0,pipeline-run ${run.runId})`);
|
|
799
|
+
if (!c.ok) stopGateFail(ctx, run, s, [c.msg]);
|
|
800
|
+
stopDone(ctx, run);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (s === 'verify-only') { run.stage = 'verify'; continue; }
|
|
804
|
+
if (s === 'review-only') {
|
|
805
|
+
if (opt.done) stopDone(ctx, run); // 评审清单交付即收(宿主向用户汇报后回填 --done)
|
|
806
|
+
issueWO(ctx, run, wo(run, {
|
|
807
|
+
kind: 'review', title: '横切评审', goal: '按 .agents/commands/review.md 产出 P0/P1/P2 分级清单交用户定性',
|
|
808
|
+
instructions: ['1. 读 review.md 与评审对象', '2. 机器兜底项先跑门,AI 自查项列清单', '3. 结论交用户定性与合入时机'],
|
|
809
|
+
context: { docPath: '.agents/commands/review.md', kbHits: [], stderrTail: '', relatedDocs: [] },
|
|
810
|
+
constraints: ['只读评审;定性归用户'],
|
|
811
|
+
acceptance: ['清单产出(P0/P1/P2 分级)'],
|
|
812
|
+
}), 'review-only');
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (s === 'deploy-check') {
|
|
816
|
+
// 复核 P1-2 补实现(2026-10-02 用户拍板):deploy-prep 工单 + 三件 done 机器校验(spec §功能行为 deploy-prep 变体)
|
|
817
|
+
const target = locateDeployTarget(ctx, run);
|
|
818
|
+
if (!target.entry) stopGateFail(ctx, run, s, ['找不到可上线的对象——workflow/{intents,incidents} 无终态(done/closed)入口;或以 --triage topic=<主题> 显式指定']);
|
|
819
|
+
run.deployTarget = target;
|
|
820
|
+
issueWO(ctx, run, wo(run, {
|
|
821
|
+
kind: 'deploy-prep', title: `上线清单核对:${target.entry}`, goal: '回归清单人工核对 + 三件同名 done 机器校验,全过到授权点',
|
|
822
|
+
instructions: [
|
|
823
|
+
'1. 核对 workflow/regression-checklist.md 回归必过条目(活文档)',
|
|
824
|
+
'2. 抽查最近历史 incident 的防复发条目',
|
|
825
|
+
'3. 完成后回跑 next——脚本机器校验:check-loop 门 + 三件同名 done',
|
|
826
|
+
'4. 全过停授权点;打 tag 由宿主按 git 纪律执行(脚本不代办)',
|
|
827
|
+
],
|
|
828
|
+
context: { docPath: 'workflow/regression-checklist.md', kbHits: [], stderrTail: '', relatedDocs: [target.entry, target.spec, target.plan].filter(Boolean) },
|
|
829
|
+
constraints: ['只读核对;发现缺口停机上报,不带病上线'],
|
|
830
|
+
acceptance: ['check-loop 门 exit 0(脚本执行)', '三件同名 done:入口+spec(如有)+plan 全终态(脚本执行)'],
|
|
831
|
+
}), 'deploy-prep');
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (s === 'deploy-prep') {
|
|
835
|
+
const t = run.deployTarget;
|
|
836
|
+
const r = runNode(ctx, run, 'check-loop.mjs', []);
|
|
837
|
+
if (r.exit !== 0) stopGateFail(ctx, run, s, ['check-loop 未过——文档闭环有断档,先补齐再谈上线']);
|
|
838
|
+
const gaps = [];
|
|
839
|
+
const entrySt = docState(ctx, t.entry);
|
|
840
|
+
if (!['done', 'closed'].includes(entrySt)) gaps.push(`${t.entry}(${entrySt || '缺失'}——须 done/closed)`);
|
|
841
|
+
if (t.spec) {
|
|
842
|
+
const st = docState(ctx, t.spec);
|
|
843
|
+
if (st !== 'done') gaps.push(`${t.spec}(${st || '缺失'}——须 done)`);
|
|
844
|
+
}
|
|
845
|
+
const planSt = docState(ctx, t.plan);
|
|
846
|
+
if (planSt !== 'done') gaps.push(`${t.plan}(${planSt || '缺失'}——须 done)`);
|
|
847
|
+
if (gaps.length) stopGateFail(ctx, run, s, [`三件同名 done 未满足(spec §校验规则 deploy-prep 行):${gaps.join(';')}`]);
|
|
848
|
+
run.stage = 'deploy-await';
|
|
849
|
+
stopConfirm(ctx, run, '', `上线授权——对象 ${t.entry}(仅用户本人;tag 由宿主按 git 纪律执行,脚本不代办)`, false);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (s === 'deploy-await') {
|
|
853
|
+
if (!opt.delegated) stopConfirm(ctx, run, '', `上线授权——对象 ${run.deployTarget?.entry || ''}:用户明确说「上」后回跑 next --delegated "<原话>";打 tag 由宿主执行`, false);
|
|
854
|
+
emit(run, { type: 'confirm', doc: 'deploy', to: 'authorized', source: 'conversation', quote: opt.delegated });
|
|
855
|
+
stopDone(ctx, run);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (s === 'done') stopDone(ctx, run);
|
|
859
|
+
fail(`未知阶段:${s}(run 文件可能损坏——status --adopt 重建,或删 run 重来)`, 1);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ── 子命令 ─────────────────────────────────────────────────────────────
|
|
864
|
+
const BOOL_FLAGS = new Set(['adopt', 'json', 'done']); // 复核 P2-5:布尔旗标不吞下一参
|
|
865
|
+
function parseArgs(argv) {
|
|
866
|
+
const out = { cmd: argv[0], positional: [] };
|
|
867
|
+
for (let i = 1; i < argv.length; i++) {
|
|
868
|
+
const a = argv[i];
|
|
869
|
+
if (a.startsWith('--')) {
|
|
870
|
+
const k = a.slice(2);
|
|
871
|
+
if (BOOL_FLAGS.has(k)) out[k] = true; else out[k] = argv[++i] ?? true;
|
|
872
|
+
} else out.positional.push(a);
|
|
873
|
+
}
|
|
874
|
+
return out;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function resolveRun(ctx, a) {
|
|
878
|
+
const id = a.run || latestRunId(ctx);
|
|
879
|
+
if (!id) fail('无活跃 run——先 start "<需求原文>"');
|
|
880
|
+
const run = loadRun(ctx, id);
|
|
881
|
+
if (!run) fail(`run 文件不存在或损坏:${id}(status --adopt 可按文档现状重建)`);
|
|
882
|
+
return run;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('pipeline-run.mjs');
|
|
886
|
+
if (isMain) {
|
|
887
|
+
const a = parseArgs(process.argv.slice(2));
|
|
888
|
+
const ctx = ctxEnv();
|
|
889
|
+
const cmd = a.cmd;
|
|
890
|
+
|
|
891
|
+
if (cmd === 'start') {
|
|
892
|
+
const req = a.positional[0];
|
|
893
|
+
if (!req) fail('start 必填需求原文:start "<需求>" [--hint "kind=… level=… module=…"]');
|
|
894
|
+
const runId = newRunId(req);
|
|
895
|
+
const run = {
|
|
896
|
+
runId, requirement: req, hint: a.hint || '', createdAt: nowIso(), updatedAt: nowIso(),
|
|
897
|
+
triage: null, stage: 'triage', stopType: 'work-order', workOrder: null, awaitConfirm: null,
|
|
898
|
+
fixLoop: { count: 0, cap: FIX_CAP }, docs: { entry: null, spec: null, plan: null },
|
|
899
|
+
lastKbHits: [], lastFiles: [], events: [],
|
|
900
|
+
};
|
|
901
|
+
emit(run, { type: 'run-created', requirement: req });
|
|
902
|
+
saveRun(ctx, run);
|
|
903
|
+
advance(ctx, run, {}); // 首停机点 = triage 工单
|
|
904
|
+
} else if (cmd === 'next') {
|
|
905
|
+
const run = resolveRun(ctx, a);
|
|
906
|
+
if (run.stopType === 'done' || run.stopType === 'aborted') { printRun(ctx, run); process.exit(0); }
|
|
907
|
+
advance(ctx, run, { triage: typeof a.triage === 'string' ? a.triage : undefined, delegated: typeof a.delegated === 'string' ? a.delegated : undefined, files: typeof a.files === 'string' ? a.files : undefined, done: a.done === true });
|
|
908
|
+
} else if (cmd === 'status') {
|
|
909
|
+
if (a.adopt) { adoptRun(ctx, a); process.exit(0); }
|
|
910
|
+
const run = resolveRun(ctx, a);
|
|
911
|
+
if (a.json) { console.log(JSON.stringify(run, null, 2)); process.exit(0); }
|
|
912
|
+
printRun(ctx, run);
|
|
913
|
+
console.log(`\n事件流水(${run.events.length}):`);
|
|
914
|
+
for (const e of run.events) {
|
|
915
|
+
const bits = [e.t.slice(11, 19), e.type];
|
|
916
|
+
if (e.cmd) bits.push(`${e.cmd.slice(0, 70)}${e.exit !== 0 ? ` [exit ${e.exit}]` : ''}`);
|
|
917
|
+
if (e.doc) bits.push(e.doc);
|
|
918
|
+
if (e.quote) bits.push(`原话「${e.quote.slice(0, 30)}」`);
|
|
919
|
+
if (e.subject) bits.push(e.subject.slice(0, 50));
|
|
920
|
+
if (e.id) bits.push(`#${e.id}`);
|
|
921
|
+
console.log(' ' + bits.join(' | '));
|
|
922
|
+
}
|
|
923
|
+
process.exit(0);
|
|
924
|
+
} else if (cmd === 'watch') {
|
|
925
|
+
const interval = Math.max(1, Number(a.interval) || 2);
|
|
926
|
+
const render = () => {
|
|
927
|
+
const id = a.run || latestRunId(ctx);
|
|
928
|
+
console.log('\x1B[2J\x1B[H');
|
|
929
|
+
if (!id) { console.log('无 run'); return; }
|
|
930
|
+
const run = loadRun(ctx, id);
|
|
931
|
+
if (!run) { console.log(`run ${id} 不存在`); return; }
|
|
932
|
+
printRun(ctx, run);
|
|
933
|
+
const recent = run.events.slice(-5);
|
|
934
|
+
console.log('最近事件:');
|
|
935
|
+
for (const e of recent) console.log(` ${e.t.slice(11, 19)} ${e.type} ${e.cmd ? e.cmd.slice(0, 50) : ''} ${e.exit !== undefined && e.type === 'gate' ? `exit=${e.exit}` : ''}`);
|
|
936
|
+
console.log(`(每 ${interval}s 刷新,Ctrl+C 退出)`);
|
|
937
|
+
};
|
|
938
|
+
render();
|
|
939
|
+
setInterval(render, interval * 1000);
|
|
940
|
+
} else if (cmd === 'abort') {
|
|
941
|
+
if (!a.run) fail('abort 必填 --run <id>');
|
|
942
|
+
const run = loadRun(ctx, a.run);
|
|
943
|
+
if (!run) fail(`run 不存在:${a.run}`);
|
|
944
|
+
const to = a.to === true ? 'cancelled' : (a.to || 'cancelled');
|
|
945
|
+
if (!['superseded', 'cancelled'].includes(to)) fail('--to 须为 superseded|cancelled');
|
|
946
|
+
const manual = [];
|
|
947
|
+
const flipped = [];
|
|
948
|
+
for (const rel of [run.docs?.entry, run.docs?.spec, run.docs?.plan].filter(Boolean)) {
|
|
949
|
+
const st = docState(ctx, rel);
|
|
950
|
+
if (['approved'].includes(st)) {
|
|
951
|
+
const r = confirmGate(ctx, run, rel, typeof a.delegated === 'string' ? a.delegated : '', to);
|
|
952
|
+
if (r.ok) flipped.push(rel); else manual.push(rel);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (flipped.length) {
|
|
956
|
+
runNode(ctx, run, 'gen-workflow-index.mjs', []); // 复核 P2-3:提交前重生成,防陈旧索引入库
|
|
957
|
+
const c = docsCommit(ctx, run, [...flipped, 'workflow/INDEX.md'], `docs(workflow): ${run.triage?.topic || 'task'} 放弃——${to}`);
|
|
958
|
+
if (!c.ok) console.log('⚠️ 放弃态 docs 提交失败(可手工补交)');
|
|
959
|
+
}
|
|
960
|
+
run.stopType = 'aborted';
|
|
961
|
+
run.abort = { to, note: manual.length ? `以下文档须用户终端亲跑 confirm-doc --to ${to}:${manual.join('、')}` : '' };
|
|
962
|
+
emit(run, { type: 'abort', to });
|
|
963
|
+
saveRun(ctx, run);
|
|
964
|
+
printRun(ctx, run);
|
|
965
|
+
process.exit(0);
|
|
966
|
+
} else {
|
|
967
|
+
fail(`未知命令:${cmd ?? ''}(start | next | status | watch | abort,用法见文件头注释)`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// ── adopt:run 文件丢失/损坏时按文档现状重建(尽力而为,非权威仲裁) ──
|
|
972
|
+
function adoptRun(ctx, a) {
|
|
973
|
+
const id = a.run || latestRunId(ctx);
|
|
974
|
+
if (!id) fail('无可 adopt 的 run id');
|
|
975
|
+
const topic = (id.split('-').slice(2, -1).join('-')) || 'task';
|
|
976
|
+
const find = (dir) => {
|
|
977
|
+
const d = path.join(ctx.root, 'workflow', dir);
|
|
978
|
+
if (!fs.existsSync(d)) return null;
|
|
979
|
+
const hit = fs.readdirSync(d).map((f) => f.replace(/\.md$/, '')).filter((b) => b.includes(topic) && !b.startsWith('_'))
|
|
980
|
+
.sort().pop();
|
|
981
|
+
return hit ? `workflow/${dir}/${hit}.md` : null;
|
|
982
|
+
};
|
|
983
|
+
const entry = find('intents') || find('incidents');
|
|
984
|
+
const spec = find('specs');
|
|
985
|
+
const plan = find('plans');
|
|
986
|
+
if (!entry && !plan) fail(`按 topic「${topic}」未找到任何 workflow 文档——无法重建`);
|
|
987
|
+
const run = {
|
|
988
|
+
runId: id, requirement: `(adopted ${nowIso()}——原需求原文已不可考,见 git 历史)`, hint: '', createdAt: nowIso(), updatedAt: nowIso(),
|
|
989
|
+
triage: { kind: entry && entry.includes('incidents') ? 'fix' : 'require', level: 'L1', module: 'pipeline', topic, decidedBy: 'adopt', decidedAt: nowIso() },
|
|
990
|
+
stage: 'adopted', stopType: 'work-order', workOrder: null, awaitConfirm: null, fixLoop: { count: 0, cap: FIX_CAP },
|
|
991
|
+
docs: { entry, spec, plan }, lastKbHits: [], lastFiles: [], events: [{ t: nowIso(), type: 'adopt', entry, spec, plan }],
|
|
992
|
+
};
|
|
993
|
+
saveRun(ctx, run);
|
|
994
|
+
console.log(`✅ 已重建 run ${id}(stage=adopted)——文档现状:`);
|
|
995
|
+
for (const [k, rel] of Object.entries(run.docs)) if (rel) console.log(` ${k}: ${rel}(${docState(ctx, rel) || '缺失'})`);
|
|
996
|
+
console.log(' 提示:adopt 不推断断点位置——按文档现状人工续走(next 会按 stage 拒绝,请对照事件缺失情况手动接续或另起 run)');
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export default { parseKV, validateDraftContent, closeoutComplete, placeholdersIn };
|