paseo-req-kit 1.7.0-beta.3 → 1.8.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/CHANGELOG.md +94 -0
- package/README.md +15 -10
- package/bin/req.mjs +4 -0
- package/config.example.json +32 -1
- package/contracts/feature-files.md +30 -0
- package/contracts/notifier-event.md +3 -2
- package/contracts/result-file.md +8 -4
- package/contracts/spec.md +3 -1
- package/contracts/state.md +7 -2
- package/contracts/verify.md +2 -0
- package/docs/adr/0023-types-gate-slices.md +54 -0
- package/docs/adr/0024-confirm-early-review-axes.md +27 -0
- package/docs/adr/0025-joint-requirements.md +27 -0
- package/docs/adr/README.md +3 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +18 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +16 -7
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +1 -1
- 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 +105 -0
- package/engine/prompts/00-shared-context.md +4 -0
- package/engine/prompts/02-spec-review.md +1 -1
- package/engine/prompts/03-design.md +31 -8
- package/engine/prompts/04-design-review.md +3 -2
- package/engine/prompts/04b-design-revise.md +8 -1
- package/engine/prompts/05-implement.md +11 -5
- package/engine/prompts/06-code-review.md +13 -9
- package/engine/prompts/07-review-fix.md +1 -1
- package/engine/prompts/09-verify.md +1 -1
- package/engine/prompts/10-defect-fix.md +1 -1
- package/engine/prompts/11-report.md +20 -1
- package/engine/prompts/incremental.md +1 -0
- package/engine/prompts/joint.md +55 -0
- package/engine/prompts/related-repos.md +7 -0
- package/engine/prompts/review-axis-spec.md +10 -0
- package/engine/prompts/review-axis-standards.md +11 -0
- package/engine/prompts/types/assessment.md +41 -0
- package/engine/prompts/types/defect.md +55 -0
- package/engine/prompts/types/refactor.md +39 -0
- package/engine/src/cli-args.ts +14 -1
- package/engine/src/config.ts +140 -0
- package/engine/src/control.ts +27 -11
- package/engine/src/digest.ts +12 -3
- package/engine/src/doctor.ts +28 -1
- package/engine/src/eval.ts +1 -1
- package/engine/src/features.ts +2 -0
- package/engine/src/gate.ts +157 -0
- package/engine/src/joint.ts +173 -0
- package/engine/src/launch.ts +1 -1
- package/engine/src/lessons.ts +72 -0
- package/engine/src/main.ts +38 -5
- package/engine/src/messages.ts +87 -1
- package/engine/src/notifiers.ts +1 -1
- package/engine/src/notify-test.ts +4 -0
- package/engine/src/pipeline.ts +777 -63
- package/engine/src/playbooks.ts +46 -0
- package/engine/src/preflight.ts +41 -2
- package/engine/src/state.ts +58 -3
- package/engine/src/steps.ts +29 -5
- package/notifiers/im-template.mjs +3 -1
- package/package.json +2 -2
- package/skills/acceptance-criteria/SKILL.md +10 -1
- package/skills/req/SKILL.md +5 -3
- package/skills/req/playbooks/new.md +16 -5
- package/skills/req/playbooks/small.md +1 -1
- package/skills/req/playbooks/status.md +2 -0
- package/skills/req-retro/SKILL.md +2 -0
- package/skills/req-setup/SKILL.md +22 -5
- package/skills/requirement-interview/SKILL.md +3 -0
- package/skills/spec-writing/SKILL.md +18 -4
- package/skills/spec-writing/spec-template.md +4 -1
package/engine/src/eval.ts
CHANGED
|
@@ -199,7 +199,7 @@ export function evaluateCase(
|
|
|
199
199
|
const rounds = (id: string) => Math.max(0, ...history.filter((h) => h.stepId === id).map((h) => h.round ?? 0));
|
|
200
200
|
result.reviewRounds = { design: rounds("design_review"), code: rounds("code_review"), defectFix: rounds("defect_fix") };
|
|
201
201
|
result.resultFollowUps = history.reduce((sum, h) => sum + (h.followUps ?? 0), 0);
|
|
202
|
-
result.humanInterventions =
|
|
202
|
+
result.humanInterventions = ["waiting_for_business", "waiting_for_approval", "failed"].includes(state.status ?? "") ? 1 : 0;
|
|
203
203
|
} else if (!result.error) {
|
|
204
204
|
result.error = "流水线没有留下状态文件,见 .pipeline/eval-run.log";
|
|
205
205
|
}
|
package/engine/src/features.ts
CHANGED
|
@@ -89,6 +89,8 @@ function stageOf(confirmed: boolean, state: RawState | null, acceptance: string
|
|
|
89
89
|
return state.pid && isAlive(state.pid) ? "开发中" : "已中断(可继续)";
|
|
90
90
|
case "waiting_for_business":
|
|
91
91
|
return "等待需求方决定";
|
|
92
|
+
case "waiting_for_approval":
|
|
93
|
+
return "等待确认方案";
|
|
92
94
|
case "phase_done":
|
|
93
95
|
return "研发完成,待测试验证";
|
|
94
96
|
case "failed":
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The quality gate: project checks (lint, type check, tests, build) that the engine runs
|
|
2
|
+
// itself after the steps that change code, instead of trusting the agent's own report.
|
|
3
|
+
// Only checks that passed before the change and fail after it block; see ADR 0023.
|
|
4
|
+
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { CheckDef } from "./config.js";
|
|
9
|
+
import type { CheckOutcome } from "./state.js";
|
|
10
|
+
|
|
11
|
+
function git(repo: string, args: string[]): string | null {
|
|
12
|
+
try {
|
|
13
|
+
return execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function headCommit(repo: string): string | null {
|
|
20
|
+
return git(repo, ["rev-parse", "HEAD"])?.trim() || null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Files changed since `base`: committed, staged, unstaged and untracked (repository-relative,
|
|
25
|
+
* "/" separators). Null when it cannot be told (not a git repository, unknown base).
|
|
26
|
+
*/
|
|
27
|
+
export function changedFiles(repo: string, base: string | null): string[] | null {
|
|
28
|
+
if (!base) return null;
|
|
29
|
+
const diff = git(repo, ["diff", "--name-only", base]);
|
|
30
|
+
if (diff === null) return null;
|
|
31
|
+
const untracked = git(repo, ["ls-files", "--others", "--exclude-standard"]) ?? "";
|
|
32
|
+
const files = new Set([...diff.split("\n"), ...untracked.split("\n")].map((f) => f.trim()).filter(Boolean));
|
|
33
|
+
return [...files].sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Which of `files` changed since `commit` (committed or not); null when the commit is not
|
|
38
|
+
* in this repository. Used to tell whether acceptance tests committed first were changed later.
|
|
39
|
+
*/
|
|
40
|
+
export function changedSince(repo: string, commit: string, files: string[]): string[] | null {
|
|
41
|
+
if (git(repo, ["rev-parse", "--verify", "--quiet", `${commit}^{commit}`]) === null) return null;
|
|
42
|
+
const out = git(repo, ["diff", "--name-only", commit, "--", ...files]);
|
|
43
|
+
if (out === null) return null;
|
|
44
|
+
return out.split("\n").map((f) => f.trim()).filter(Boolean).sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function globToRegExp(pattern: string): RegExp {
|
|
48
|
+
let re = "";
|
|
49
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
50
|
+
const c = pattern[i];
|
|
51
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
52
|
+
re += ".*";
|
|
53
|
+
i += 1;
|
|
54
|
+
if (pattern[i + 1] === "/") i += 1;
|
|
55
|
+
} else if (c === "*") re += "[^/]*";
|
|
56
|
+
else if (c === "?") re += "[^/]";
|
|
57
|
+
else re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
58
|
+
}
|
|
59
|
+
return new RegExp(`^${re}$`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether a repository-relative file falls under a path: "web/" (a directory), "src/a.ts", or a glob with *. */
|
|
63
|
+
export function matchesPath(file: string, pattern: string): boolean {
|
|
64
|
+
const p = pattern.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
65
|
+
if (p === "" || p === "." || p === "/") return true;
|
|
66
|
+
if (p.includes("*") || p.includes("?")) return globToRegExp(p).test(file);
|
|
67
|
+
if (p.endsWith("/")) return file.startsWith(p);
|
|
68
|
+
return file === p || file.startsWith(`${p}/`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The paths a check watches; empty: the whole repository. */
|
|
72
|
+
export function checkPaths(check: CheckDef): string[] {
|
|
73
|
+
if (check.paths && check.paths.length > 0) return check.paths;
|
|
74
|
+
if (check.cwd && check.cwd !== ".") return [`${check.cwd.replace(/\\/g, "/").replace(/\/$/, "")}/`];
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Whether a check applies to a change; with the changed files unknown, every check applies. */
|
|
79
|
+
export function checkApplies(check: CheckDef, files: string[] | null): boolean {
|
|
80
|
+
const paths = checkPaths(check);
|
|
81
|
+
if (paths.length === 0 || files === null) return true;
|
|
82
|
+
return files.some((f) => paths.some((p) => matchesPath(f, p)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface CheckRun {
|
|
86
|
+
outcome: CheckOutcome;
|
|
87
|
+
/** Exit code, or null when it was stopped (timeout) or could not start. */
|
|
88
|
+
exitCode: number | null;
|
|
89
|
+
timedOut: boolean;
|
|
90
|
+
/** Combined output, trimmed to the end. */
|
|
91
|
+
output: string;
|
|
92
|
+
seconds: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const MAX_OUTPUT = 200_000;
|
|
96
|
+
|
|
97
|
+
/** Runs one check through the system shell, in the repository (or its cwd). Never throws. */
|
|
98
|
+
export function runCheck(check: CheckDef, repo: string, defaultTimeoutMinutes: number): Promise<CheckRun> {
|
|
99
|
+
const started = Date.now();
|
|
100
|
+
const cwd = path.join(repo, check.cwd ?? ".");
|
|
101
|
+
const timeoutMs = (check.timeoutMinutes ?? defaultTimeoutMinutes) * 60_000;
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
let output = "";
|
|
104
|
+
const append = (chunk: Buffer) => {
|
|
105
|
+
output += chunk.toString("utf8");
|
|
106
|
+
if (output.length > MAX_OUTPUT * 2) output = output.slice(-MAX_OUTPUT);
|
|
107
|
+
};
|
|
108
|
+
const finish = (exitCode: number | null, timedOut: boolean, extra = "") => {
|
|
109
|
+
const text = `${output}${extra}`.slice(-MAX_OUTPUT);
|
|
110
|
+
resolve({ outcome: exitCode === 0 && !timedOut ? "pass" : "fail", exitCode, timedOut, output: text, seconds: Math.round((Date.now() - started) / 1000) });
|
|
111
|
+
};
|
|
112
|
+
if (!fs.existsSync(cwd)) {
|
|
113
|
+
finish(null, false, `检查的目录不存在:${cwd}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let child;
|
|
117
|
+
try {
|
|
118
|
+
// CI=1: test runners stay non-interactive (no watch mode, no prompts).
|
|
119
|
+
child = spawn(check.command, { cwd, shell: true, env: { ...process.env, CI: process.env.CI ?? "1" }, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32" });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
finish(null, false, `无法启动:${(error as Error).message}`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
let timedOut = false;
|
|
125
|
+
const timer = setTimeout(() => {
|
|
126
|
+
timedOut = true;
|
|
127
|
+
try {
|
|
128
|
+
// The whole process group: a test runner's own children too.
|
|
129
|
+
if (process.platform !== "win32" && child.pid) process.kill(-child.pid, "SIGKILL");
|
|
130
|
+
else child.kill("SIGKILL");
|
|
131
|
+
} catch {
|
|
132
|
+
// Already gone.
|
|
133
|
+
}
|
|
134
|
+
}, timeoutMs);
|
|
135
|
+
child.stdout?.on("data", append);
|
|
136
|
+
child.stderr?.on("data", append);
|
|
137
|
+
child.on("error", (error) => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
finish(null, false, `\n无法启动:${error.message}`);
|
|
140
|
+
});
|
|
141
|
+
child.on("close", (code) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
finish(code, timedOut, timedOut ? `\n(超过 ${check.timeoutMinutes ?? defaultTimeoutMinutes} 分钟,已停止)` : "");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The last lines of a check's output, for a message to the agent. */
|
|
149
|
+
export function tail(text: string, lines = 60): string {
|
|
150
|
+
const all = text.trimEnd().split(/\r?\n/);
|
|
151
|
+
return (all.length > lines ? ["…", ...all.slice(-lines)] : all).join("\n");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A file name for a check's log. */
|
|
155
|
+
export function logName(name: string): string {
|
|
156
|
+
return `${name.replace(/[^\p{L}\p{N}._-]+/gu, "-").replace(/^-+|-+$/g, "") || "check"}.log`;
|
|
157
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Joint requirements (ADR 0025): one requirement that also changes other repositories
|
|
2
|
+
// (e.g. the backend of a frontend project). The run in the chat's repository coordinates:
|
|
3
|
+
// it designs the whole change and the interface contract, then each other repository gets
|
|
4
|
+
// a Paseo worktree on the requirement's branch where its own implementation, code review
|
|
5
|
+
// and fixes run (in parallel with this repository's); handoff, verification and the report
|
|
6
|
+
// stay here. Everything is committed locally only.
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { type CheckDef, type RelatedRepo, resolveRelatedRepo } from "./config.js";
|
|
12
|
+
import type { AgentRuntime } from "./runtime.js";
|
|
13
|
+
import { ensurePipelineDir, hashFile, type JointRepo, type PipelineState } from "./state.js";
|
|
14
|
+
|
|
15
|
+
/** At most this many other repositories in one requirement (three in all). */
|
|
16
|
+
export const MAX_JOINT = 2;
|
|
17
|
+
|
|
18
|
+
function git(cwd: string, ...args: string[]): string {
|
|
19
|
+
return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Where a step runs: this repository (no target) or one of the joint repositories. */
|
|
23
|
+
export interface StepTarget {
|
|
24
|
+
name: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
repo: string;
|
|
27
|
+
featureDir: string;
|
|
28
|
+
workspaceId: string | null;
|
|
29
|
+
branch: string | null;
|
|
30
|
+
joint: JointRepo;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An ASCII suffix for step keys, from the repository's directory name. */
|
|
34
|
+
export function slugFor(dir: string, taken: string[]): string {
|
|
35
|
+
const base = path.basename(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "repo";
|
|
36
|
+
let slug = base;
|
|
37
|
+
for (let i = 2; taken.includes(slug); i++) slug = `${base}-${i}`;
|
|
38
|
+
return slug;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Problems with the spec's "联合开发" names for this configuration, or null. */
|
|
42
|
+
export function jointProblem(names: string[], related: RelatedRepo[], projectDir: string): string | null {
|
|
43
|
+
if (names.length === 0) return null;
|
|
44
|
+
if (names.length > MAX_JOINT) return `联合开发最多再涉及 ${MAX_JOINT} 个仓库(一共 ${MAX_JOINT + 1} 个),需求说明里写了 ${names.length} 个:${names.join("、")}`;
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const r = related.find((x) => x.name === name);
|
|
47
|
+
if (!r) {
|
|
48
|
+
return `需求说明的"联合开发"中有「${name}」,但项目配置的 relatedRepos 里没有登记这个仓库${related.length > 0 ? `(已登记:${related.map((x) => x.name).join("、")})` : ""}。请在 .requirement-pipeline.json 中登记,或改正需求说明`;
|
|
49
|
+
}
|
|
50
|
+
const dir = resolveRelatedRepo(r, projectDir);
|
|
51
|
+
if (!fs.existsSync(dir)) return `联合开发的仓库「${name}」不在这台机器上:${dir}`;
|
|
52
|
+
try {
|
|
53
|
+
git(dir, "rev-parse", "--git-dir");
|
|
54
|
+
} catch {
|
|
55
|
+
return `联合开发的仓库「${name}」不是 git 仓库:${dir}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The joint repositories this run works in: those the spec names now, in that order. */
|
|
62
|
+
export function activeJoint(state: Pick<PipelineState, "joint">, names: string[]): JointRepo[] {
|
|
63
|
+
return names.map((n) => (state.joint ?? []).find((j) => j.name === n)).filter((j): j is JointRepo => j !== undefined);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates the worktree of each named repository that does not have one yet, on the branch
|
|
68
|
+
* feature/<requirement>, cut from what that repository has checked out. Returns the entries.
|
|
69
|
+
*/
|
|
70
|
+
export async function ensureJointRepos(input: {
|
|
71
|
+
runtime: Pick<AgentRuntime, "createWorktree">;
|
|
72
|
+
state: PipelineState;
|
|
73
|
+
featureDir: string;
|
|
74
|
+
names: string[];
|
|
75
|
+
related: RelatedRepo[];
|
|
76
|
+
projectDir: string;
|
|
77
|
+
title: string;
|
|
78
|
+
log: (message: string) => void;
|
|
79
|
+
}): Promise<JointRepo[]> {
|
|
80
|
+
const { state } = input;
|
|
81
|
+
state.joint ??= [];
|
|
82
|
+
for (const name of input.names) {
|
|
83
|
+
if (state.joint.some((j) => j.name === name)) continue;
|
|
84
|
+
const r = input.related.find((x) => x.name === name);
|
|
85
|
+
if (!r) throw new Error(`联合开发的仓库「${name}」没有在 relatedRepos 中登记`);
|
|
86
|
+
const sourceRepo = fs.realpathSync(resolveRelatedRepo(r, input.projectDir));
|
|
87
|
+
const branch = `feature/${path.basename(input.featureDir)}`;
|
|
88
|
+
let exists = true;
|
|
89
|
+
try {
|
|
90
|
+
git(sourceRepo, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`);
|
|
91
|
+
} catch {
|
|
92
|
+
exists = false;
|
|
93
|
+
}
|
|
94
|
+
if (exists) {
|
|
95
|
+
throw new Error(`联合开发的仓库「${name}」(${sourceRepo})里已经有分支 ${branch}。可能是之前开始过这个需求:请在那个仓库确认后删除或改名这个分支,再继续`);
|
|
96
|
+
}
|
|
97
|
+
const head = git(sourceRepo, "rev-parse", "--abbrev-ref", "HEAD");
|
|
98
|
+
const baseRef = head === "HEAD" ? git(sourceRepo, "rev-parse", "HEAD") : head;
|
|
99
|
+
const created = await input.runtime.createWorktree({
|
|
100
|
+
sourceRepo,
|
|
101
|
+
baseRef,
|
|
102
|
+
branchName: branch,
|
|
103
|
+
title: `${input.title}(${name})`,
|
|
104
|
+
callerAgentId: state.originAgentId,
|
|
105
|
+
});
|
|
106
|
+
const repo = fs.realpathSync(created.directory);
|
|
107
|
+
ensurePipelineDir(repo, input.featureDir);
|
|
108
|
+
const entry: JointRepo = {
|
|
109
|
+
name,
|
|
110
|
+
slug: slugFor(sourceRepo, state.joint.map((j) => j.slug)),
|
|
111
|
+
sourceRepo,
|
|
112
|
+
repo,
|
|
113
|
+
workspaceId: created.workspaceId,
|
|
114
|
+
branch,
|
|
115
|
+
specHash: null,
|
|
116
|
+
};
|
|
117
|
+
state.joint.push(entry);
|
|
118
|
+
input.log(`联合开发:为「${name}」新建了工作区 ${repo}(分支 ${branch},从 ${baseRef} 切出)`);
|
|
119
|
+
}
|
|
120
|
+
return activeJoint(state, input.names);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Copies the requirement's documents (spec, design, contract, preview) from this repository's
|
|
125
|
+
* feature directory into each joint repository's, and commits them there when they changed,
|
|
126
|
+
* so whoever reviews and merges that repository reads them next to its code.
|
|
127
|
+
*/
|
|
128
|
+
export function syncJointDocs(entries: JointRepo[], mainRepo: string, featureDir: string, files: string[], title: string, log: (message: string) => void): void {
|
|
129
|
+
const from = path.join(mainRepo, featureDir);
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const to = path.join(entry.repo, featureDir);
|
|
132
|
+
fs.mkdirSync(to, { recursive: true });
|
|
133
|
+
ensurePipelineDir(entry.repo, featureDir);
|
|
134
|
+
const copied: string[] = [];
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
const src = path.join(from, file);
|
|
137
|
+
if (!fs.existsSync(src)) continue;
|
|
138
|
+
fs.cpSync(src, path.join(to, file), { recursive: true });
|
|
139
|
+
copied.push(path.join(featureDir, file));
|
|
140
|
+
}
|
|
141
|
+
const spec = path.join(from, "spec.md");
|
|
142
|
+
if (fs.existsSync(spec)) entry.specHash = hashFile(spec);
|
|
143
|
+
if (copied.length === 0) continue;
|
|
144
|
+
try {
|
|
145
|
+
git(entry.repo, "add", "--", ...copied);
|
|
146
|
+
if (git(entry.repo, "diff", "--cached", "--name-only", "--", ...copied)) {
|
|
147
|
+
git(entry.repo, "commit", "-q", "-m", `docs(req): ${title} 的需求说明和方案(同步自主仓库)`, "--", ...copied);
|
|
148
|
+
log(`联合开发:已把 ${files.filter((f) => fs.existsSync(path.join(from, f))).join("、")} 同步到「${entry.name}」并提交`);
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const stderr = (error as { stderr?: Buffer | string }).stderr?.toString().trim();
|
|
152
|
+
log(`联合开发:同步到「${entry.name}」后提交失败(${stderr || (error as Error).message}),文件已复制,步骤照常可以读到`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The quality-gate checks a joint repository defines in its own project configuration. */
|
|
158
|
+
export function loadProjectChecks(repo: string, log: (message: string) => void): CheckDef[] {
|
|
159
|
+
const file = path.join(repo, ".requirement-pipeline.json");
|
|
160
|
+
if (!fs.existsSync(file)) return [];
|
|
161
|
+
try {
|
|
162
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8")) as { checks?: unknown };
|
|
163
|
+
if (!Array.isArray(raw.checks)) return [];
|
|
164
|
+
const checks = raw.checks.filter(
|
|
165
|
+
(c): c is CheckDef => typeof c === "object" && c !== null && typeof (c as CheckDef).name === "string" && typeof (c as CheckDef).command === "string",
|
|
166
|
+
);
|
|
167
|
+
if (checks.length !== raw.checks.length) log(`${file} 中有写法不对的检查,已跳过`);
|
|
168
|
+
return checks;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
log(`读不了 ${file}:${(error as Error).message},这个仓库不运行质量门`);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
package/engine/src/launch.ts
CHANGED
|
@@ -380,7 +380,7 @@ console.log(
|
|
|
380
380
|
[
|
|
381
381
|
checked.entersVerify
|
|
382
382
|
? `已在后台开始测试验证阶段(${PROFILE_LABELS[profile]}档),进程 ${child.pid}。提测说明:${checked.featureDir}/handoff.md`
|
|
383
|
-
: `已在后台启动(${modeText[checked.mode.kind]},${PROFILE_LABELS[profile]}档${phaseMode === "continuous" ? "" : `,执行方式:${PHASE_MODE_LABELS[phaseMode]}`}),进程 ${child.pid}。`,
|
|
383
|
+
: `已在后台启动(${args.approve && checked.mode.kind === "resume-reuse" ? "已确认方案,开始写代码" : modeText[checked.mode.kind]},${PROFILE_LABELS[profile]}档${phaseMode === "continuous" ? "" : `,执行方式:${PHASE_MODE_LABELS[phaseMode]}`}),进程 ${child.pid}。`,
|
|
384
384
|
!checked.entersVerify && phaseMode === "gated"
|
|
385
385
|
? `研发完成(写好提测说明)后会停下来通知这个对话,等待测试验证${args.verifyAt ? `;已安排在 ${describeTime(new Date(args.verifyAt))} 之后自动开始测试验证` : ""}。`
|
|
386
386
|
: !checked.entersVerify && phaseMode === "develop-only"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Recurring review findings: the report step writes lessons.tsv (problems a project-level
|
|
2
|
+
// rule would prevent); the overview and /req-retro look for a category that keeps coming
|
|
3
|
+
// back across requirements, the cue to turn it into a lint rule, a type, a test or a note.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import type { FeatureSummary } from "./features.js";
|
|
8
|
+
import { PROJECT_DIR } from "./prompts.js";
|
|
9
|
+
|
|
10
|
+
export interface Lesson {
|
|
11
|
+
category: string;
|
|
12
|
+
finding: string;
|
|
13
|
+
source: string;
|
|
14
|
+
rule: string;
|
|
15
|
+
layer: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RecurringLesson {
|
|
19
|
+
category: string;
|
|
20
|
+
/** Requirements (directory names) the category appeared in. */
|
|
21
|
+
features: string[];
|
|
22
|
+
/** Suggested rules, as written, without repeats. */
|
|
23
|
+
rules: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Rows of a lessons.tsv; an unreadable or missing file gives none. */
|
|
27
|
+
export function readLessons(file: string): Lesson[] {
|
|
28
|
+
let text: string;
|
|
29
|
+
try {
|
|
30
|
+
text = fs.readFileSync(file, "utf8").replace(/^/, "");
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const [header, ...rows] = text.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
35
|
+
if (!header) return [];
|
|
36
|
+
const columns = header.split("\t").map((c) => c.trim());
|
|
37
|
+
const at = (name: string) => columns.indexOf(name);
|
|
38
|
+
return rows
|
|
39
|
+
.map((row) => row.split("\t"))
|
|
40
|
+
.map((cells) => {
|
|
41
|
+
const get = (name: string) => (at(name) >= 0 ? (cells[at(name)] ?? "").trim() : "");
|
|
42
|
+
return { category: get("category"), finding: get("finding"), source: get("source"), rule: get("rule"), layer: get("layer") };
|
|
43
|
+
})
|
|
44
|
+
.filter((l) => l.category !== "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Categories already turned into rules (or set aside), one per line in .requirement-pipeline/lessons-handled.txt. */
|
|
48
|
+
export function handledCategories(repo: string): Set<string> {
|
|
49
|
+
try {
|
|
50
|
+
const text = fs.readFileSync(path.join(repo, PROJECT_DIR, "lessons-handled.txt"), "utf8");
|
|
51
|
+
return new Set(text.split(/\r?\n/).map((l) => l.replace(/#.*/, "").trim()).filter(Boolean));
|
|
52
|
+
} catch {
|
|
53
|
+
return new Set();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Categories found in at least `min` requirements, most frequent first. */
|
|
58
|
+
export function recurringLessons(features: FeatureSummary[], handled: Set<string> = new Set(), min = 2): RecurringLesson[] {
|
|
59
|
+
const byCategory = new Map<string, RecurringLesson>();
|
|
60
|
+
for (const f of features) {
|
|
61
|
+
for (const lesson of readLessons(path.join(f.worktree, f.featureDir, "lessons.tsv"))) {
|
|
62
|
+
if (handled.has(lesson.category)) continue;
|
|
63
|
+
const entry = byCategory.get(lesson.category) ?? { category: lesson.category, features: [], rules: [] };
|
|
64
|
+
if (!entry.features.includes(f.name)) entry.features.push(f.name);
|
|
65
|
+
if (lesson.rule && !entry.rules.includes(lesson.rule)) entry.rules.push(lesson.rule);
|
|
66
|
+
byCategory.set(lesson.category, entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [...byCategory.values()]
|
|
70
|
+
.filter((e) => e.features.length >= min)
|
|
71
|
+
.sort((a, b) => b.features.length - a.features.length || a.category.localeCompare(b.category));
|
|
72
|
+
}
|
package/engine/src/main.ts
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { LAUNCHER, parseLaunchArgs, resumeCommand, statusCommand, verifyCommand } from "./cli-args.js";
|
|
6
|
+
import { approveCommand, LAUNCHER, parseLaunchArgs, resumeCommand, statusCommand, verifyCommand } from "./cli-args.js";
|
|
7
7
|
import { dequeuedMessage, queuedMessage, watchdogPrompt } from "./messages.js";
|
|
8
8
|
import { slotHolders, slotsDir, tryAcquireSlot } from "./queue.js";
|
|
9
9
|
import { cronEvery, parseStartTime } from "./schedule-time.js";
|
|
10
|
-
import { loadConfig, PHASE_MODE_LABELS, PROFILE_LABELS } from "./config.js";
|
|
10
|
+
import { loadConfig, PHASE_MODE_LABELS, PROFILE_LABELS, REQUIREMENT_TYPE_LABELS } from "./config.js";
|
|
11
11
|
import { defaultConfigPath, kitVersion, localPromptsDir, PRINCIPLES_INDEX, PROMPTS_DIR } from "./locations.js";
|
|
12
12
|
import { satisfies } from "./daemon-check.js";
|
|
13
13
|
import { describeAgentProfile, resolveConfigProfiles, usesPaseoProfiles } from "./paseo-profiles.js";
|
|
14
|
+
import { jointProblem } from "./joint.js";
|
|
14
15
|
import { runPipeline } from "./pipeline.js";
|
|
15
16
|
import { preflight, verifyPhaseProblem } from "./preflight.js";
|
|
16
17
|
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
@@ -71,6 +72,12 @@ async function main(): Promise<number> {
|
|
|
71
72
|
args.phases ?? (reuse ? previousState?.phases?.mode : null) ?? checked.specPhases ?? previousState?.phases?.mode ?? config.phases.mode;
|
|
72
73
|
// --verify-at only makes sense with a gate; asking for it implies one.
|
|
73
74
|
if (args.verifyAt && phaseMode === "continuous" && !args.phases) phaseMode = "gated";
|
|
75
|
+
// A playbook type the configuration turned off runs the normal flow.
|
|
76
|
+
const type = checked.specType && config.requirementTypes.includes(checked.specType) ? checked.specType : null;
|
|
77
|
+
// An assessment writes no code: there is nothing to hand over for testing.
|
|
78
|
+
if (type === "assessment") phaseMode = "continuous";
|
|
79
|
+
// Joint requirement: other repositories this one also changes (an assessment only reads them).
|
|
80
|
+
const jointNames = type === "assessment" ? [] : checked.specJoint;
|
|
74
81
|
const phases: RunPhases = {
|
|
75
82
|
mode: phaseMode,
|
|
76
83
|
environment: args.verifyEnv ?? previousState?.phases?.environment ?? config.phases.verify.environment,
|
|
@@ -86,6 +93,9 @@ async function main(): Promise<number> {
|
|
|
86
93
|
const chatDir = args.sourceRepo ?? (mode.kind === "fresh" ? null : mode.previous.worktree?.sourceRepo) ?? repo;
|
|
87
94
|
if (verifyAtText && !verifyAt) throw new Error(`看不懂测试验证的开始时间"${verifyAtText}"`);
|
|
88
95
|
if (verifyAtText && phases.mode === "develop-only") throw new Error("只做研发时没有测试验证阶段,--verify-at 不适用");
|
|
96
|
+
if (type === "assessment" && (verifyAtText || args.phase === "verify")) throw new Error("评估类需求只做调研、不写代码,没有测试验证阶段");
|
|
97
|
+
const jointIssue = jointProblem(jointNames, config.relatedRepos, chatDir);
|
|
98
|
+
if (jointIssue) throw new Error(jointIssue);
|
|
89
99
|
if (checked.entersVerify) {
|
|
90
100
|
const problem = verifyPhaseProblem(repo, featureDir, phases.environment);
|
|
91
101
|
if (problem) throw new Error(problem);
|
|
@@ -138,8 +148,11 @@ async function main(): Promise<number> {
|
|
|
138
148
|
phase: "develop",
|
|
139
149
|
verifyAt: verifyAt?.toISOString() ?? null,
|
|
140
150
|
handbacks: 0,
|
|
151
|
+
type,
|
|
141
152
|
};
|
|
142
|
-
log(
|
|
153
|
+
log(
|
|
154
|
+
`开始新的流水线:${featureDir}(${PROFILE_LABELS[profile]}档,${type ? `类型:${REQUIREMENT_TYPE_LABELS[type]},` : ""}执行方式:${PHASE_MODE_LABELS[phases.mode]},分支 ${checked.branch ?? "无"})`,
|
|
155
|
+
);
|
|
143
156
|
} else {
|
|
144
157
|
const startOver = mode.kind !== "resume-reuse";
|
|
145
158
|
const previousImplemented = Object.keys(mode.previous.results).some((k) => k === "implement") ||
|
|
@@ -170,13 +183,30 @@ async function main(): Promise<number> {
|
|
|
170
183
|
phase: checked.entersVerify ? "verify" : startOver ? "develop" : (mode.previous.phase ?? "develop"),
|
|
171
184
|
verifyAt: checked.entersVerify ? null : (verifyAt?.toISOString() ?? (startOver ? null : (mode.previous.verifyAt ?? null))),
|
|
172
185
|
handbacks: mode.kind === "restart" ? 0 : (mode.previous.handbacks ?? 0),
|
|
186
|
+
type,
|
|
187
|
+
codeBase: startOver ? null : (mode.previous.codeBase ?? null),
|
|
188
|
+
// --approve: the requester confirmed the design that was waiting at the confirmation point.
|
|
189
|
+
approval:
|
|
190
|
+
args.approve && mode.previous.status === "waiting_for_approval"
|
|
191
|
+
? { attempt: mode.previous.attempt, status: "approved", at: now }
|
|
192
|
+
: startOver
|
|
193
|
+
? null
|
|
194
|
+
: (mode.previous.approval ?? null),
|
|
195
|
+
gate: mode.kind === "restart" ? null : (mode.previous.gate ?? null),
|
|
173
196
|
};
|
|
174
197
|
if (checked.entersVerify) {
|
|
175
198
|
// A develop-only run that ends with its development report gets a full report after verification.
|
|
176
199
|
delete state.results.report;
|
|
177
200
|
log(`开始测试验证阶段(${phases.environment === "deployed" ? "已部署的测试环境" : "本地"},缺陷处理:${phases.defects})`);
|
|
178
201
|
}
|
|
179
|
-
const why =
|
|
202
|
+
const why =
|
|
203
|
+
mode.kind === "resume-from-design"
|
|
204
|
+
? mode.reason
|
|
205
|
+
: mode.kind === "restart"
|
|
206
|
+
? "按 --restart 从头开始"
|
|
207
|
+
: args.approve
|
|
208
|
+
? "需求方已确认方案,开始写代码"
|
|
209
|
+
: "从中断处继续,已完成的步骤沿用结果";
|
|
180
210
|
log(`继续流水线:${featureDir}(第 ${state.attempt} 次尝试,${PROFILE_LABELS[profile]}档)—— ${why}`);
|
|
181
211
|
}
|
|
182
212
|
writeState(stateFile, state);
|
|
@@ -198,6 +228,7 @@ async function main(): Promise<number> {
|
|
|
198
228
|
resume: resumeCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
|
|
199
229
|
status: statusCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
|
|
200
230
|
verify: verifyCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
|
|
231
|
+
approve: approveCommand({ repo: chatRepo, featureDir, configPath: args.configPath }),
|
|
201
232
|
};
|
|
202
233
|
const notify = async (text: string) => {
|
|
203
234
|
log(`通知:\n${text}`);
|
|
@@ -285,11 +316,13 @@ async function main(): Promise<number> {
|
|
|
285
316
|
resumeCommand: commands.resume,
|
|
286
317
|
statusCommand: commands.status,
|
|
287
318
|
verifyCommand: commands.verify,
|
|
319
|
+
approveCommand: commands.approve,
|
|
320
|
+
jointNames,
|
|
288
321
|
log,
|
|
289
322
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
290
323
|
});
|
|
291
324
|
log(`流水线结束:${outcome}`);
|
|
292
|
-
return outcome === "completed" || outcome === "phase_done" ? 0 : outcome === "waiting_for_business" ? 3 : 1;
|
|
325
|
+
return outcome === "completed" || outcome === "phase_done" ? 0 : outcome === "waiting_for_business" || outcome === "waiting_for_approval" ? 3 : 1;
|
|
293
326
|
} finally {
|
|
294
327
|
// The run ended by itself: the watchdog has nothing left to watch.
|
|
295
328
|
if (watchdog) {
|
package/engine/src/messages.ts
CHANGED
|
@@ -12,13 +12,17 @@ export function blockedMessage(input: {
|
|
|
12
12
|
resumeCommand: string;
|
|
13
13
|
/** A reminder is scheduled after this many hours without an answer. */
|
|
14
14
|
reminderHours?: number | null;
|
|
15
|
+
/** "reproduce": the defect could not be reproduced (defect playbook); default: the design review. */
|
|
16
|
+
source?: "design_review" | "reproduce";
|
|
15
17
|
}): string {
|
|
16
18
|
const list = input.questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
|
|
17
19
|
return [
|
|
18
20
|
HEADER,
|
|
19
21
|
"",
|
|
20
22
|
`功能目录:${input.featureDir}`,
|
|
21
|
-
"
|
|
23
|
+
input.source === "reproduce"
|
|
24
|
+
? "情况:复现与定位这一步没能复现这个缺陷,需要需求方补充信息(复现步骤、环境、账号或数据、截图或报错)。流水线已暂停,还没有修改代码。"
|
|
25
|
+
: "情况:方案审核发现有问题必须由需求方决定,否则可能做出错误的东西。流水线已暂停,还没有开始写代码。",
|
|
22
26
|
"",
|
|
23
27
|
"需要用户决定的问题:",
|
|
24
28
|
list,
|
|
@@ -128,6 +132,29 @@ export function resultFollowUp(input: { resultFile: string; problem: string }):
|
|
|
128
132
|
].join("\n");
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
/** Sent to the step agent when checks that passed before its change now fail (the quality gate). */
|
|
136
|
+
export function gateFollowUp(input: {
|
|
137
|
+
failures: Array<{ name: string; command: string; cwd: string | null; logFile: string; tail: string }>;
|
|
138
|
+
round: number;
|
|
139
|
+
max: number;
|
|
140
|
+
/** Checks that already failed before this requirement's change: not this step's problem. */
|
|
141
|
+
preexisting: string[];
|
|
142
|
+
}): string {
|
|
143
|
+
const parts = [
|
|
144
|
+
`流水线在你这一步结束后运行了项目的检查(质量门),以下检查在改动之前通过、现在失败(第 ${input.round} 次要求修复,最多 ${input.max} 次):`,
|
|
145
|
+
"",
|
|
146
|
+
];
|
|
147
|
+
for (const f of input.failures) {
|
|
148
|
+
parts.push(`### ${f.name}`, "", `命令:\`${f.command}\`${f.cwd ? `(在 ${f.cwd} 目录)` : ""}`, `完整输出:${f.logFile}`, "", "最后的输出:", "", "```text", f.tail, "```", "");
|
|
149
|
+
}
|
|
150
|
+
if (input.preexisting.length > 0) parts.push(`(${input.preexisting.join("、")} 在改动之前就失败,不算本次引入,不需要处理。)`, "");
|
|
151
|
+
parts.push(
|
|
152
|
+
"请找到原因并修复,让这些检查重新通过;修复要针对原因,不要删除、跳过或放宽检查和测试。修完自己运行一遍这些命令确认通过,提交,并在本步骤的产物里记一笔(哪个检查、原因、怎么修的);结果文件按需要更新。",
|
|
153
|
+
"确实修不了的(例如需要环境或权限),在产物里写明原因后照常结束,流水线会把它带进评审和报告。",
|
|
154
|
+
);
|
|
155
|
+
return parts.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
131
158
|
export function milestoneMessage(text: string): string {
|
|
132
159
|
return [
|
|
133
160
|
"【需求开发流水线通知 · 进展】这条消息来自后台流水线,不是用户发的。仅告知进展,用一句话转告用户即可,不需要其他操作。",
|
|
@@ -306,3 +333,62 @@ export function verifyStartPrompt(input: { featureDir: string; statusCommand: st
|
|
|
306
333
|
"3. 其他情况(已经开始、已完成、用户已经手动处理过):什么也不做,也不要回复用户。",
|
|
307
334
|
].join("\n");
|
|
308
335
|
}
|
|
336
|
+
|
|
337
|
+
/** The design waits at the confirmation point (designApproval). */
|
|
338
|
+
export function approvalMessage(input: {
|
|
339
|
+
featureDir: string;
|
|
340
|
+
previewPath: string;
|
|
341
|
+
/** The prototype (UI changes), when the design made one. */
|
|
342
|
+
prototypePath: string | null;
|
|
343
|
+
approveCommand: string;
|
|
344
|
+
resumeCommand: string;
|
|
345
|
+
reminderHours?: number | null;
|
|
346
|
+
/** The run carries on with the design at this time (text) without an answer. */
|
|
347
|
+
autoContinueAt?: string | null;
|
|
348
|
+
}): string {
|
|
349
|
+
return [
|
|
350
|
+
`${HEADER.replace("【需求开发流水线通知】", "【需求开发流水线通知 · 请确认方案】")}`,
|
|
351
|
+
"",
|
|
352
|
+
`功能目录:${input.featureDir}`,
|
|
353
|
+
"情况:方案已经通过审核,还没有开始写代码。开发前请需求方确认\"做完之后会看到什么\",这时调整的成本最低。",
|
|
354
|
+
`- 做完后你会看到什么:${input.previewPath}`,
|
|
355
|
+
...(input.prototypePath ? [`- 界面原型:${input.prototypePath}(静态页面,可以在浏览器中打开)`] : []),
|
|
356
|
+
"",
|
|
357
|
+
"请这样处理:",
|
|
358
|
+
"1. 读 preview.md,用几句话把要点讲给用户:会看到的变化、主要的假设(尤其是影响界面、数据、权限的),有原型就告诉用户在哪里看。",
|
|
359
|
+
"2. 问用户:按这个做,还是有要改的地方?",
|
|
360
|
+
`3. 用户确认后,执行下面的命令继续开发:\n\n${input.approveCommand}`,
|
|
361
|
+
"4. 用户要改的:按需求对齐时的方式问清楚,把结论记入 alignment.md,更新 spec.md(顶部\"版本\"加 1,在 changes.md 中记一节,原因写\"方案确认时的修改\"),请用户再次确认需求说明,然后执行下面的命令,流水线会从方案设计开始按修改后的需求调整方案,再请用户确认一次:",
|
|
362
|
+
"",
|
|
363
|
+
input.resumeCommand,
|
|
364
|
+
...(input.reminderHours ? ["", `(如果 ${input.reminderHours} 小时内还没有确认,会在这个对话里提醒一次。)`] : []),
|
|
365
|
+
...(input.autoContinueAt ? ["", `(按团队设置,到 ${input.autoContinueAt} 仍没有确认时,会按这个方案继续开发。)`] : []),
|
|
366
|
+
].join("\n");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function approvalReminderMessage(input: { featureDir: string; hours: number; previewPath: string; approveCommand: string }): string {
|
|
370
|
+
return [
|
|
371
|
+
"【需求开发流水线通知】这是一条预约的提醒,不是用户发的。",
|
|
372
|
+
"",
|
|
373
|
+
`功能目录:${input.featureDir}`,
|
|
374
|
+
`这个需求的方案等待需求方确认已经超过 ${input.hours} 小时,开发一直暂停着(要确认的内容:${input.previewPath})。`,
|
|
375
|
+
"",
|
|
376
|
+
"请先执行查看进度的命令确认:仍然是\"等待需求方确认方案\"时,在用户下次说话时提醒他看一下并确认;已经确认、开发已经继续或停止时,忽略这条提醒,不要打扰用户。",
|
|
377
|
+
`用户确认后继续开发的命令是:${input.approveCommand}`,
|
|
378
|
+
].join("\n");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function approvalContinuePrompt(input: { featureDir: string; hours: number; statusCommand: string; approveCommand: string }): string {
|
|
382
|
+
return [
|
|
383
|
+
"【需求开发流水线通知】这是一条预约的自动继续,不是用户发的。",
|
|
384
|
+
"",
|
|
385
|
+
`功能目录:${input.featureDir}`,
|
|
386
|
+
`这个需求的方案等待确认已经 ${input.hours} 小时。按团队的设置,超时没有确认时按方案继续开发。`,
|
|
387
|
+
"",
|
|
388
|
+
"请这样处理:",
|
|
389
|
+
`1. 先执行查看进度的命令:${input.statusCommand}`,
|
|
390
|
+
"2. 仍然是\"等待需求方确认方案\"时,执行下面的命令,然后用一句话告诉用户:方案一直没有确认,已按方案继续开发,之后仍可以修改需求:",
|
|
391
|
+
` ${input.approveCommand}`,
|
|
392
|
+
"3. 其他情况(已经确认、需求已修改、已经停止):什么也不做,也不要回复用户。",
|
|
393
|
+
].join("\n");
|
|
394
|
+
}
|