@zhushanwen/pi-subagent-workflow 8.0.0 → 8.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -8
- package/scripts/rfl.mjs +308 -0
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/channel-registry-handshake.test.ts +1 -1
- package/src/execution/__tests__/dialog-queue.test.ts +1 -1
- package/src/execution/__tests__/finalize-record.test.ts +33 -7
- package/src/execution/__tests__/helpers/spawn-mock.ts +12 -3
- package/src/execution/__tests__/host-mode.test.ts +1 -1
- package/src/execution/__tests__/record-store.test.ts +284 -2
- package/src/execution/__tests__/spawn-args.test.ts +5 -5
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +1 -1
- package/src/execution/__tests__/subagent-service-message-close.test.ts +31 -0
- package/src/execution/__tests__/ui-channels.test.ts +1 -1
- package/src/execution/__tests__/ui-interaction-model.test.ts +1 -1
- package/src/execution/__tests__/ui-request-observability.test.ts +1 -1
- package/src/execution/__tests__/worktree-manager.test.ts +9 -2
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +4 -1
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +72 -34
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/argv-mirror.ts +1 -1
- package/src/execution/channel-registry-access.ts +3 -1
- package/src/execution/finalize-record.ts +16 -7
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +9 -2
- package/src/execution/record-entry.ts +118 -0
- package/src/execution/record-store.ts +290 -1
- package/src/execution/session-pending.ts +5 -4
- package/src/execution/session-runner.ts +485 -290
- package/src/execution/subagent-service.ts +87 -37
- package/src/execution/temp-prompt.ts +8 -3
- package/src/execution/types.ts +20 -2
- package/src/execution/worktree-manager.ts +325 -14
- package/src/execution/worktree-registry.ts +90 -33
- package/src/index.ts +42 -2
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
- package/src/interface/format.ts +10 -4
- package/src/interface/tool-render.ts +10 -3
- package/src/interface/tool-workflow-script.ts +29 -33
- package/src/interface/tool-workflow.ts +47 -65
- package/src/interface/views/detail-content.ts +1 -1
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +9 -1
- package/src/orchestration/__tests__/execute-agent-call.test.ts +49 -1
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +181 -19
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
- package/src/orchestration/__tests__/test-mocks.ts +9 -3
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +12 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +39 -41
- package/src/orchestration/error-recovery.ts +4 -1
- package/src/orchestration/execute-agent-call.ts +13 -3
- package/src/orchestration/jsonl-run-store.ts +139 -60
- package/src/orchestration/lifecycle.ts +10 -1
- package/src/orchestration/worker-script-builder.ts +9 -1
- package/src/shared/__tests__/resource-discovery.test.ts +24 -0
- package/src/shared/agent-ref.ts +6 -1
- package/src/shared/resource-discovery.ts +15 -1
- package/src/shared/schema-jsonify.ts +4 -1
- package/workflows/review-fix-loop-utils.cjs +543 -33
- package/workflows/review-fix-loop.js +462 -109
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"agents/",
|
|
23
23
|
"skills/",
|
|
24
24
|
"workflows/",
|
|
25
|
+
"scripts/",
|
|
25
26
|
"src/index.ts",
|
|
26
27
|
"src/execution/",
|
|
27
28
|
"src/injectors/",
|
|
@@ -29,6 +30,9 @@
|
|
|
29
30
|
"src/interface/",
|
|
30
31
|
"src/shared/"
|
|
31
32
|
],
|
|
33
|
+
"xyz-agent": {
|
|
34
|
+
"role": "universal"
|
|
35
|
+
},
|
|
32
36
|
"pi": {
|
|
33
37
|
"extensions": [
|
|
34
38
|
"./index.ts"
|
|
@@ -41,18 +45,19 @@
|
|
|
41
45
|
]
|
|
42
46
|
},
|
|
43
47
|
"dependencies": {
|
|
44
|
-
"@xyz-agent/extension-protocol": "^0.4.0",
|
|
45
48
|
"ajv": "^8.20.0",
|
|
46
49
|
"yaml": "^2.9.0",
|
|
47
|
-
"@
|
|
50
|
+
"@xyz-agent/extension-protocol": "0.5.1",
|
|
51
|
+
"@zhushanwen/pi-extension-logger": "0.2.2",
|
|
52
|
+
"@zhushanwen/pi-file-lock": "0.1.1"
|
|
48
53
|
},
|
|
49
54
|
"peerDependencies": {
|
|
50
|
-
"@earendil-works/pi-ai": "
|
|
51
|
-
"@earendil-works/pi-coding-agent": "
|
|
52
|
-
"@earendil-works/pi-tui": "
|
|
55
|
+
"@earendil-works/pi-ai": "^0.84.1",
|
|
56
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
57
|
+
"@earendil-works/pi-tui": "^0.84.1",
|
|
53
58
|
"typebox": "*",
|
|
54
|
-
"@zhushanwen/pi-pending-notifications": "0.3.
|
|
55
|
-
"@zhushanwen/pi-structured-output": "5.0.
|
|
59
|
+
"@zhushanwen/pi-pending-notifications": "0.3.4",
|
|
60
|
+
"@zhushanwen/pi-structured-output": "5.0.2"
|
|
56
61
|
},
|
|
57
62
|
"peerDependenciesMeta": {
|
|
58
63
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -64,6 +69,7 @@
|
|
|
64
69
|
},
|
|
65
70
|
"devDependencies": {
|
|
66
71
|
"@types/node": "^24.0.0",
|
|
72
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
67
73
|
"vitest": "^4.1.8"
|
|
68
74
|
},
|
|
69
75
|
"scripts": {
|
package/scripts/rfl.mjs
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// rfl.mjs — review-fix-loop run 数据查询 CLI(tier-1 §7.4,零依赖 node 脚本)
|
|
3
|
+
//
|
|
4
|
+
// 读取 ~/.review-fix-loop/<repo-slug>/<runId>/state.json(workflow 脚本落盘),
|
|
5
|
+
// 派生指标现算不落盘。M0 数据结构(calls/batches[].rounds[].phaseTimings)为全集,
|
|
6
|
+
// M1/M2 字段(issues[].origin/dormant/scores)缺省容错显示。
|
|
7
|
+
//
|
|
8
|
+
// 用法:
|
|
9
|
+
// node rfl.mjs list [repoSlug] run 清单(runId/时间/终止原因/轮数)
|
|
10
|
+
// node rfl.mjs stats <runId|latest> 单 run 全景(token/缓存/per-role/轮次时间线)
|
|
11
|
+
// node rfl.mjs trends [repoSlug] 跨 run 趋势表
|
|
12
|
+
// node rfl.mjs clean --older-than 30d [--yes] 清理(默认干跑)
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
const ROOT = join(homedir(), ".review-fix-loop");
|
|
18
|
+
|
|
19
|
+
function isDir(p) {
|
|
20
|
+
try { return statSync(p).isDirectory(); } catch { return false; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function listRuns(repoSlug) {
|
|
24
|
+
if (!isDir(ROOT)) return [];
|
|
25
|
+
const repos = repoSlug ? [repoSlug] : readdirSync(ROOT).filter((d) => isDir(join(ROOT, d)));
|
|
26
|
+
const runs = [];
|
|
27
|
+
for (const repo of repos) {
|
|
28
|
+
const repoDir = join(ROOT, repo);
|
|
29
|
+
if (!isDir(repoDir)) continue;
|
|
30
|
+
for (const runId of readdirSync(repoDir)) {
|
|
31
|
+
const stateFile = join(repoDir, runId, "state.json");
|
|
32
|
+
if (!existsSync(stateFile)) continue;
|
|
33
|
+
try {
|
|
34
|
+
runs.push({ repo, runId, state: JSON.parse(readFileSync(stateFile, "utf8")), stateFile });
|
|
35
|
+
} catch { /* 损坏 state 跳过(半写入 run) */ }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return runs.sort((a, b) => String(a.state?.meta?.startedAt ?? "").localeCompare(String(b.state?.meta?.startedAt ?? "")));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fmtTokens(n) {
|
|
42
|
+
if (n == null) return "-";
|
|
43
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
|
|
44
|
+
if (n >= 1_000) return Math.round(n / 1_000) + "k";
|
|
45
|
+
return String(n);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fmtMs(ms) {
|
|
49
|
+
if (ms == null) return "-";
|
|
50
|
+
if (ms >= 60_000) return Math.floor(ms / 60_000) + "m" + Math.round((ms % 60_000) / 1000) + "s";
|
|
51
|
+
if (ms >= 1_000) return Math.round(ms / 1000) + "s";
|
|
52
|
+
return Math.round(ms) + "ms";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fmtCost(c) {
|
|
56
|
+
if (c == null) return "-";
|
|
57
|
+
return "$" + (c >= 0.01 ? c.toFixed(2) : c.toFixed(4));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** usage 容错取数:usage 缺失或字段缺省一律按 0 计。 */
|
|
61
|
+
function usageOrZero(u, k) {
|
|
62
|
+
return (u && u[k]) || 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 累加单个 call 的 token 四分量与 cost(无 usage 的 call 跳过)。 */
|
|
66
|
+
function addUsageSums(sum, u) {
|
|
67
|
+
if (!u) return;
|
|
68
|
+
sum.input += u.input || 0;
|
|
69
|
+
sum.output += u.output || 0;
|
|
70
|
+
sum.cacheRead += u.cacheRead || 0;
|
|
71
|
+
sum.cacheWrite += u.cacheWrite || 0;
|
|
72
|
+
sum.cost += u.cost || 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 聚合 calls[]:token 四分量/cost/命中率/per-role。 */
|
|
76
|
+
function summarizeCalls(calls) {
|
|
77
|
+
const sum = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
78
|
+
const byRole = {};
|
|
79
|
+
for (const c of calls || []) {
|
|
80
|
+
const u = c.usage;
|
|
81
|
+
addUsageSums(sum, u);
|
|
82
|
+
const key = c.role || "unknown";
|
|
83
|
+
byRole[key] = byRole[key] || { count: 0, input: 0, cacheRead: 0 };
|
|
84
|
+
byRole[key].count++;
|
|
85
|
+
byRole[key].input += usageOrZero(u, "input");
|
|
86
|
+
byRole[key].cacheRead += usageOrZero(u, "cacheRead");
|
|
87
|
+
}
|
|
88
|
+
const denom = sum.input + sum.cacheRead;
|
|
89
|
+
const cachePct = denom > 0 ? Math.round((sum.cacheRead / denom) * 100) : null;
|
|
90
|
+
return { sum, byRole, cachePct };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function roundCount(state) {
|
|
94
|
+
return (state.batches || []).reduce((a, b) => a + (b.rounds || []).length, 0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** phaseTimings 值合法性:[startMs, endMs] 数字对(半写入 run 容错)。 */
|
|
98
|
+
function isTimingPair(pair) {
|
|
99
|
+
return Array.isArray(pair) && typeof pair[0] === "number" && typeof pair[1] === "number";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 收集 run 内全部合法 phase 时序对 [startMs, endMs](非法值=半写入 run 容错跳过)。 */
|
|
103
|
+
function validTimingPairs(state) {
|
|
104
|
+
const pairs = [];
|
|
105
|
+
for (const b of state.batches || []) {
|
|
106
|
+
for (const r of b.rounds || []) {
|
|
107
|
+
for (const pair of Object.values(r.phaseTimings || {})) {
|
|
108
|
+
if (isTimingPair(pair)) pairs.push(pair);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return pairs;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** run 墙钟时间 = 全部合法时序对的 min(start) → max(end);无任何合法对返回 null。 */
|
|
116
|
+
function wallMs(state) {
|
|
117
|
+
const pairs = validTimingPairs(state);
|
|
118
|
+
if (pairs.length === 0) return null;
|
|
119
|
+
return Math.max(...pairs.map((p) => p[1])) - Math.min(...pairs.map((p) => p[0]));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function cmdList(repoSlug) {
|
|
123
|
+
const runs = listRuns(repoSlug);
|
|
124
|
+
if (runs.length === 0) {
|
|
125
|
+
console.log("no runs found" + (repoSlug ? " for repo " + repoSlug : "") + " (root: " + ROOT + ")");
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
for (const r of runs) {
|
|
129
|
+
const started = (r.state.meta && r.state.meta.startedAt) || "?";
|
|
130
|
+
const term = (r.state.meta && r.state.meta.terminated) || "?";
|
|
131
|
+
console.log(
|
|
132
|
+
r.runId.padEnd(28) + " " + String(started).padEnd(25) +
|
|
133
|
+
" " + String(term).padEnd(18) +
|
|
134
|
+
" rounds: " + roundCount(r.state) +
|
|
135
|
+
(r.state.calls ? " calls: " + r.state.calls.length : ""),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** stats 概览段:terminated/轮数 + token 四分量 + per-role 明细。 */
|
|
142
|
+
function printStatsSummary(s, { sum, cachePct, byRole }) {
|
|
143
|
+
console.log(" terminated: " + ((s.meta && s.meta.terminated) || "?") + " rounds: " + roundCount(s) + " started: " + (s.meta && s.meta.startedAt));
|
|
144
|
+
console.log(
|
|
145
|
+
" tokens: input " + fmtTokens(sum.input) +
|
|
146
|
+
" (cacheRead " + (cachePct == null ? "-" : cachePct + "%") + ")" +
|
|
147
|
+
" output " + fmtTokens(sum.output) +
|
|
148
|
+
" cost " + fmtCost(sum.cost) +
|
|
149
|
+
" wall " + fmtMs(wallMs(s)),
|
|
150
|
+
);
|
|
151
|
+
const roleParts = Object.entries(byRole).map(([role, v]) =>
|
|
152
|
+
role + " ×" + v.count + " " + fmtTokens(v.input + v.cacheRead));
|
|
153
|
+
console.log(" per-role: " + (roleParts.join(" │ ") || "(no calls)"));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** stats issues 概览段(M1 origin/dormant 字段缺省容错)。 */
|
|
157
|
+
function printStatsIssues(s) {
|
|
158
|
+
const issues = s.issues ? Object.values(s.issues) : [];
|
|
159
|
+
const fixed = issues.filter((i) => i.status === "fixed").length;
|
|
160
|
+
const regressed = issues.filter((i) => (i.history || []).some((h) => h.status === "regressed")).length;
|
|
161
|
+
const origins = {};
|
|
162
|
+
for (const i of issues) origins[i.origin || "-"] = (origins[i.origin || "-"] || 0) + 1;
|
|
163
|
+
// issues 与 dormant 都是批作用域(state.issues 每批重置,最终 state 只含末批
|
|
164
|
+
// 条目,非 run 累计)——表述显式限定作用域,防止被误读为整个 run 的总数
|
|
165
|
+
console.log(" issues (last batch): total " + issues.length + " → fixed " + fixed +
|
|
166
|
+
" regressed-ever " + regressed +
|
|
167
|
+
" origins " + (issues.length ? Object.entries(origins).map(([k, v]) => k + " " + v).join("/") : "-") +
|
|
168
|
+
" dormant (last batch) " + ((s.dormant || []).length));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** stats scores 表段(M2 字段缺省容错)。 */
|
|
172
|
+
function printStatsScores(s) {
|
|
173
|
+
if (Array.isArray(s.scores) && s.scores.length > 0) {
|
|
174
|
+
for (const sc of s.scores) {
|
|
175
|
+
// regression=null = 该维度 unverifiable(workflow 无对账数据可回填),无分值
|
|
176
|
+
// 语义——显示 n/a 而非 JS 字面量 "null"(与 total 缺省 "(no total)" 惯例一致)
|
|
177
|
+
const dims = sc.dimensions ? Object.entries(sc.dimensions).map(([k, v]) => k + " " + (v == null ? "n/a" : v)).join(" ") : "";
|
|
178
|
+
// scores entry 的 round 是批局部编号(每批从 1 重新计数),多批 run 下批 1 与
|
|
179
|
+
// 批 2 的 "R1" 两行完全同形无法归批——行首补 B<batch> 标识(与下方时间线
|
|
180
|
+
// R<n> (batch N) 的批标注口径呼应);旧数据无 batch 字段回退 B1
|
|
181
|
+
console.log(" score B" + (sc.batch ?? 1) + " R" + (sc.round ?? "?") + " " + (sc.targetKind || "?") + "/" + (sc.targetName || "?") +
|
|
182
|
+
": " + (sc.total != null ? sc.total + "/10" : "(no total)") + " [" + dims + "]");
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
console.log(" scores: (none)");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** stats 轮次时间线段。 */
|
|
190
|
+
function printStatsTimeline(s) {
|
|
191
|
+
for (const b of s.batches || []) {
|
|
192
|
+
for (const r of b.rounds || []) {
|
|
193
|
+
const pt = r.phaseTimings || {};
|
|
194
|
+
const seg = (k) => (Array.isArray(pt[k]) ? k + " " + fmtMs(pt[k][1] - pt[k][0]) : k + " -");
|
|
195
|
+
console.log(" R" + r.round + " (batch " + b.index + "): " + seg("review") + " │ " + seg("aggregate") + " │ " + seg("fix") +
|
|
196
|
+
" mustFix " + (r.mustFix ?? "-"));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cmdStats(runIdOrLatest, repoSlug) {
|
|
202
|
+
const runs = listRuns(repoSlug);
|
|
203
|
+
if (runs.length === 0) { console.log("no runs found (root: " + ROOT + ")"); return 1; }
|
|
204
|
+
const run = runIdOrLatest === "latest" ? runs[runs.length - 1] : runs.find((r) => r.runId === runIdOrLatest);
|
|
205
|
+
if (!run) { console.log("run not found: " + runIdOrLatest); return 1; }
|
|
206
|
+
const s = run.state;
|
|
207
|
+
const { sum, byRole, cachePct } = summarizeCalls(s.calls);
|
|
208
|
+
console.log("run " + run.runId + " (repo: " + run.repo + ")");
|
|
209
|
+
printStatsSummary(s, { sum, cachePct, byRole });
|
|
210
|
+
printStatsIssues(s);
|
|
211
|
+
printStatsScores(s);
|
|
212
|
+
printStatsTimeline(s);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function cmdTrends(repoSlug) {
|
|
217
|
+
const runs = listRuns(repoSlug);
|
|
218
|
+
if (runs.length === 0) { console.log("no runs found (root: " + ROOT + ")"); return 0; }
|
|
219
|
+
// started 列宽 = ISO 8601 恒长 24(padEnd(20) 会让表头比数据行左移 4 字符错位)
|
|
220
|
+
// reg/att 列名刻意缩短:该列是末批作用域(见循环后 legend),长名 "regressed/
|
|
221
|
+
// attempted" 会被读成与 rounds/tokens/cache% 同级的 run 级指标
|
|
222
|
+
const header = "runId".padEnd(28) + " " + "started".padEnd(24) + " " + "rounds".padEnd(6) + " " + "tokens".padEnd(8) + " " + "cache%".padEnd(6) + " " + "reg/att";
|
|
223
|
+
console.log(header);
|
|
224
|
+
for (const r of runs) {
|
|
225
|
+
const { sum, cachePct } = summarizeCalls(r.state.calls);
|
|
226
|
+
const issues = r.state.issues ? Object.values(r.state.issues) : [];
|
|
227
|
+
const regressed = issues.filter((i) => (i.history || []).some((h) => h.status === "regressed")).length;
|
|
228
|
+
// 分母 = 曾进入 fix-attempted 的 issue 数,与分子(含 regressed history 的 issue
|
|
229
|
+
// 数)同为 issue 计数口径(§6.7「fix 质量 = 1 − regressed/fix-attempted」的分母
|
|
230
|
+
// 应是被尝试修复过的 issue)。不能用 Σ fixAttempts:该字段权威语义是「修复失败
|
|
231
|
+
// 次数」(初始 0,每次转 regressed 才 +1),5 个 issue 修 4 成 1 败会算成 1/1
|
|
232
|
+
// (字面 100% 回归率),正确口径是 1/5;进入过修复的痕迹只有 history 的
|
|
233
|
+
// fix-attempted 条目。分母 0(从未尝试修复)率无意义,显示 "-"。
|
|
234
|
+
const attempted = issues.filter((i) => (i.history || []).some((h) => h.status === "fix-attempted")).length;
|
|
235
|
+
const regressedCol = attempted > 0 ? regressed + "/" + attempted : "-";
|
|
236
|
+
console.log(
|
|
237
|
+
r.runId.padEnd(28) + " " + String((r.state.meta && r.state.meta.startedAt) || "?").padEnd(24) + " " +
|
|
238
|
+
String(roundCount(r.state)).padEnd(6) + " " +
|
|
239
|
+
fmtTokens(sum.input + sum.cacheRead + sum.output).padEnd(8) + " " +
|
|
240
|
+
String(cachePct == null ? "-" : cachePct + "%").padEnd(6) + " " + regressedCol,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
// reg/att 与同行其他列口径不同:state.issues 每批重置,最终 state 只含末批条目
|
|
244
|
+
// (分子分母内部一致),而 rounds/tokens/cache% 是 run 级累计——跨 run 对比时
|
|
245
|
+
// 易把末批数据误读为 run 级,表后用 legend 显式标注作用域与分母口径(函数开头
|
|
246
|
+
// 已保证 runs 非空,走到这里表必然非空)
|
|
247
|
+
console.log("reg/att: last-batch scope (issues reset per batch); denominator = issues ever fix-attempted");
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseOlderThan(spec) {
|
|
252
|
+
const m = /^(\d+)([dh])$/.exec(String(spec || ""));
|
|
253
|
+
if (!m) return null;
|
|
254
|
+
return Date.now() - parseInt(m[1], 10) * (m[2] === "d" ? 86_400_000 : 3_600_000);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function cmdClean(spec, yes) {
|
|
258
|
+
const cutoff = parseOlderThan(spec);
|
|
259
|
+
if (cutoff == null) {
|
|
260
|
+
console.log("invalid --older-than (expect <N>d or <N>h, e.g. 30d)");
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
const runs = listRuns();
|
|
264
|
+
const expired = runs.filter((r) => {
|
|
265
|
+
const t = r.stateFile ? statSync(r.stateFile).mtimeMs : 0;
|
|
266
|
+
const started = r.state.meta && r.state.meta.startedAt ? Date.parse(r.state.meta.startedAt) : NaN;
|
|
267
|
+
const basis = Number.isFinite(started) ? started : t;
|
|
268
|
+
return basis < cutoff;
|
|
269
|
+
});
|
|
270
|
+
if (expired.length === 0) { console.log("nothing older than " + spec); return 0; }
|
|
271
|
+
for (const r of expired) {
|
|
272
|
+
const dir = join(ROOT, r.repo, r.runId);
|
|
273
|
+
if (yes) {
|
|
274
|
+
rmSync(dir, { recursive: true, force: true });
|
|
275
|
+
console.log("deleted " + dir);
|
|
276
|
+
} else {
|
|
277
|
+
console.log("would delete " + dir + " (dry-run; add --yes to execute)");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── main ──────────────────────────────────────────────────────
|
|
284
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
285
|
+
try {
|
|
286
|
+
switch (cmd) {
|
|
287
|
+
case "list": process.exit(cmdList(rest[0])); break;
|
|
288
|
+
case "stats": {
|
|
289
|
+
if (!rest[0]) { console.log("usage: rfl stats <runId|latest> [repoSlug]"); process.exit(1); }
|
|
290
|
+
process.exit(cmdStats(rest[0], rest[1]));
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "trends": process.exit(cmdTrends(rest[0])); break;
|
|
294
|
+
case "clean": {
|
|
295
|
+
const oi = rest.indexOf("--older-than");
|
|
296
|
+
const spec = oi >= 0 ? rest[oi + 1] : null;
|
|
297
|
+
if (!spec) { console.log("usage: rfl clean --older-than <N>d [--yes]"); process.exit(1); }
|
|
298
|
+
process.exit(cmdClean(spec, rest.includes("--yes")));
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
default:
|
|
302
|
+
console.log("usage: rfl <list|stats|trends|clean> ...\n list [repoSlug]\n stats <runId|latest> [repoSlug]\n trends [repoSlug]\n clean --older-than 30d [--yes]");
|
|
303
|
+
process.exit(cmd ? 1 : 0);
|
|
304
|
+
}
|
|
305
|
+
} catch (err) {
|
|
306
|
+
console.error("rfl: " + (err && err.message ? err.message : String(err)));
|
|
307
|
+
process.exit(2);
|
|
308
|
+
}
|
|
@@ -201,7 +201,7 @@ const results = await parallel(
|
|
|
201
201
|
);
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
> 内置通用编排 workflow(chain / parallel / scatter-gather / map-reduce,可直接 `workflow run`,用 `agent()` 自包含实现)见 `extensions/subagent-workflow/workflows/`。本段教 `workflow()` 嵌套 API,workflows 目录是开箱即用的通用编排工具(用 `agent()` 而非 `workflow()` 嵌套)。
|
|
204
|
+
> 内置通用编排 workflow(chain / parallel / scatter-gather / map-reduce,可直接 `workflow run`,用 `agent()` 自包含实现)见 `extensions/universal/subagent-workflow/workflows/`。本段教 `workflow()` 嵌套 API,workflows 目录是开箱即用的通用编排工具(用 `agent()` 而非 `workflow()` 嵌套)。
|
|
205
205
|
|
|
206
206
|
### Other globals
|
|
207
207
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 决策 D(修复 M4):channel handshake 协议升级测试。
|
|
4
4
|
//
|
|
5
|
-
// 测试对象:extensions/subagent-workflow/src/execution/channel-registry-access.ts
|
|
5
|
+
// 测试对象:extensions/universal/subagent-workflow/src/execution/channel-registry-access.ts
|
|
6
6
|
//
|
|
7
7
|
// 协议契约(与 ask-user 侧严格对齐):
|
|
8
8
|
// key 字面量: "@zhushanwen/pi-subagents.channelHandshake"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// W2 红灯测试:dialog-queue.ts — L2 跨子进程全局 dialog 串行队列。
|
|
4
4
|
//
|
|
5
|
-
// 测试对象:extensions/subagent-workflow/src/execution/dialog-queue.ts(新建)
|
|
5
|
+
// 测试对象:extensions/universal/subagent-workflow/src/execution/dialog-queue.ts(新建)
|
|
6
6
|
// 契约来源:.fix-plans/00-master-summary.md §一 冲突 3(L2 DialogGlobalQueue 设计)
|
|
7
7
|
//
|
|
8
8
|
// DialogGlobalQueue 设计要点:
|
|
@@ -86,7 +86,7 @@ describe("doFinalizeRecord — manifest status 透传 (M3 4 态)", () => {
|
|
|
86
86
|
return {
|
|
87
87
|
manifestStore,
|
|
88
88
|
worktreeManager: {} as never,
|
|
89
|
-
store: { archive: vi.fn() } as never,
|
|
89
|
+
store: { archive: vi.fn(), reportRecordTransition: vi.fn() } as never,
|
|
90
90
|
modelService: {} as never,
|
|
91
91
|
pi: { appendEntry: vi.fn() },
|
|
92
92
|
emitUnregister: vi.fn(),
|
|
@@ -206,7 +206,7 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
|
|
|
206
206
|
return {
|
|
207
207
|
manifestStore,
|
|
208
208
|
worktreeManager: { cleanup: vi.fn(), collectPatch: vi.fn() } as never,
|
|
209
|
-
store: { archive: vi.fn() } as never,
|
|
209
|
+
store: { archive: vi.fn(), reportRecordTransition: vi.fn() } as never,
|
|
210
210
|
modelService: {} as never,
|
|
211
211
|
pi: { appendEntry: vi.fn() } as never,
|
|
212
212
|
emitUnregister: vi.fn(),
|
|
@@ -234,6 +234,17 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
|
|
|
234
234
|
expect(fs.existsSync(`${sessionFile}.alive`)).toBe(false);
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
it("W16: 轮终上报 reportRecordTransition(record-store 类外恢复写点迁移落 entry)", async () => {
|
|
238
|
+
const deps = makeDeps();
|
|
239
|
+
const record = makeMinimalRecord({ id: "rec-report", chatMode: true, round: 2 });
|
|
240
|
+
record.status = "closed";
|
|
241
|
+
await doFinalizeRoundToIdle(deps, record, makeMinimalResult());
|
|
242
|
+
expect(deps.store.reportRecordTransition).toHaveBeenCalledTimes(1);
|
|
243
|
+
// 上报发生在 round 推进之后(entry 携带新轮计数,重建源不滞后)
|
|
244
|
+
expect(record.round).toBe(3);
|
|
245
|
+
expect(record.status).toBe("running");
|
|
246
|
+
});
|
|
247
|
+
|
|
237
248
|
it("record.round 已为 N → round 变 N+1", async () => {
|
|
238
249
|
const record = makeMinimalRecord({ id: "rec-round", round: 3 });
|
|
239
250
|
record.status = "closed";
|
|
@@ -322,7 +333,7 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
|
|
|
322
333
|
expect(record.result).not.toContain("PREV-ROUND-TEXT");
|
|
323
334
|
});
|
|
324
335
|
|
|
325
|
-
it("C1TC11:
|
|
336
|
+
it("C1TC11 [R2-1]: one-shot 首轮空文本成功完成 → record.result 补占位「(empty)」(非 undefined)", async () => {
|
|
326
337
|
const record = makeMinimalRecord({ id: "rec-oneshot-empty", chatMode: false });
|
|
327
338
|
record.status = "closed";
|
|
328
339
|
// one-shot 成功空文本完成路径(collectResult getFullText 返回 ""、success=true):
|
|
@@ -333,10 +344,25 @@ describe("doFinalizeRoundToIdle — chatMode 轮次完成进 idle (M2-A)", () =>
|
|
|
333
344
|
result.success = true;
|
|
334
345
|
result.error = undefined;
|
|
335
346
|
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//
|
|
347
|
+
// [R2-1] 轮终信号优先:占位非 undefined 是 renderer hasRunning 排除轮终 running 的
|
|
348
|
+
// 判据——保持 undefined 会让完成注入后末位 turn 永久「工作中」
|
|
349
|
+
expect(record.result).toBe("(empty)");
|
|
350
|
+
// notifier buildLlmContent 的 record.result ?? "(empty)" 兜底同款措辞:
|
|
351
|
+
// 通知文案逐字节产出 "completed. Result:\n(empty)"(G4 保持)
|
|
340
352
|
expect(record.result ?? "(empty)").toBe("(empty)");
|
|
341
353
|
});
|
|
354
|
+
|
|
355
|
+
it("C1TC11b [R2-1]: one-shot 续轮空文本 → 沿用前值(one-shot 无增量语义,不覆盖为占位)", async () => {
|
|
356
|
+
const record = makeMinimalRecord({ id: "rec-oneshot-cont", chatMode: false });
|
|
357
|
+
record.status = "closed";
|
|
358
|
+
// 上一轮真实产出(续轮 record.result 前值)
|
|
359
|
+
record.result = "first round output";
|
|
360
|
+
const result = makeMinimalResult();
|
|
361
|
+
result.text = "";
|
|
362
|
+
result.success = true;
|
|
363
|
+
result.error = undefined;
|
|
364
|
+
await doFinalizeRoundToIdle(makeDeps(), record, result);
|
|
365
|
+
// one-shot record.result = 该 subagent 最终输出:空文本续轮沿用前值,不被 "(empty)" 覆盖
|
|
366
|
+
expect(record.result).toBe("first round output");
|
|
367
|
+
});
|
|
342
368
|
});
|
|
@@ -63,6 +63,15 @@ import { PassThrough } from "node:stream";
|
|
|
63
63
|
import { createRecord } from "../../execution-record.ts";
|
|
64
64
|
import type { RunOptions, SessionRunnerContext } from "../../session-runner.ts";
|
|
65
65
|
|
|
66
|
+
/** FakeChild 的假 pid(满足 ChildProcess.pid 形状,无真实进程语义)。 */
|
|
67
|
+
const FAKE_PID = 12345;
|
|
68
|
+
|
|
69
|
+
/** waitForSpawn 默认超时(ms),超时说明 spawn 未被调(runSpawn 前置 await 卡死)。 */
|
|
70
|
+
const WAIT_SPAWN_TIMEOUT_MS = 1000;
|
|
71
|
+
|
|
72
|
+
/** waitForSpawn 轮询间隔(ms),微任务级等待的粒度。 */
|
|
73
|
+
const SPAWN_POLL_INTERVAL_MS = 5;
|
|
74
|
+
|
|
66
75
|
/**
|
|
67
76
|
* FakeChild:模拟 ChildProcess(EventEmitter + PassThrough streams)。
|
|
68
77
|
*
|
|
@@ -74,7 +83,7 @@ import type { RunOptions, SessionRunnerContext } from "../../session-runner.ts";
|
|
|
74
83
|
* 2. C13 e2e 测试可直接 `new FakeChild()` 手动构造 child 喂给 ui-request-queue(不经 spawn)。
|
|
75
84
|
*/
|
|
76
85
|
export class FakeChild extends EventEmitter {
|
|
77
|
-
pid =
|
|
86
|
+
pid = FAKE_PID;
|
|
78
87
|
stdout = new PassThrough();
|
|
79
88
|
stderr = new PassThrough();
|
|
80
89
|
stdin = new PassThrough();
|
|
@@ -136,14 +145,14 @@ export function lastSpawnedChild<
|
|
|
136
145
|
*/
|
|
137
146
|
export async function waitForSpawn<
|
|
138
147
|
T extends { mock: { results: unknown[] } },
|
|
139
|
-
>(mockSpawn: T, timeoutMs =
|
|
148
|
+
>(mockSpawn: T, timeoutMs: number = WAIT_SPAWN_TIMEOUT_MS): Promise<void> {
|
|
140
149
|
const start = Date.now();
|
|
141
150
|
const baseline = mockSpawn.mock.results.length;
|
|
142
151
|
while (mockSpawn.mock.results.length <= baseline) {
|
|
143
152
|
if (Date.now() - start > timeoutMs) {
|
|
144
153
|
throw new Error(`spawn was not called within ${timeoutMs}ms`);
|
|
145
154
|
}
|
|
146
|
-
await new Promise((r) => setTimeout(r,
|
|
155
|
+
await new Promise((r) => setTimeout(r, SPAWN_POLL_INTERVAL_MS));
|
|
147
156
|
}
|
|
148
157
|
}
|
|
149
158
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// W1 红灯测试:host-mode.ts 主进程运行模式分类工具。
|
|
4
4
|
//
|
|
5
|
-
// 测试对象:extensions/subagent-workflow/src/execution/host-mode.ts(新建)
|
|
5
|
+
// 测试对象:extensions/universal/subagent-workflow/src/execution/host-mode.ts(新建)
|
|
6
6
|
// 契约来源:.fix-plans/00-master-summary.md §一 冲突 4 + §2.5(W4 守卫消费点)
|
|
7
7
|
//
|
|
8
8
|
// 三个导出:
|