paseo-req-kit 0.0.0-stage → 1.7.0-beta.3
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 +217 -0
- package/LICENSE +21 -0
- package/README.md +141 -2
- package/SCOPE.md +11 -0
- package/bin/locate.mjs +46 -0
- package/bin/req-doctor.mjs +21 -0
- package/bin/req.mjs +175 -0
- package/bin/start-pipeline +21 -0
- package/bin/start-pipeline.mjs +22 -0
- package/config.example.json +101 -0
- package/contracts/README.md +18 -0
- package/contracts/feature-files.md +87 -0
- package/contracts/notifier-event.md +71 -0
- package/contracts/result-file.md +17 -0
- package/contracts/spec.md +34 -0
- package/contracts/state.md +44 -0
- package/contracts/verify.md +39 -0
- package/docs/adr/0001-step-agents-under-origin-chat.md +15 -0
- package/docs/adr/0002-no-directory-switch-in-chat.md +15 -0
- package/docs/adr/0003-script-engine-not-hub.md +15 -0
- package/docs/adr/0004-result-files.md +15 -0
- package/docs/adr/0005-no-default-branch-local-commits-only.md +15 -0
- package/docs/adr/0006-skill-install-targets.md +15 -0
- package/docs/adr/0007-own-interview-skill.md +15 -0
- package/docs/adr/0008-entries-do-not-call-entries.md +15 -0
- package/docs/adr/0009-versioned-install-layout.md +15 -0
- package/docs/adr/0010-generated-feature-overview.md +15 -0
- package/docs/adr/0011-resume-after-completion.md +15 -0
- package/docs/adr/0012-review-panel.md +18 -0
- package/docs/adr/0013-no-release-tooling.md +15 -0
- package/docs/adr/0014-notifier-slot.md +19 -0
- package/docs/adr/0015-incremental-continue.md +17 -0
- package/docs/adr/0016-upgrade-from-tags.md +19 -0
- package/docs/adr/0017-use-paseo-capabilities.md +18 -0
- package/docs/adr/0018-own-worktree-option.md +21 -0
- package/docs/adr/0019-parallel-orchestration.md +21 -0
- package/docs/adr/0020-scheduling-through-the-chat.md +21 -0
- package/docs/adr/0021-npm-version-manager.md +19 -0
- package/docs/adr/0022-develop-and-verify-phases.md +32 -0
- package/docs/adr/README.md +26 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +78 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +348 -0
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +85 -0
- 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 +251 -0
- package/engine/prompts/00-shared-context.md +100 -0
- package/engine/prompts/02-spec-review.md +46 -0
- package/engine/prompts/03-design.md +50 -0
- package/engine/prompts/03a-design-proposal.md +30 -0
- package/engine/prompts/04-design-review.md +72 -0
- package/engine/prompts/04b-design-revise.md +34 -0
- package/engine/prompts/05-implement.md +66 -0
- package/engine/prompts/06-code-review.md +66 -0
- package/engine/prompts/07-review-fix.md +41 -0
- package/engine/prompts/07b-handoff.md +45 -0
- package/engine/prompts/08-service-start.md +59 -0
- package/engine/prompts/09-verify.md +90 -0
- package/engine/prompts/10-defect-fix.md +41 -0
- package/engine/prompts/10b-reverify.md +43 -0
- package/engine/prompts/11-report.md +93 -0
- package/engine/prompts/browser-verify.md +13 -0
- package/engine/prompts/incremental.md +21 -0
- package/engine/prompts/panel-member.md +38 -0
- package/engine/prompts/paseo-services.md +9 -0
- package/engine/prompts/verify-deployed.md +14 -0
- package/engine/src/activity.ts +87 -0
- package/engine/src/cli-args.ts +204 -0
- package/engine/src/config.ts +479 -0
- package/engine/src/configure.ts +237 -0
- package/engine/src/control.ts +230 -0
- package/engine/src/daemon-check.ts +104 -0
- package/engine/src/daemon-version.ts +66 -0
- package/engine/src/digest.ts +133 -0
- package/engine/src/doctor.ts +435 -0
- package/engine/src/eval.ts +324 -0
- package/engine/src/features.ts +268 -0
- package/engine/src/git.ts +30 -0
- package/engine/src/launch.ts +405 -0
- package/engine/src/locations.ts +55 -0
- package/engine/src/main.ts +313 -0
- package/engine/src/messages.ts +308 -0
- package/engine/src/notifiers.ts +76 -0
- package/engine/src/notify-test.ts +97 -0
- package/engine/src/paseo-json.ts +73 -0
- package/engine/src/paseo-profiles.ts +133 -0
- package/engine/src/paseo-runtime.ts +437 -0
- package/engine/src/paths.ts +10 -0
- package/engine/src/pipeline.ts +1126 -0
- package/engine/src/preflight.ts +298 -0
- package/engine/src/prompts.ts +72 -0
- package/engine/src/queue.ts +61 -0
- package/engine/src/runtime-loader.ts +14 -0
- package/engine/src/runtime.ts +155 -0
- package/engine/src/schedule-time.ts +51 -0
- package/engine/src/smoke-test.ts +202 -0
- package/engine/src/spec-review.ts +100 -0
- package/engine/src/state.ts +278 -0
- package/engine/src/steps.ts +242 -0
- package/engine/src/usage.ts +67 -0
- package/engine/src/worktree.ts +72 -0
- package/install.mjs +928 -0
- package/notifiers/im-template.mjs +58 -0
- package/package.json +67 -4
- package/principles/INDEX.md +11 -0
- package/skills/acceptance-criteria/SKILL.md +52 -0
- package/skills/principle-delete-before-add/SKILL.md +33 -0
- package/skills/principle-dont-block-on-humans/SKILL.md +35 -0
- package/skills/principle-fix-root-cause/SKILL.md +33 -0
- package/skills/principle-prove-it-works/SKILL.md +33 -0
- package/skills/principle-report-honestly/SKILL.md +33 -0
- package/skills/principle-speak-to-reader/SKILL.md +33 -0
- package/skills/principle-this-requirement-only/SKILL.md +33 -0
- package/skills/req/SKILL.md +99 -0
- package/skills/req/playbooks/accept.md +65 -0
- package/skills/req/playbooks/change.md +51 -0
- package/skills/req/playbooks/incident.md +55 -0
- package/skills/req/playbooks/new.md +83 -0
- package/skills/req/playbooks/small.md +48 -0
- package/skills/req/playbooks/status.md +39 -0
- package/skills/req/playbooks/takeover.md +51 -0
- package/skills/req/playbooks/verify.md +57 -0
- package/skills/req-doctor/SKILL.md +36 -0
- package/skills/req-retro/SKILL.md +79 -0
- package/skills/req-setup/SKILL.md +101 -0
- package/skills/req-setup/templates/paseo/copy-local-files.mjs +32 -0
- package/skills/req-setup/templates/paseo/with-port.mjs +36 -0
- package/skills/requirement-interview/SKILL.md +94 -0
- package/skills/requirement-kickoff/SKILL.md +17 -0
- package/skills/result-report/SKILL.md +34 -0
- package/skills/spec-writing/SKILL.md +41 -0
- package/skills/spec-writing/spec-template.md +106 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// The long-running pipeline process. Normally started by launch.ts, detached
|
|
2
|
+
// from the chat agent's shell; can also be run in the foreground for debugging.
|
|
3
|
+
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { LAUNCHER, parseLaunchArgs, resumeCommand, statusCommand, verifyCommand } from "./cli-args.js";
|
|
7
|
+
import { dequeuedMessage, queuedMessage, watchdogPrompt } from "./messages.js";
|
|
8
|
+
import { slotHolders, slotsDir, tryAcquireSlot } from "./queue.js";
|
|
9
|
+
import { cronEvery, parseStartTime } from "./schedule-time.js";
|
|
10
|
+
import { loadConfig, PHASE_MODE_LABELS, PROFILE_LABELS } from "./config.js";
|
|
11
|
+
import { defaultConfigPath, kitVersion, localPromptsDir, PRINCIPLES_INDEX, PROMPTS_DIR } from "./locations.js";
|
|
12
|
+
import { satisfies } from "./daemon-check.js";
|
|
13
|
+
import { describeAgentProfile, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
|
|
14
|
+
import { runPipeline } from "./pipeline.js";
|
|
15
|
+
import { preflight, verifyPhaseProblem } from "./preflight.js";
|
|
16
|
+
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
17
|
+
import {
|
|
18
|
+
acquireLock,
|
|
19
|
+
ensurePipelineDir,
|
|
20
|
+
hashFile,
|
|
21
|
+
type PipelineState,
|
|
22
|
+
pipelineDir,
|
|
23
|
+
type RunPhases,
|
|
24
|
+
STATE_VERSION,
|
|
25
|
+
statePath,
|
|
26
|
+
writeState,
|
|
27
|
+
} from "./state.js";
|
|
28
|
+
|
|
29
|
+
function headCommit(repo: string): string | null {
|
|
30
|
+
try {
|
|
31
|
+
return execFileSync("git", ["rev-parse", "HEAD"], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function log(message: string): void {
|
|
38
|
+
console.log(`[${new Date().toISOString()}] ${message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main(): Promise<number> {
|
|
42
|
+
const args = parseLaunchArgs(process.argv.slice(2));
|
|
43
|
+
const configPath = args.configPath ?? defaultConfigPath();
|
|
44
|
+
const checked = preflight(args, hashFile);
|
|
45
|
+
const { repo, featureDir, mode } = checked;
|
|
46
|
+
const loaded = loadConfig(configPath, repo);
|
|
47
|
+
const { sources, ignored } = loaded;
|
|
48
|
+
let config = loaded.config;
|
|
49
|
+
log(`req-kit ${kitVersion()};配置:${sources.join(" → ")}`);
|
|
50
|
+
if (ignored.length > 0) log(`${ignored.join("、")} 只能在本机配置中设置,已忽略`);
|
|
51
|
+
if (config.requiredVersion && !satisfies(kitVersion(), config.requiredVersion)) {
|
|
52
|
+
log(`提醒:团队或项目要求 req-kit ${config.requiredVersion},当前为 ${kitVersion()}。流水线照常运行,请找时间升级(见 /req-doctor)。`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ensurePipelineDir(repo, featureDir);
|
|
56
|
+
const release = acquireLock(repo, featureDir);
|
|
57
|
+
const stateFile = statePath(repo, featureDir);
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
const specHash = hashFile(checked.specPath);
|
|
60
|
+
|
|
61
|
+
const createRuntime = await loadRuntimeFactory();
|
|
62
|
+
const runtime = await createRuntime({ config, log });
|
|
63
|
+
|
|
64
|
+
// A resume without --origin-agent keeps reporting to the chat that started the run.
|
|
65
|
+
const originAgentId = args.originAgentId ?? (mode.kind === "fresh" ? null : mode.previous.originAgentId);
|
|
66
|
+
|
|
67
|
+
const previousState = mode.kind === "fresh" ? null : mode.previous;
|
|
68
|
+
const reuse = mode.kind === "resume-reuse";
|
|
69
|
+
// Execution mode: --phases, else what this run already uses (continuing), else the spec, else the config.
|
|
70
|
+
let phaseMode =
|
|
71
|
+
args.phases ?? (reuse ? previousState?.phases?.mode : null) ?? checked.specPhases ?? previousState?.phases?.mode ?? config.phases.mode;
|
|
72
|
+
// --verify-at only makes sense with a gate; asking for it implies one.
|
|
73
|
+
if (args.verifyAt && phaseMode === "continuous" && !args.phases) phaseMode = "gated";
|
|
74
|
+
const phases: RunPhases = {
|
|
75
|
+
mode: phaseMode,
|
|
76
|
+
environment: args.verifyEnv ?? previousState?.phases?.environment ?? config.phases.verify.environment,
|
|
77
|
+
defects: args.defects ?? previousState?.phases?.defects ?? config.phases.verify.defects,
|
|
78
|
+
};
|
|
79
|
+
const verifyAtText = args.verifyAt ?? null;
|
|
80
|
+
const verifyAt = verifyAtText ? (parseStartTime(verifyAtText) ?? (Number.isNaN(Date.parse(verifyAtText)) ? null : new Date(verifyAtText))) : null;
|
|
81
|
+
|
|
82
|
+
// Checked before the state file is touched, so a refused launch leaves no record behind.
|
|
83
|
+
{
|
|
84
|
+
try {
|
|
85
|
+
// In a worktree created for the requirement, the chat stays in the checkout it was cut from.
|
|
86
|
+
const chatDir = args.sourceRepo ?? (mode.kind === "fresh" ? null : mode.previous.worktree?.sourceRepo) ?? repo;
|
|
87
|
+
if (verifyAtText && !verifyAt) throw new Error(`看不懂测试验证的开始时间"${verifyAtText}"`);
|
|
88
|
+
if (verifyAtText && phases.mode === "develop-only") throw new Error("只做研发时没有测试验证阶段,--verify-at 不适用");
|
|
89
|
+
if (checked.entersVerify) {
|
|
90
|
+
const problem = verifyPhaseProblem(repo, featureDir, phases.environment);
|
|
91
|
+
if (problem) throw new Error(problem);
|
|
92
|
+
}
|
|
93
|
+
if (originAgentId) await runtime.verifyOrigin(originAgentId, chatDir);
|
|
94
|
+
if (usesPaseoProfiles(config)) config = resolveConfigProfiles(config, await runtime.listPaseoProfiles());
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const reason = (error as Error).message;
|
|
97
|
+
log(`未启动:${reason}`);
|
|
98
|
+
if (originAgentId) {
|
|
99
|
+
await runtime
|
|
100
|
+
.notifyAgent(originAgentId, `【需求开发流水线通知】这条消息来自后台流水线,不是用户发的。流水线没有启动:${reason}`)
|
|
101
|
+
.catch(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
await runtime.close().catch(() => undefined);
|
|
104
|
+
release();
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log(`builder:${describeAgentProfile(config.agents.builder)};reviewer:${describeAgentProfile(config.agents.reviewer)}`);
|
|
110
|
+
|
|
111
|
+
const previous = mode.kind === "fresh" ? null : mode.previous;
|
|
112
|
+
const profile =
|
|
113
|
+
args.profile ?? (mode.kind === "resume-reuse" ? previous?.profile : null) ?? checked.specProfile ?? config.defaultProfile;
|
|
114
|
+
|
|
115
|
+
let state: PipelineState;
|
|
116
|
+
if (mode.kind === "fresh") {
|
|
117
|
+
state = {
|
|
118
|
+
version: STATE_VERSION,
|
|
119
|
+
repo,
|
|
120
|
+
featureDir,
|
|
121
|
+
status: "running",
|
|
122
|
+
attempt: 1,
|
|
123
|
+
specHash,
|
|
124
|
+
originAgentId,
|
|
125
|
+
pid: process.pid,
|
|
126
|
+
startedAt: now,
|
|
127
|
+
updatedAt: now,
|
|
128
|
+
results: {},
|
|
129
|
+
history: [],
|
|
130
|
+
stopReason: null,
|
|
131
|
+
outcome: null,
|
|
132
|
+
profile,
|
|
133
|
+
branch: checked.branch,
|
|
134
|
+
milestones: [],
|
|
135
|
+
incremental: null,
|
|
136
|
+
worktree: args.sourceRepo && args.workspaceId ? { workspaceId: args.workspaceId, sourceRepo: args.sourceRepo } : null,
|
|
137
|
+
phases,
|
|
138
|
+
phase: "develop",
|
|
139
|
+
verifyAt: verifyAt?.toISOString() ?? null,
|
|
140
|
+
handbacks: 0,
|
|
141
|
+
};
|
|
142
|
+
log(`开始新的流水线:${featureDir}(${PROFILE_LABELS[profile]}档,执行方式:${PHASE_MODE_LABELS[phases.mode]},分支 ${checked.branch ?? "无"})`);
|
|
143
|
+
} else {
|
|
144
|
+
const startOver = mode.kind !== "resume-reuse";
|
|
145
|
+
const previousImplemented = Object.keys(mode.previous.results).some((k) => k === "implement") ||
|
|
146
|
+
mode.previous.history.some((h) => h.stepId === "implement" && h.outcome === "completed");
|
|
147
|
+
state = {
|
|
148
|
+
...mode.previous,
|
|
149
|
+
status: "running",
|
|
150
|
+
attempt: startOver ? mode.previous.attempt + 1 : mode.previous.attempt,
|
|
151
|
+
specHash,
|
|
152
|
+
// A later launch from a different chat moves notifications to that chat.
|
|
153
|
+
originAgentId,
|
|
154
|
+
pid: process.pid,
|
|
155
|
+
results: startOver ? {} : mode.previous.results,
|
|
156
|
+
stopReason: null,
|
|
157
|
+
outcome: null,
|
|
158
|
+
profile,
|
|
159
|
+
branch: checked.branch,
|
|
160
|
+
// Older state files have no milestones; a fresh attempt sends them again.
|
|
161
|
+
milestones: startOver ? [] : (mode.previous.milestones ?? []),
|
|
162
|
+
incremental:
|
|
163
|
+
mode.kind === "resume-from-design"
|
|
164
|
+
? { fromAttempt: mode.previous.attempt, reason: mode.reason, baseCommit: headCommit(repo), hadImplementation: previousImplemented }
|
|
165
|
+
: mode.kind === "restart"
|
|
166
|
+
? null
|
|
167
|
+
: (mode.previous.incremental ?? null),
|
|
168
|
+
phases,
|
|
169
|
+
// The verify phase starts when development is done; any other start over goes back to development.
|
|
170
|
+
phase: checked.entersVerify ? "verify" : startOver ? "develop" : (mode.previous.phase ?? "develop"),
|
|
171
|
+
verifyAt: checked.entersVerify ? null : (verifyAt?.toISOString() ?? (startOver ? null : (mode.previous.verifyAt ?? null))),
|
|
172
|
+
handbacks: mode.kind === "restart" ? 0 : (mode.previous.handbacks ?? 0),
|
|
173
|
+
};
|
|
174
|
+
if (checked.entersVerify) {
|
|
175
|
+
// A develop-only run that ends with its development report gets a full report after verification.
|
|
176
|
+
delete state.results.report;
|
|
177
|
+
log(`开始测试验证阶段(${phases.environment === "deployed" ? "已部署的测试环境" : "本地"},缺陷处理:${phases.defects})`);
|
|
178
|
+
}
|
|
179
|
+
const why = mode.kind === "resume-from-design" ? mode.reason : mode.kind === "restart" ? "按 --restart 从头开始" : "从中断处继续,已完成的步骤沿用结果";
|
|
180
|
+
log(`继续流水线:${featureDir}(第 ${state.attempt} 次尝试,${PROFILE_LABELS[profile]}档)—— ${why}`);
|
|
181
|
+
}
|
|
182
|
+
writeState(stateFile, state);
|
|
183
|
+
if (state.worktree) log(`在为这个需求新建的工作区中开发:${repo}(Paseo 工作区 ${state.worktree.workspaceId},对话所在目录 ${state.worktree.sourceRepo})`);
|
|
184
|
+
|
|
185
|
+
if (state.worktree && mode.kind === "fresh") {
|
|
186
|
+
// paseo.json's worktree.setup (installing dependencies, copying .env) runs in the background.
|
|
187
|
+
const setup = await runtime.waitForWorkspaceSetup(state.worktree.workspaceId, 30 * 60_000).catch(() => ({ status: "none" as const, error: null }));
|
|
188
|
+
if (setup.status === "completed") log("新工作区的环境准备(paseo.json)已完成");
|
|
189
|
+
else if (setup.status === "failed" || setup.status === "timeout") {
|
|
190
|
+
const why = setup.status === "timeout" ? "30 分钟内没有完成" : `失败:${setup.error ?? "原因未知"}`;
|
|
191
|
+
log(`新工作区的环境准备(paseo.json)${why}。流水线继续,步骤会按验证方法自行准备环境。`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The chat's own directory: a tester's chat may sit in another checkout of the same repository.
|
|
196
|
+
const chatRepo = args.sourceRepo ?? state.worktree?.sourceRepo ?? repo;
|
|
197
|
+
const commands = {
|
|
198
|
+
resume: resumeCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
|
|
199
|
+
status: statusCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
|
|
200
|
+
verify: verifyCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
|
|
201
|
+
};
|
|
202
|
+
const notify = async (text: string) => {
|
|
203
|
+
log(`通知:\n${text}`);
|
|
204
|
+
if (originAgentId) await runtime.notifyAgent(originAgentId, text).catch((error: Error) => log(`通知发送失败:${error.message}`));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Reminders and automatic continuations from the previous run are no longer needed.
|
|
208
|
+
if (state.schedules?.length) {
|
|
209
|
+
for (const s of state.schedules) await runtime.cancelSchedule(s.id).catch((error: Error) => log(`取消定时任务 ${s.id} 失败:${error.message}`));
|
|
210
|
+
log(`已取消上次运行留下的 Paseo 定时任务:${state.schedules.map((s) => s.kind).join("、")}`);
|
|
211
|
+
state.schedules = [];
|
|
212
|
+
writeState(stateFile, state);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let releaseSlot = () => {};
|
|
216
|
+
const shutdown = (signal: string) => {
|
|
217
|
+
log(`收到 ${signal},流水线停止`);
|
|
218
|
+
state.status = "failed";
|
|
219
|
+
state.stopReason = `流水线进程被 ${signal} 终止`;
|
|
220
|
+
state.queuedSince = null;
|
|
221
|
+
writeState(stateFile, state);
|
|
222
|
+
releaseSlot();
|
|
223
|
+
release();
|
|
224
|
+
process.exit(1);
|
|
225
|
+
};
|
|
226
|
+
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
|
227
|
+
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
228
|
+
|
|
229
|
+
// Machine-wide limit on concurrent pipelines: wait for a free slot.
|
|
230
|
+
if (config.maxConcurrentPipelines > 0) {
|
|
231
|
+
const dir = slotsDir();
|
|
232
|
+
const holder = { pid: process.pid, repo, featureDir, since: new Date().toISOString() };
|
|
233
|
+
let slot = tryAcquireSlot(dir, config.maxConcurrentPipelines, holder);
|
|
234
|
+
if (!slot) {
|
|
235
|
+
const since = Date.now();
|
|
236
|
+
state.queuedSince = new Date(since).toISOString();
|
|
237
|
+
writeState(stateFile, state);
|
|
238
|
+
log(`本机同时运行的流水线已达上限(${config.maxConcurrentPipelines}),排队等待`);
|
|
239
|
+
await notify(queuedMessage({ featureDir, max: config.maxConcurrentPipelines, holders: slotHolders(dir) }));
|
|
240
|
+
while (!slot) {
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, config.queuePollSeconds * 1000));
|
|
242
|
+
slot = tryAcquireSlot(dir, config.maxConcurrentPipelines, holder);
|
|
243
|
+
}
|
|
244
|
+
state.queuedSince = null;
|
|
245
|
+
writeState(stateFile, state);
|
|
246
|
+
log("排队结束,开始运行");
|
|
247
|
+
await notify(dequeuedMessage({ featureDir, waitedMinutes: (Date.now() - since) / 60_000 }));
|
|
248
|
+
}
|
|
249
|
+
releaseSlot = slot;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Optional watchdog: the chat checks on the run now and then and continues it if the process died.
|
|
253
|
+
let watchdog: string | null = null;
|
|
254
|
+
if (config.watchdogMinutes > 0 && originAgentId) {
|
|
255
|
+
watchdog = await runtime
|
|
256
|
+
.scheduleMessage({
|
|
257
|
+
agentId: originAgentId,
|
|
258
|
+
cron: cronEvery(config.watchdogMinutes),
|
|
259
|
+
expiresAt: new Date(Date.now() + 3 * 24 * 3600_000),
|
|
260
|
+
name: `req-kit 定时检查:${path.basename(featureDir)}`,
|
|
261
|
+
prompt: watchdogPrompt({ featureDir, statusCommand: commands.status, resumeCommand: commands.resume }),
|
|
262
|
+
})
|
|
263
|
+
.catch((error: Error) => {
|
|
264
|
+
log(`预约定时检查失败:${error.message}`);
|
|
265
|
+
return null;
|
|
266
|
+
});
|
|
267
|
+
if (watchdog) {
|
|
268
|
+
state.schedules = [...(state.schedules ?? []), { id: watchdog, kind: "watchdog", at: null }];
|
|
269
|
+
writeState(stateFile, state);
|
|
270
|
+
log(`已预约定时检查(每 ${config.watchdogMinutes} 分钟,Paseo 定时任务 ${watchdog})`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const outcome = await runPipeline({
|
|
276
|
+
repo,
|
|
277
|
+
featureDir,
|
|
278
|
+
config,
|
|
279
|
+
runtime,
|
|
280
|
+
state,
|
|
281
|
+
statePath: stateFile,
|
|
282
|
+
prompts: { promptsDir: PROMPTS_DIR, localPromptsDir: localPromptsDir(), principlesFile: PRINCIPLES_INDEX },
|
|
283
|
+
logPath: path.join(pipelineDir(repo, featureDir), "pipeline.log"),
|
|
284
|
+
// Run from the chat, whose directory is the source checkout when the requirement has its own worktree.
|
|
285
|
+
resumeCommand: commands.resume,
|
|
286
|
+
statusCommand: commands.status,
|
|
287
|
+
verifyCommand: commands.verify,
|
|
288
|
+
log,
|
|
289
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
290
|
+
});
|
|
291
|
+
log(`流水线结束:${outcome}`);
|
|
292
|
+
return outcome === "completed" || outcome === "phase_done" ? 0 : outcome === "waiting_for_business" ? 3 : 1;
|
|
293
|
+
} finally {
|
|
294
|
+
// The run ended by itself: the watchdog has nothing left to watch.
|
|
295
|
+
if (watchdog) {
|
|
296
|
+
await runtime.cancelSchedule(watchdog).catch(() => undefined);
|
|
297
|
+
state.schedules = (state.schedules ?? []).filter((s) => s.id !== watchdog);
|
|
298
|
+
writeState(stateFile, state);
|
|
299
|
+
}
|
|
300
|
+
await runtime.close().catch(() => undefined);
|
|
301
|
+
releaseSlot();
|
|
302
|
+
release();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
main().then(
|
|
307
|
+
(code) => process.exit(code),
|
|
308
|
+
(error: Error) => {
|
|
309
|
+
console.error(`[${new Date().toISOString()}] 流水线无法启动:${error.message}`);
|
|
310
|
+
console.error(`启动命令:${LAUNCHER}`);
|
|
311
|
+
process.exit(2);
|
|
312
|
+
},
|
|
313
|
+
);
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// Text the pipeline sends into the chat that launched it, and follow-ups it
|
|
2
|
+
// sends to step agents. Messages to the chat are read by that chat's agent, who
|
|
3
|
+
// relays them to the person who asked for the change (not a developer), so each one says what to tell the user
|
|
4
|
+
// and what to do next.
|
|
5
|
+
|
|
6
|
+
const HEADER =
|
|
7
|
+
"【需求开发流水线通知】这条消息来自后台运行的开发流水线,不是用户发的。请把要点转告用户:讲需求和结果,不讲代码实现,除非用户问起。";
|
|
8
|
+
|
|
9
|
+
export function blockedMessage(input: {
|
|
10
|
+
featureDir: string;
|
|
11
|
+
questions: string[];
|
|
12
|
+
resumeCommand: string;
|
|
13
|
+
/** A reminder is scheduled after this many hours without an answer. */
|
|
14
|
+
reminderHours?: number | null;
|
|
15
|
+
}): string {
|
|
16
|
+
const list = input.questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
|
|
17
|
+
return [
|
|
18
|
+
HEADER,
|
|
19
|
+
"",
|
|
20
|
+
`功能目录:${input.featureDir}`,
|
|
21
|
+
"情况:方案审核发现有问题必须由需求方决定,否则可能做出错误的东西。流水线已暂停,还没有开始写代码。",
|
|
22
|
+
"",
|
|
23
|
+
"需要用户决定的问题:",
|
|
24
|
+
list,
|
|
25
|
+
"",
|
|
26
|
+
"请这样处理:",
|
|
27
|
+
"1. 按需求对齐时的方式,逐个向用户提问(给出选项和你的推荐)。",
|
|
28
|
+
"2. 把答案记入功能目录下的 alignment.md,并更新 spec.md(顶部\"版本\"加 1,在 changes.md 中记录这次变更)。",
|
|
29
|
+
"3. 把更新后的要点讲给用户听,请用户再次确认 spec(保持顶部为 \"- 状态:已确认\",并更新日期)。",
|
|
30
|
+
"4. 用户确认后执行下面的命令,流水线会从方案设计开始,在已有方案的基础上处理这些回答:",
|
|
31
|
+
"",
|
|
32
|
+
input.resumeCommand,
|
|
33
|
+
...(input.reminderHours ? ["", `(如果 ${input.reminderHours} 小时内还没有回答,会在这个对话里提醒一次。)`] : []),
|
|
34
|
+
].join("\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function permissionMessage(input: {
|
|
38
|
+
stepTitle: string;
|
|
39
|
+
agentTitle: string;
|
|
40
|
+
requests: string[];
|
|
41
|
+
}): string {
|
|
42
|
+
return [
|
|
43
|
+
HEADER,
|
|
44
|
+
"",
|
|
45
|
+
`流水线的「${input.stepTitle}」步骤在等待权限批准,批准前会一直停在这里:`,
|
|
46
|
+
...input.requests.map((r) => `- ${r}`),
|
|
47
|
+
"",
|
|
48
|
+
`请告诉用户:在 Paseo 中打开名为「${input.agentTitle}」的 agent,查看并批准这个请求(或拒绝)。`,
|
|
49
|
+
"如果这类等待经常出现,说明流水线配置中的 agent 模式还会询问权限,需要把配置中的 agent 改成免确认模式(运行 /req-doctor 可以查看当前配置)。",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function failedMessage(input: {
|
|
54
|
+
stepTitle: string;
|
|
55
|
+
reason: string;
|
|
56
|
+
logPath: string;
|
|
57
|
+
resumeCommand: string;
|
|
58
|
+
/** An automatic continuation is scheduled at this time (text). */
|
|
59
|
+
autoResumeAt?: string | null;
|
|
60
|
+
}): string {
|
|
61
|
+
return [
|
|
62
|
+
HEADER,
|
|
63
|
+
"",
|
|
64
|
+
`流水线在「${input.stepTitle}」步骤停止了。`,
|
|
65
|
+
`原因:${input.reason}`,
|
|
66
|
+
"",
|
|
67
|
+
"已完成的步骤会保留。请告诉用户开发暂时中断,并说明原因。用户可以先看日志判断原因;涉及环境、配置或代码的问题请开发同事处理。",
|
|
68
|
+
`日志:${input.logPath}`,
|
|
69
|
+
...(input.autoResumeAt
|
|
70
|
+
? [
|
|
71
|
+
`这是临时错误(例如限流或网络中断),已预约在 ${input.autoResumeAt} 自动继续(Paseo 定时任务,到时会在这个对话里执行继续命令)。请告诉用户这一点;用户不想自动继续时,执行暂停命令即可取消。`,
|
|
72
|
+
"也可以现在就执行下面的命令继续:",
|
|
73
|
+
]
|
|
74
|
+
: ["问题解决后,执行下面的命令可以从中断的步骤继续:"]),
|
|
75
|
+
"",
|
|
76
|
+
input.resumeCommand,
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const OUTCOME_TEXT: Record<string, string> = {
|
|
81
|
+
completed: "已完成,可以验收",
|
|
82
|
+
completed_with_issues: "已完成,但有遗留问题",
|
|
83
|
+
not_completed: "未完成",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export function completedMessage(input: {
|
|
87
|
+
featureDir: string;
|
|
88
|
+
outcome: string;
|
|
89
|
+
summary: string;
|
|
90
|
+
reportPath: string;
|
|
91
|
+
/** Set for a develop-only run: people test by hand from this handoff note. */
|
|
92
|
+
handoffPath?: string | null;
|
|
93
|
+
}): string {
|
|
94
|
+
if (input.handoffPath) {
|
|
95
|
+
return [
|
|
96
|
+
HEADER,
|
|
97
|
+
"",
|
|
98
|
+
`需求的研发已经完成(执行方式:只做研发,流水线没有做测试验证)。研发结论:${OUTCOME_TEXT[input.outcome] ?? input.outcome}`,
|
|
99
|
+
"",
|
|
100
|
+
"摘要:",
|
|
101
|
+
input.summary,
|
|
102
|
+
"",
|
|
103
|
+
`研发报告:${input.reportPath}`,
|
|
104
|
+
`提测说明:${input.handoffPath}`,
|
|
105
|
+
"",
|
|
106
|
+
"请把结论和摘要转告用户,说明接下来需要人工测试:按 /req 的验收手册,以提测说明中的“建议重点测试”和验收标准为线索,带用户逐条测试。",
|
|
107
|
+
"如果之后想让流水线做测试验证,在用户同意后执行继续命令并加上 --phase verify。",
|
|
108
|
+
].join("\n");
|
|
109
|
+
}
|
|
110
|
+
return [
|
|
111
|
+
HEADER,
|
|
112
|
+
"",
|
|
113
|
+
`需求开发流水线已经结束。结论:${OUTCOME_TEXT[input.outcome] ?? input.outcome}`,
|
|
114
|
+
"",
|
|
115
|
+
"摘要:",
|
|
116
|
+
input.summary,
|
|
117
|
+
"",
|
|
118
|
+
`完整报告:${input.reportPath}`,
|
|
119
|
+
"",
|
|
120
|
+
"请把结论和摘要转告用户,并读一下报告中「需要你做的事」一节,提醒用户接下来要做什么。用户有疑问时,以报告和功能目录中的文件为准来回答。",
|
|
121
|
+
].join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function resultFollowUp(input: { resultFile: string; problem: string }): string {
|
|
125
|
+
return [
|
|
126
|
+
`流水线没有拿到本步骤的有效结果:${input.problem}。`,
|
|
127
|
+
`如果本步骤的工作还没有完成,请先完成它;然后按照"本步骤任务"中"结果文件"一节的要求,把结果写入 ${input.resultFile}(只含一个 JSON 对象),再结束。`,
|
|
128
|
+
].join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function milestoneMessage(text: string): string {
|
|
132
|
+
return [
|
|
133
|
+
"【需求开发流水线通知 · 进展】这条消息来自后台流水线,不是用户发的。仅告知进展,用一句话转告用户即可,不需要其他操作。",
|
|
134
|
+
"",
|
|
135
|
+
text,
|
|
136
|
+
].join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function specReviewMessage(input: { featureDir: string; verdict: string; issues: string[]; reviewPath: string }): string {
|
|
140
|
+
if (input.verdict === "ready") {
|
|
141
|
+
return [
|
|
142
|
+
HEADER,
|
|
143
|
+
"",
|
|
144
|
+
`功能目录:${input.featureDir}`,
|
|
145
|
+
`需求说明的独立审阅已完成:没有发现需要澄清的问题(详见 ${input.reviewPath})。`,
|
|
146
|
+
"请告诉用户这一点,然后继续请用户确认需求说明。",
|
|
147
|
+
].join("\n");
|
|
148
|
+
}
|
|
149
|
+
return [
|
|
150
|
+
HEADER,
|
|
151
|
+
"",
|
|
152
|
+
`功能目录:${input.featureDir}`,
|
|
153
|
+
`需求说明的独立审阅已完成:另一个模型发现 ${input.issues.length} 个建议在确认前澄清的问题,详见 ${input.reviewPath}(其中每个问题都写成了带选项和推荐的选择题):`,
|
|
154
|
+
"",
|
|
155
|
+
...input.issues.map((q, i) => `${i + 1}. ${q}`),
|
|
156
|
+
"",
|
|
157
|
+
"请这样处理:",
|
|
158
|
+
"1. 先读审阅文件,自己核对一遍:不成立的问题直接略过,并在最后告诉用户略过了哪些、为什么。",
|
|
159
|
+
"2. 成立的问题,按需求对齐时的方式一次一个地问用户(用审阅文件中的选项和推荐)。",
|
|
160
|
+
"3. 把答案记入 alignment.md,更新 spec.md,再请用户确认需求说明。",
|
|
161
|
+
].join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function specReviewFailedMessage(input: { featureDir: string; reason: string }): string {
|
|
165
|
+
return [
|
|
166
|
+
HEADER,
|
|
167
|
+
"",
|
|
168
|
+
`功能目录:${input.featureDir}`,
|
|
169
|
+
`需求说明的独立审阅没有完成:${input.reason}`,
|
|
170
|
+
"这不影响继续:请告诉用户审阅没能完成,然后照常请用户确认需求说明。",
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Sent by a Paseo schedule into the chat when a decision has been waited for a long time. */
|
|
175
|
+
export function waitingReminderMessage(input: { featureDir: string; hours: number; questions: string[]; resumeCommand: string }): string {
|
|
176
|
+
return [
|
|
177
|
+
"【需求开发流水线通知】这是一条预约的提醒,不是用户发的。",
|
|
178
|
+
"",
|
|
179
|
+
`功能目录:${input.featureDir}`,
|
|
180
|
+
`这个需求在等需求方回答方案审核提出的问题,已经超过 ${input.hours} 小时,开发一直暂停着:`,
|
|
181
|
+
...input.questions.map((q, i) => `${i + 1}. ${q}`),
|
|
182
|
+
"",
|
|
183
|
+
"请先执行查看进度的命令确认:仍然是\"等待需求方决定\"时,在用户下次说话时提醒他这几个问题,并按之前的方式逐个询问;用户已经回答、开发已经继续或停止时,忽略这条提醒,不要打扰用户。",
|
|
184
|
+
`回答完、更新需求说明后,继续的命令是:${input.resumeCommand}`,
|
|
185
|
+
].join("\n");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Sent by a Paseo schedule into the chat to continue after a transient error. */
|
|
189
|
+
export function autoResumePrompt(input: { featureDir: string; reason: string; statusCommand: string; resumeCommand: string }): string {
|
|
190
|
+
return [
|
|
191
|
+
"【需求开发流水线通知】这是一条预约的自动继续,不是用户发的。",
|
|
192
|
+
"",
|
|
193
|
+
`功能目录:${input.featureDir}`,
|
|
194
|
+
`开发流水线之前因为临时错误中断:${input.reason}`,
|
|
195
|
+
"",
|
|
196
|
+
"请这样处理:",
|
|
197
|
+
`1. 先执行查看进度的命令:${input.statusCommand}`,
|
|
198
|
+
"2. 仍然是\"已中断,可以继续\"时,执行下面的继续命令,然后用一句话告诉用户已经自动继续:",
|
|
199
|
+
` ${input.resumeCommand}`,
|
|
200
|
+
"3. 其他情况(已经在运行、已完成、用户已经手动处理过):什么也不做,也不要回复用户。",
|
|
201
|
+
].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Sent by a Paseo schedule into the chat while a pipeline runs (watchdogMinutes). */
|
|
205
|
+
export function watchdogPrompt(input: { featureDir: string; statusCommand: string; resumeCommand: string }): string {
|
|
206
|
+
return [
|
|
207
|
+
"【需求开发流水线通知】这是一条定时检查,不是用户发的。",
|
|
208
|
+
"",
|
|
209
|
+
`请执行查看进度的命令:${input.statusCommand}`,
|
|
210
|
+
`- 输出中的状态是"已中断(后台进程意外退出,可以继续)":执行继续命令 ${input.resumeCommand},然后用一句话告诉用户开发在意外中断后已经自动继续。`,
|
|
211
|
+
"- 其他任何情况:什么也不做,不要回复用户,不要打扰用户。",
|
|
212
|
+
`(功能目录:${input.featureDir}。这个检查在流水线结束后会自动取消。)`,
|
|
213
|
+
].join("\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function queuedMessage(input: { featureDir: string; max: number; holders: Array<{ featureDir: string; repo: string }> }): string {
|
|
217
|
+
return [
|
|
218
|
+
HEADER,
|
|
219
|
+
"",
|
|
220
|
+
`功能目录:${input.featureDir}`,
|
|
221
|
+
`这台机器同时最多运行 ${input.max} 条开发流水线,现在已经满了,这个需求在排队,轮到时会自动开始并通知这个对话:`,
|
|
222
|
+
...input.holders.map((h) => `- 正在运行:${h.featureDir}(${h.repo})`),
|
|
223
|
+
"",
|
|
224
|
+
"请把这一点告诉用户。不想等的话,可以执行暂停命令取消排队。",
|
|
225
|
+
].join("\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function dequeuedMessage(input: { featureDir: string; waitedMinutes: number }): string {
|
|
229
|
+
return [HEADER, "", `功能目录:${input.featureDir}`, `排队结束(等了约 ${Math.max(1, Math.round(input.waitedMinutes))} 分钟),开发已经开始。请用一句话告诉用户。`].join("\n");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function scheduledStartPrompt(input: { featureDir: string; command: string }): string {
|
|
233
|
+
return [
|
|
234
|
+
"【需求开发流水线通知】这是用户之前预约的开发开始时间,不是用户现在发的消息。",
|
|
235
|
+
"",
|
|
236
|
+
`功能目录:${input.featureDir}`,
|
|
237
|
+
"请执行下面的启动命令,然后用一句话告诉用户预约的开发已经开始(输出\"未启动\"时,按 /req 中的说明处理并告诉用户原因):",
|
|
238
|
+
"",
|
|
239
|
+
input.command,
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ENVIRONMENT_HINT = {
|
|
244
|
+
local: "测试验证会在本地启动服务进行。",
|
|
245
|
+
deployed: "测试验证会在已部署的测试环境中进行:开始前要先把版本部署到测试环境,并在功能目录写好 test-env.md(地址、账号从哪里取、可以用的测试数据;不写口令本身)。",
|
|
246
|
+
} as const;
|
|
247
|
+
|
|
248
|
+
/** The develop phase is done and the run stopped at the gate (phases.mode "gated"). */
|
|
249
|
+
export function phaseDoneMessage(input: {
|
|
250
|
+
featureDir: string;
|
|
251
|
+
handoffPath: string;
|
|
252
|
+
verifyCommand: string;
|
|
253
|
+
environment: "local" | "deployed";
|
|
254
|
+
/** The verify phase is scheduled at this time (text). */
|
|
255
|
+
verifyAt?: string | null;
|
|
256
|
+
/** A reminder is scheduled after this many hours. */
|
|
257
|
+
reminderHours?: number | null;
|
|
258
|
+
/** How many times verification has handed defects back so far. */
|
|
259
|
+
handbacks?: number;
|
|
260
|
+
}): string {
|
|
261
|
+
return [
|
|
262
|
+
HEADER,
|
|
263
|
+
"",
|
|
264
|
+
`功能目录:${input.featureDir}`,
|
|
265
|
+
`研发阶段已经完成${input.handbacks ? `(这是测试退回后第 ${input.handbacks} 次修复完成)` : ""}:代码已写完并通过代码评审,提测说明在 ${input.handoffPath}。按这个需求的执行方式(分阶段),流水线在这里停下,等待测试验证。`,
|
|
266
|
+
"",
|
|
267
|
+
"请这样处理:",
|
|
268
|
+
"1. 告诉用户研发已完成、可以提测,用两三句话讲提测说明里\"这次改了什么\"和\"建议重点测试\"。",
|
|
269
|
+
`2. ${ENVIRONMENT_HINT[input.environment]}`,
|
|
270
|
+
input.verifyAt
|
|
271
|
+
? `3. 已预约在 ${input.verifyAt} 自动开始测试验证(Paseo 定时任务)。用户想提前开始或改用别的方式时,按 /req 的"开始测试验证"手册处理;想取消预约,执行暂停命令。`
|
|
272
|
+
: "3. 用户(或测试同事)准备好之后,按 /req 的\"开始测试验证\"手册处理;确认后执行下面的命令:",
|
|
273
|
+
"",
|
|
274
|
+
input.verifyCommand,
|
|
275
|
+
...(input.reminderHours && !input.verifyAt ? ["", `(如果 ${input.reminderHours} 小时内还没有开始测试验证,会在这个对话里提醒一次。)`] : []),
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Sent by a Paseo schedule when a requirement has waited at the gate for a long time. */
|
|
280
|
+
export function phaseWaitingReminderMessage(input: { featureDir: string; hours: number; handoffPath: string; verifyCommand: string }): string {
|
|
281
|
+
return [
|
|
282
|
+
"【需求开发流水线通知】这是一条预约的提醒,不是用户发的。",
|
|
283
|
+
"",
|
|
284
|
+
`功能目录:${input.featureDir}`,
|
|
285
|
+
`这个需求的研发已经完成超过 ${input.hours} 小时,一直在等待测试验证(提测说明:${input.handoffPath})。`,
|
|
286
|
+
"",
|
|
287
|
+
"请先执行查看进度的命令确认:仍然是\"研发完成,待测试验证\"时,在用户下次说话时提醒他可以提测了;已经开始测试验证、已完成或已停止时,忽略这条提醒,不要打扰用户。",
|
|
288
|
+
`开始测试验证的命令是:${input.verifyCommand}`,
|
|
289
|
+
].join("\n");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Sent by a Paseo schedule at the time given with --verify-at. */
|
|
293
|
+
export function verifyStartPrompt(input: { featureDir: string; statusCommand: string; verifyCommand: string; environment: "local" | "deployed" }): string {
|
|
294
|
+
return [
|
|
295
|
+
"【需求开发流水线通知】这是用户之前预约的测试验证开始时间,不是用户现在发的消息。",
|
|
296
|
+
"",
|
|
297
|
+
`功能目录:${input.featureDir}`,
|
|
298
|
+
"请这样处理:",
|
|
299
|
+
`1. 先执行查看进度的命令:${input.statusCommand}`,
|
|
300
|
+
"2. 状态是\"研发完成,待测试验证\"时:" +
|
|
301
|
+
(input.environment === "deployed"
|
|
302
|
+
? "检查功能目录里有 test-env.md;有就执行下面的命令,没有就不要启动,等用户下次说话时告诉他需要先写好测试环境说明。"
|
|
303
|
+
: "执行下面的命令。") +
|
|
304
|
+
"然后用一句话告诉用户预约的测试验证已经开始(输出\"未启动\"时告诉用户原因):",
|
|
305
|
+
` ${input.verifyCommand}`,
|
|
306
|
+
"3. 其他情况(已经开始、已完成、用户已经手动处理过):什么也不做,也不要回复用户。",
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|