paseo-req-kit 1.11.0 → 1.12.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/bin/req.mjs +1 -0
- package/config.example.json +3 -1
- package/contracts/cli-json.md +2 -0
- package/contracts/state.md +3 -1
- package/contracts/usage-record.md +4 -1
- package/docs/adr/0031-fewer-interventions.md +33 -0
- package/docs/adr/README.md +1 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +3 -2
- package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +21 -3
- package/engine/prompts/04b-design-revise.md +15 -0
- package/engine/src/api.ts +33 -1
- package/engine/src/card-cmd.ts +8 -1
- package/engine/src/card.ts +9 -2
- package/engine/src/config.ts +23 -2
- package/engine/src/configure.ts +161 -36
- package/engine/src/control.ts +1 -0
- package/engine/src/factory.ts +48 -4
- package/engine/src/main.ts +1 -0
- package/engine/src/paseo-profiles.ts +2 -0
- package/engine/src/pipeline.ts +161 -26
- package/engine/src/records.ts +8 -0
- package/engine/src/state.ts +30 -0
- package/engine/src/stats.ts +90 -0
- package/engine/src/steps.ts +4 -1
- package/install.mjs +5 -1
- package/package.json +2 -2
- package/paseo-plugin/OVERVIEW.md +2 -0
- package/paseo-plugin/client/data.ts +15 -1
- package/paseo-plugin/client/models.tsx +117 -0
- package/paseo-plugin/client/settings.tsx +3 -1
- package/paseo-plugin/index.server.ts +3 -1
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +14 -1
- package/paseo-plugin/shared/contracts.ts +37 -0
- package/skills/req/playbooks/new.md +14 -14
- package/skills/req/playbooks/small.md +3 -3
- package/skills/req/playbooks/unattended.md +4 -2
package/engine/src/configure.ts
CHANGED
|
@@ -122,6 +122,48 @@ async function chooseRole(
|
|
|
122
122
|
return profileFor(provider, model, modeId);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** One agent a role can use: a Paseo profile, or a provider's model in its unattended mode (1.12). */
|
|
126
|
+
export interface AgentChoice {
|
|
127
|
+
id: string;
|
|
128
|
+
label: string;
|
|
129
|
+
provider: string;
|
|
130
|
+
profile: AgentProfile;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Everything the daemon offers that can run unattended; Paseo profiles first. */
|
|
134
|
+
export async function agentChoices(catalog: Catalog): Promise<AgentChoice[]> {
|
|
135
|
+
const out: AgentChoice[] = [];
|
|
136
|
+
for (const p of ((await catalog.profiles?.().catch(() => [])) ?? []).filter((x) => x.model)) {
|
|
137
|
+
out.push({ id: `paseo:${p.name}`, label: `Paseo profile「${p.name}」:${p.provider}/${p.model}`, provider: p.provider ?? "", profile: { paseoProfile: p.name } });
|
|
138
|
+
}
|
|
139
|
+
for (const provider of (await catalog.providers()).filter((p) => p.status === "ready")) {
|
|
140
|
+
const modes = await catalog.modes(provider.id).catch(() => []);
|
|
141
|
+
const modeId = unattendedMode(provider.id, modes);
|
|
142
|
+
// A provider whose modes all stop for approval would hold the pipeline: left out.
|
|
143
|
+
if (modes.length > 0 && !modeId) continue;
|
|
144
|
+
for (const m of await catalog.models(provider.id).catch(() => [])) {
|
|
145
|
+
out.push({
|
|
146
|
+
id: `${provider.id}/${m.id}`,
|
|
147
|
+
label: `${m.label && m.label !== m.id ? `${m.label}(${provider.id}/${m.id})` : `${provider.id}/${m.id}`}${modeId ? `,模式 ${modeId}` : ""}`,
|
|
148
|
+
provider: provider.id,
|
|
149
|
+
profile: profileFor(provider.id, m.id, modeId),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The recommended fallback: another vendor than both roles if there is one, else the reviewer's when it differs from the builder's. */
|
|
157
|
+
export function recommendFallback(choices: AgentChoice[], builderProvider: string, reviewerProvider: string): AgentChoice | null {
|
|
158
|
+
return (
|
|
159
|
+
choices.find((c) => c.provider !== builderProvider && c.provider !== reviewerProvider) ??
|
|
160
|
+
(reviewerProvider !== builderProvider ? (choices.find((c) => c.provider === reviewerProvider) ?? null) : null)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Takes the recommended answer to every question (install, `req configure --auto`). */
|
|
165
|
+
export const recommended: Chooser = async (_question, options) => options[0].value;
|
|
166
|
+
|
|
125
167
|
export interface WizardResult {
|
|
126
168
|
config: Record<string, unknown>;
|
|
127
169
|
summary: string[];
|
|
@@ -133,6 +175,17 @@ export async function runWizard(existing: Record<string, unknown>, catalog: Cata
|
|
|
133
175
|
const providerOf = (p: AgentProfile) => (p.provider ?? profiles.find((x) => x.name === p.paseoProfile)?.provider ?? "").split("/")[0];
|
|
134
176
|
const builder = await chooseRole("builder", catalog, choose, null, profiles);
|
|
135
177
|
const reviewer = await chooseRole("reviewer", catalog, choose, providerOf(builder), profiles);
|
|
178
|
+
const choices = await agentChoices(catalog);
|
|
179
|
+
const suggested = recommendFallback(choices, providerOf(builder), providerOf(reviewer));
|
|
180
|
+
const others = choices.filter((c) => c.id !== suggested?.id && c.provider !== providerOf(builder)).slice(0, 8);
|
|
181
|
+
const fallback = await choose<AgentProfile | null>(
|
|
182
|
+
"备用模型:builder 或 reviewer 一直限流、额度用完或报错时,换它把这一步做完,而不是停下等人。用哪个?",
|
|
183
|
+
[
|
|
184
|
+
...(suggested ? [{ label: `${suggested.label}(推荐:与主模型不同厂商)`, value: suggested.profile as AgentProfile | null }] : []),
|
|
185
|
+
{ label: "不用备用模型", value: null },
|
|
186
|
+
...others.map((c) => ({ label: c.label, value: c.profile as AgentProfile | null })),
|
|
187
|
+
],
|
|
188
|
+
);
|
|
136
189
|
const defaultProfile = await choose("没有特别说明时,需求默认走哪个档位?", [
|
|
137
190
|
{ label: "标准:完整的方案审核、代码评审和验证(推荐)", value: "standard" },
|
|
138
191
|
{ label: "快速:每个环节一轮,适合以小改动为主的项目", value: "quick" },
|
|
@@ -141,14 +194,18 @@ export async function runWizard(existing: Record<string, unknown>, catalog: Cata
|
|
|
141
194
|
{ label: "要(推荐)", value: true },
|
|
142
195
|
{ label: "不要,只发需要处理的事和最终结果", value: false },
|
|
143
196
|
]);
|
|
144
|
-
const config = { ...existing, agents: { builder, reviewer }, defaultProfile, notifyMilestones };
|
|
197
|
+
const config = { ...existing, agents: { builder, reviewer, ...(fallback ? { fallback } : {}) }, defaultProfile, notifyMilestones };
|
|
145
198
|
const describe = (p: AgentProfile) =>
|
|
146
199
|
p.paseoProfile
|
|
147
200
|
? `Paseo profile「${p.paseoProfile}」(在 Paseo 中修改这个 profile 即可换模型)`
|
|
148
201
|
: `${p.provider}${p.modeId ? `(模式 ${p.modeId})` : ""}${p.featureValues ? ",自动接受权限请求" : ""}`;
|
|
149
202
|
return {
|
|
150
203
|
config,
|
|
151
|
-
summary: [
|
|
204
|
+
summary: [
|
|
205
|
+
`builder:${describe(builder)}`,
|
|
206
|
+
`reviewer:${describe(reviewer)}${providerOf(reviewer) === providerOf(builder) ? "(只有一种 agent 可用,评审用同一个;以后登录第二种 agent 再运行 req configure,评审更容易发现问题)" : ""}`,
|
|
207
|
+
`备用模型:${fallback ? describe(fallback) : "不用"}`,
|
|
208
|
+
`默认档位:${defaultProfile}`, `进展通知:${notifyMilestones ? "开" : "关"}`],
|
|
152
209
|
};
|
|
153
210
|
}
|
|
154
211
|
|
|
@@ -168,26 +225,35 @@ function terminalChooser(): { choose: Chooser; close: () => void } {
|
|
|
168
225
|
return { choose, close: () => rl.close() };
|
|
169
226
|
}
|
|
170
227
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const configPath = values.config ? path.resolve(values.config) : defaultConfigPath();
|
|
174
|
-
const existing = fs.existsSync(configPath)
|
|
228
|
+
function readConfigFile(configPath: string): Record<string, unknown> {
|
|
229
|
+
return fs.existsSync(configPath)
|
|
175
230
|
? (JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>)
|
|
176
231
|
: (JSON.parse(fs.readFileSync(path.join(KIT_ROOT, "config.example.json"), "utf8")) as Record<string, unknown>);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeConfigFile(configPath: string, config: Record<string, unknown>): void {
|
|
235
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
236
|
+
if (fs.existsSync(configPath)) fs.copyFileSync(configPath, `${configPath}.bak-${Date.now()}`);
|
|
237
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The agents the plugin's settings page chose (1.12): checked, then written to the local config. */
|
|
241
|
+
export function applyAgents(existing: Record<string, unknown>, raw: unknown): Record<string, unknown> {
|
|
242
|
+
const input = (raw ?? {}) as { builder?: unknown; reviewer?: unknown; fallback?: unknown };
|
|
243
|
+
const isProfile = (p: unknown): p is AgentProfile => {
|
|
244
|
+
if (!p || typeof p !== "object") return false;
|
|
245
|
+
const q = p as AgentProfile;
|
|
246
|
+
return (typeof q.paseoProfile === "string" && q.paseoProfile.trim() !== "") || (typeof q.provider === "string" && /^[^/]+\/.+/.test(q.provider) && !q.provider.includes("CHANGE-ME"));
|
|
180
247
|
};
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const catalog: Catalog = {
|
|
248
|
+
if (!isProfile(input.builder) || !isProfile(input.reviewer)) throw new Error("builder 和 reviewer 都要选一个 agent");
|
|
249
|
+
if (input.fallback !== null && input.fallback !== undefined && !isProfile(input.fallback)) throw new Error("备用模型的设置不对");
|
|
250
|
+
const agents: Record<string, AgentProfile> = { builder: input.builder, reviewer: input.reviewer };
|
|
251
|
+
if (input.fallback) agents.fallback = input.fallback as AgentProfile;
|
|
252
|
+
return { ...existing, agents };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function daemonCatalog(client: Awaited<ReturnType<typeof connect>>): Catalog {
|
|
256
|
+
return {
|
|
191
257
|
async providers() {
|
|
192
258
|
const snapshot = await client.providers.waitForReady({ timeoutMs: 60_000 });
|
|
193
259
|
return snapshot.entries.map((e) => ({ id: e.provider, status: e.status }));
|
|
@@ -202,26 +268,85 @@ async function main(): Promise<number> {
|
|
|
202
268
|
return profilesFromDaemonConfig((await client.config.get()).config) ?? [];
|
|
203
269
|
},
|
|
204
270
|
};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function main(): Promise<number> {
|
|
274
|
+
const { values } = parseArgs({
|
|
275
|
+
args: process.argv.slice(2),
|
|
276
|
+
options: { config: { type: "string" }, auto: { type: "boolean", default: false }, "catalog-json": { type: "boolean", default: false }, set: { type: "string" } },
|
|
277
|
+
});
|
|
278
|
+
const configPath = values.config ? path.resolve(values.config) : defaultConfigPath();
|
|
279
|
+
const existing = readConfigFile(configPath);
|
|
280
|
+
const json = (data: unknown) => console.log(JSON.stringify(data));
|
|
281
|
+
|
|
282
|
+
if (values.set !== undefined) {
|
|
283
|
+
try {
|
|
284
|
+
writeConfigFile(configPath, applyAgents(existing, JSON.parse(values.set)));
|
|
285
|
+
json({ ok: true, message: `已写入 ${configPath}(原文件已备份)。之后开始的需求使用新的设置。` });
|
|
286
|
+
return 0;
|
|
287
|
+
} catch (error) {
|
|
288
|
+
json({ ok: false, message: (error as Error).message });
|
|
216
289
|
return 1;
|
|
217
290
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const settings = {
|
|
294
|
+
daemonUrl: typeof existing.daemonUrl === "string" ? existing.daemonUrl : "ws://127.0.0.1:6767/ws",
|
|
295
|
+
daemonPasswordEnv: typeof existing.daemonPasswordEnv === "string" ? existing.daemonPasswordEnv : "PASEO_DAEMON_PASSWORD",
|
|
296
|
+
};
|
|
297
|
+
if (!values["catalog-json"]) console.log(`配置向导:连接 Paseo daemon ${settings.daemonUrl} ……`);
|
|
298
|
+
let client;
|
|
299
|
+
try {
|
|
300
|
+
// A daemon that is not running must not hold up the installer: give up after 20 seconds.
|
|
301
|
+
let timer: NodeJS.Timeout | undefined;
|
|
302
|
+
client = await Promise.race([
|
|
303
|
+
connect(settings as never),
|
|
304
|
+
new Promise<never>((_, reject) => {
|
|
305
|
+
timer = setTimeout(() => reject(new Error("20 秒内没有连上")), 20_000);
|
|
306
|
+
}),
|
|
307
|
+
]).finally(() => clearTimeout(timer));
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const why = `无法连接 daemon:${(error as Error).message}。请确认 Paseo 正在运行;设置了密码时,确认环境变量 ${settings.daemonPasswordEnv} 已设置。`;
|
|
310
|
+
if (values["catalog-json"]) json({ ok: false, message: why });
|
|
311
|
+
else console.error(why);
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
const catalog = daemonCatalog(client);
|
|
315
|
+
try {
|
|
316
|
+
if (values["catalog-json"]) {
|
|
317
|
+
const choices = await agentChoices(catalog);
|
|
318
|
+
const auto = await runWizard({}, catalog, recommended).catch(() => null);
|
|
319
|
+
json({ ok: true, current: existing.agents ?? null, choices, recommended: auto ? auto.config.agents : null });
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
if (values.auto) {
|
|
323
|
+
const { config, summary } = await runWizard(existing, catalog, recommended);
|
|
324
|
+
writeConfigFile(configPath, config);
|
|
325
|
+
console.log("已按推荐自动选好 agent:");
|
|
326
|
+
for (const line of summary) console.log(` ${line}`);
|
|
327
|
+
console.log(`写入 ${configPath}。要换:运行 req configure,或在 Paseo 的 设置 → 插件 → req-kit 中修改。`);
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
const terminal = terminalChooser();
|
|
331
|
+
try {
|
|
332
|
+
const { config, summary } = await runWizard(existing, catalog, terminal.choose);
|
|
333
|
+
console.log("\n将写入以下设置:");
|
|
334
|
+
for (const line of summary) console.log(` ${line}`);
|
|
335
|
+
const ok = await terminal.choose("确认写入吗?", [
|
|
336
|
+
{ label: "写入", value: true },
|
|
337
|
+
{ label: "放弃", value: false },
|
|
338
|
+
]);
|
|
339
|
+
if (!ok) {
|
|
340
|
+
console.log("没有修改配置。");
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
writeConfigFile(configPath, config);
|
|
344
|
+
console.log(`\n已写入 ${configPath}(原文件已备份)。接下来运行 npm run smoke -- check 确认两个 agent 能正常工作。`);
|
|
345
|
+
return 0;
|
|
346
|
+
} finally {
|
|
347
|
+
terminal.close();
|
|
348
|
+
}
|
|
223
349
|
} finally {
|
|
224
|
-
terminal.close();
|
|
225
350
|
await client.close().catch(() => undefined);
|
|
226
351
|
}
|
|
227
352
|
}
|
package/engine/src/control.ts
CHANGED
|
@@ -193,6 +193,7 @@ export async function stopPipeline(input: StopInput): Promise<{ stopped: boolean
|
|
|
193
193
|
}
|
|
194
194
|
latest.status = "failed";
|
|
195
195
|
latest.stopReason = wasAlive ? "已被手动停止" : "后台进程已意外退出";
|
|
196
|
+
latest.stopKind = wasAlive ? "stopped" : "process_exit";
|
|
196
197
|
writeState(input.statePath, latest);
|
|
197
198
|
|
|
198
199
|
// The step agent keeps working unless told otherwise; stop it so it changes nothing more.
|
package/engine/src/factory.ts
CHANGED
|
@@ -16,11 +16,14 @@ import fs from "node:fs";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { parseArgs } from "node:util";
|
|
18
18
|
import { cardRoutes, findCardEntry, hashText, parseJsonOutput } from "./card.js";
|
|
19
|
-
import { beginFromCard, type CardCmdOptions, cardIntegrations, checkCards } from "./card-cmd.js";
|
|
20
|
-
import {
|
|
19
|
+
import { beginFromCard, type CardCmdOptions, cardIntegrations, checkCards, withRuntime } from "./card-cmd.js";
|
|
20
|
+
import { resumeCommand, statusCommand } from "./cli-args.js";
|
|
21
|
+
import { loadConfig, type PipelineConfig } from "./config.js";
|
|
22
|
+
import { watchdogPrompt } from "./messages.js";
|
|
23
|
+
import { readIndex } from "./records.js";
|
|
21
24
|
import { type IntegrationConfig, renderArgs, runCommand, type Runner } from "./integrations.js";
|
|
22
25
|
import { defaultConfigPath, ENGINE_DIR, KIT_ROOT, kitHome } from "./locations.js";
|
|
23
|
-
import { isAlive } from "./state.js";
|
|
26
|
+
import { isAlive, readState, statePath } from "./state.js";
|
|
24
27
|
|
|
25
28
|
const USAGE = `用法:
|
|
26
29
|
req factory once 检查一次:为新卡片开始需求,把需求方的新回复转给需求会话
|
|
@@ -49,6 +52,8 @@ export interface PollResult {
|
|
|
49
52
|
started: string[];
|
|
50
53
|
skipped: string[];
|
|
51
54
|
relayed: string[];
|
|
55
|
+
/** 1.12: interrupted pipelines whose chat was asked to continue them. */
|
|
56
|
+
resumed: string[];
|
|
52
57
|
errors: string[];
|
|
53
58
|
notes: string[];
|
|
54
59
|
}
|
|
@@ -143,7 +148,7 @@ export async function pollOnce(configPath: string, opts: CardCmdOptions = {}, no
|
|
|
143
148
|
const env = opts.env ?? process.env;
|
|
144
149
|
const dir = factoryDir(env);
|
|
145
150
|
fs.mkdirSync(dir, { recursive: true });
|
|
146
|
-
const result: PollResult = { started: [], skipped: [], relayed: [], errors: [], notes: [] };
|
|
151
|
+
const result: PollResult = { started: [], skipped: [], relayed: [], resumed: [], errors: [], notes: [] };
|
|
147
152
|
|
|
148
153
|
// One look at a time on this machine.
|
|
149
154
|
const lock = path.join(dir, "poll.lock");
|
|
@@ -207,6 +212,12 @@ export async function pollOnce(configPath: string, opts: CardCmdOptions = {}, no
|
|
|
207
212
|
}
|
|
208
213
|
writeJson(skippedFile, skipped);
|
|
209
214
|
|
|
215
|
+
try {
|
|
216
|
+
await nudgeInterrupted(config, env, dir, result);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
result.errors.push(`续跑中断的需求:${(error as Error).message}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
210
221
|
try {
|
|
211
222
|
const checked = await checkCards(null, opts, configPath, { skipFinished: true });
|
|
212
223
|
for (const line of checked.lines) {
|
|
@@ -226,11 +237,44 @@ export async function pollOnce(configPath: string, opts: CardCmdOptions = {}, no
|
|
|
226
237
|
return result;
|
|
227
238
|
}
|
|
228
239
|
|
|
240
|
+
/**
|
|
241
|
+
* 1.12: a pipeline on this machine still marked running whose process is gone (the machine
|
|
242
|
+
* restarted, the process was killed): its chat gets the same message as the watchdog's, so
|
|
243
|
+
* the chat checks and continues it. Once per process; a chat that cannot be reached is noted.
|
|
244
|
+
*/
|
|
245
|
+
async function nudgeInterrupted(config: PipelineConfig, env: NodeJS.ProcessEnv, dir: string, result: PollResult): Promise<void> {
|
|
246
|
+
const file = path.join(dir, "nudged.json");
|
|
247
|
+
const nudged = readJson<Record<string, number>>(file, {});
|
|
248
|
+
const due: Array<{ key: string; pid: number; origin: string; featureDir: string; repo: string }> = [];
|
|
249
|
+
for (const entry of readIndex(env)) {
|
|
250
|
+
if (!entry.present) continue;
|
|
251
|
+
const state = readState(statePath(entry.repo, entry.featureDir));
|
|
252
|
+
if (!state || state.status !== "running" || isAlive(state.pid) || !state.originAgentId) continue;
|
|
253
|
+
if (nudged[entry.key] === state.pid) continue;
|
|
254
|
+
due.push({ key: entry.key, pid: state.pid, origin: state.originAgentId, featureDir: entry.featureDir, repo: entry.sourceRepo ?? entry.repo });
|
|
255
|
+
}
|
|
256
|
+
if (due.length === 0) return;
|
|
257
|
+
await withRuntime(config, async (runtime) => {
|
|
258
|
+
for (const d of due) {
|
|
259
|
+
const commands = { repo: d.repo, featureDir: d.featureDir, configPath: null };
|
|
260
|
+
try {
|
|
261
|
+
await runtime.notifyAgent(d.origin, watchdogPrompt({ featureDir: d.featureDir, statusCommand: statusCommand(commands), resumeCommand: resumeCommand(commands) }));
|
|
262
|
+
nudged[d.key] = d.pid;
|
|
263
|
+
result.resumed.push(`${path.join(d.repo, d.featureDir)}:后台进程已退出,已请它的对话检查并继续`);
|
|
264
|
+
} catch (error) {
|
|
265
|
+
result.errors.push(`${path.join(d.repo, d.featureDir)} 的后台进程已退出,但没能通知它的对话:${(error as Error).message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
writeJson(file, nudged);
|
|
270
|
+
}
|
|
271
|
+
|
|
229
272
|
function describe(result: PollResult, now = new Date()): string[] {
|
|
230
273
|
const stamp = now.toLocaleString("zh-CN", { hour12: false });
|
|
231
274
|
const lines: string[] = [];
|
|
232
275
|
for (const s of result.started) lines.push(`[${stamp}] 已开始:${s}`);
|
|
233
276
|
for (const s of result.relayed) lines.push(`[${stamp}] ${s}`);
|
|
277
|
+
for (const s of result.resumed) lines.push(`[${stamp}] 续跑:${s}`);
|
|
234
278
|
for (const s of result.skipped) lines.push(`[${stamp}] 没能接:${s}`);
|
|
235
279
|
for (const s of result.errors) lines.push(`[${stamp}] 出错:${s}`);
|
|
236
280
|
for (const s of result.notes) lines.push(`[${stamp}] ${s}`);
|
package/engine/src/main.ts
CHANGED
|
@@ -272,6 +272,7 @@ async function main(): Promise<number> {
|
|
|
272
272
|
log(`收到 ${signal},流水线停止`);
|
|
273
273
|
state.status = "failed";
|
|
274
274
|
state.stopReason = `流水线进程被 ${signal} 终止`;
|
|
275
|
+
state.stopKind = "stopped";
|
|
275
276
|
state.queuedSince = null;
|
|
276
277
|
writeState(stateFile, state);
|
|
277
278
|
recordRun();
|
|
@@ -93,6 +93,7 @@ export function usesPaseoProfiles(config: PipelineConfig): boolean {
|
|
|
93
93
|
return [
|
|
94
94
|
config.agents.builder,
|
|
95
95
|
config.agents.reviewer,
|
|
96
|
+
...(config.agents.fallback ? [config.agents.fallback] : []),
|
|
96
97
|
...config.reviewPanel.members,
|
|
97
98
|
...config.designProposals.members,
|
|
98
99
|
...Object.values(config.phases.verify.agents),
|
|
@@ -108,6 +109,7 @@ export function resolveConfigProfiles(config: PipelineConfig, available: PaseoPr
|
|
|
108
109
|
agents: {
|
|
109
110
|
builder: resolveAgentProfile("agents.builder", config.agents.builder, available),
|
|
110
111
|
reviewer: resolveAgentProfile("agents.reviewer", config.agents.reviewer, available),
|
|
112
|
+
...(config.agents.fallback ? { fallback: resolveAgentProfile("agents.fallback", config.agents.fallback, available) } : {}),
|
|
111
113
|
},
|
|
112
114
|
reviewPanel: {
|
|
113
115
|
...config.reviewPanel,
|