paseo-req-kit 1.14.0-beta.4 → 1.14.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 +39 -0
- package/bin/req.mjs +6 -0
- package/contracts/notifier-event.md +1 -1
- package/contracts/spec.md +1 -0
- package/contracts/state.md +3 -1
- package/contracts/usage-record.md +16 -2
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +3 -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 +17 -1
- package/engine/prompts/00-shared-context.md +3 -2
- package/engine/src/activity.ts +21 -0
- package/engine/src/api.ts +30 -5
- package/engine/src/code-review.ts +3 -0
- package/engine/src/config-view.ts +10 -0
- package/engine/src/config.ts +27 -0
- package/engine/src/danger-guard.ts +180 -0
- package/engine/src/features.ts +1 -1
- package/engine/src/messages.ts +29 -0
- package/engine/src/outcomes.ts +145 -0
- package/engine/src/paseo-runtime.ts +9 -1
- package/engine/src/pipeline.ts +135 -6
- package/engine/src/records.ts +71 -0
- package/engine/src/reply.ts +30 -11
- package/engine/src/review.ts +15 -1
- package/engine/src/runtime.ts +2 -0
- package/engine/src/secret-scan.ts +177 -0
- package/engine/src/state.ts +30 -0
- package/engine/src/stats.ts +143 -9
- package/engine/src/watchdog.ts +38 -4
- package/package.json +1 -1
- package/paseo-plugin/client/stats.tsx +15 -0
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/shared/contracts.ts +7 -0
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.Cp7WwPWE.js → app.VQ-QxRPH.js} +1 -1
- package/site-dist/assets/changelog.md.CIKYJXpF.js +1 -0
- package/site-dist/assets/{changelog.md.Zi-_3Rjh.lean.js → changelog.md.CIKYJXpF.lean.js} +1 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.h7oQE_np.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.CMTXQK-Q.js → VPLocalSearchBox.CT4Vlj76.js} +1 -1
- package/site-dist/assets/chunks/{theme.DvVUfKr3.js → theme.B_UmHHou.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.D8XlIQOg.js → guide_getting-started.md.CZM53rO_.js} +1 -1
- package/site-dist/assets/{guide_project.md.Csi5wiyt.js → guide_project.md.DIjDRoAq.js} +1 -1
- package/site-dist/assets/{guide_project.md.Csi5wiyt.lean.js → guide_project.md.DIjDRoAq.lean.js} +1 -1
- package/site-dist/assets/{guide_user.md.B4N9ywmr.js → guide_user.md.UV0WxdJa.js} +1 -1
- package/site-dist/assets/{guide_user.md.B4N9ywmr.lean.js → guide_user.md.UV0WxdJa.lean.js} +1 -1
- package/site-dist/assets/{reference_contracts_notifier-event.md.CCfrkm_A.js → reference_contracts_notifier-event.md.B9WqeZdO.js} +1 -1
- package/site-dist/assets/{reference_contracts_notifier-event.md.CCfrkm_A.lean.js → reference_contracts_notifier-event.md.B9WqeZdO.lean.js} +1 -1
- package/site-dist/assets/{reference_contracts_spec.md.BwwS0L_j.js → reference_contracts_spec.md.Dh5XczZL.js} +2 -2
- package/site-dist/assets/{reference_contracts_spec.md.BwwS0L_j.lean.js → reference_contracts_spec.md.Dh5XczZL.lean.js} +2 -2
- package/site-dist/assets/reference_contracts_state.md.STlqkPPS.js +1 -0
- package/site-dist/assets/{reference_contracts_usage-record.md.D5krNLW7.lean.js → reference_contracts_usage-record.md.MjVps4CA.js} +2 -2
- package/site-dist/assets/{reference_contracts_usage-record.md.D5krNLW7.js → reference_contracts_usage-record.md.MjVps4CA.lean.js} +2 -2
- package/site-dist/changelog.html +5 -5
- package/site-dist/guide/getting-started.html +5 -5
- package/site-dist/guide/how-it-works.html +4 -4
- package/site-dist/guide/index.html +4 -4
- package/site-dist/guide/install.html +4 -4
- package/site-dist/guide/maintainer.html +4 -4
- package/site-dist/guide/phases.html +4 -4
- package/site-dist/guide/project.html +6 -6
- package/site-dist/guide/user.html +5 -5
- package/site-dist/hashmap.json +1 -1
- package/site-dist/index.html +4 -4
- package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
- package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
- package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
- package/site-dist/reference/adr/0004-result-files.html +4 -4
- package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
- package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
- package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
- package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
- package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
- package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
- package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
- package/site-dist/reference/adr/0012-review-panel.html +4 -4
- package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
- package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
- package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
- package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
- package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
- package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
- package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
- package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
- package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
- package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
- package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
- package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
- package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
- package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
- package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
- package/site-dist/reference/adr/0028-external-integrations.html +4 -4
- package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
- package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
- package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
- package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
- package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
- package/site-dist/reference/adr/index.html +4 -4
- package/site-dist/reference/commands.html +4 -4
- package/site-dist/reference/config.html +4 -4
- package/site-dist/reference/contracts/cli-json.html +4 -4
- package/site-dist/reference/contracts/feature-files.html +4 -4
- package/site-dist/reference/contracts/index.html +4 -4
- package/site-dist/reference/contracts/notifier-event.html +6 -6
- package/site-dist/reference/contracts/result-file.html +4 -4
- package/site-dist/reference/contracts/spec.html +6 -6
- package/site-dist/reference/contracts/state.html +5 -5
- package/site-dist/reference/contracts/usage-record.html +6 -6
- package/site-dist/reference/contracts/verify.html +4 -4
- package/site-dist/reference/scope.html +4 -4
- package/skills/req/SKILL.md +1 -0
- package/skills/req/playbooks/incident.md +1 -1
- package/skills/spec-writing/SKILL.md +2 -1
- package/site-dist/assets/changelog.md.Zi-_3Rjh.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.COxp5Rrc.js +0 -1
- package/site-dist/assets/reference_contracts_state.md.EEBd_l2P.js +0 -1
- /package/site-dist/assets/{guide_getting-started.md.D8XlIQOg.lean.js → guide_getting-started.md.CZM53rO_.lean.js} +0 -0
- /package/site-dist/assets/{reference_contracts_state.md.EEBd_l2P.lean.js → reference_contracts_state.md.STlqkPPS.lean.js} +0 -0
package/engine/src/messages.ts
CHANGED
|
@@ -54,6 +54,24 @@ export function permissionMessage(input: {
|
|
|
54
54
|
].join("\n");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** 1.14: a step agent ran (or started) a dangerous command; the pipeline stopped it. */
|
|
58
|
+
export function dangerNotice(input: { stepTitle: string; agentTitle: string; command: string; label: string; ran: boolean }): string {
|
|
59
|
+
const one = input.command.replace(/\s+/g, " ").trim();
|
|
60
|
+
return [
|
|
61
|
+
HEADER,
|
|
62
|
+
"",
|
|
63
|
+
`流水线的「${input.stepTitle}」步骤里,agent ${input.ran ? "运行了" : "开始运行"}一条不允许的命令(${input.label}),${input.ran ? "流水线发现时它已经运行完" : "流水线已经打断了它"}:`,
|
|
64
|
+
"",
|
|
65
|
+
"```text",
|
|
66
|
+
one.length > 300 ? `${one.slice(0, 299)}…` : one,
|
|
67
|
+
"```",
|
|
68
|
+
"",
|
|
69
|
+
input.ran
|
|
70
|
+
? `请告诉用户这件事,并请用户检查它有没有改动仓库以外或远程的东西(例如远程分支、制品仓库、集群、其他机器、数据库)。需要撤销的由用户处理;不要自己去撤销。agent「${input.agentTitle}」已被告知不要再这样做,流水线继续。`
|
|
71
|
+
: `请告诉用户这件事。agent「${input.agentTitle}」已被告知不要再这样做,流水线继续;如果这条命令其实是允许的,可以在配置 dangerGuard.allow 里放行。`,
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
57
75
|
export function failedMessage(input: {
|
|
58
76
|
stepTitle: string;
|
|
59
77
|
reason: string;
|
|
@@ -427,3 +445,14 @@ export function pluginReplyMessage(input: { featureDir: string; waitingFor: "bus
|
|
|
427
445
|
: "请这样处理:按 /req 中\"需要需求方决定\"的做法,把回答记入 alignment.md 并更新 spec.md;回答已经覆盖全部问题时,向用户简要复述后执行继续命令;还缺的,在对话里接着问。",
|
|
428
446
|
].join("\n");
|
|
429
447
|
}
|
|
448
|
+
|
|
449
|
+
/** 1.14: the requirement's chat is reminded once that a confirmed spec has not started development. */
|
|
450
|
+
export function notStartedNotice(featureDir: string): string {
|
|
451
|
+
return [
|
|
452
|
+
"【需求开发流水线提示】这条消息来自 req-kit 的后台检查,不是用户发的。",
|
|
453
|
+
"",
|
|
454
|
+
`需求 ${featureDir} 的需求说明已经确认超过 30 分钟,但开发流水线还没有启动。`,
|
|
455
|
+
"如果用户确实要开发它:请按 /req 的手册启动开发(或者告诉用户可以在插件\"等你处理\"里点\"开始开发\")。",
|
|
456
|
+
"如果是用户有意先不开发、还要再改需求,或者已经在别处开始了:用一句话确认即可,不需要其他操作。这个提醒只发一次。",
|
|
457
|
+
].join("\n");
|
|
458
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// What happened after the run (1.14): whether each requirement's change reached the main line,
|
|
2
|
+
// and problems found after that. Recorded as events in the usage records (records.ts), so
|
|
3
|
+
// `req stats` can report merges and post-merge problems next to the runs. Merges are found
|
|
4
|
+
// from what is already in the local repository (no fetch): the requirement's pipeline files
|
|
5
|
+
// (implementation.md, report.md, …) are in the main line's tree. A defect requirement whose spec
|
|
6
|
+
// says "- 引入问题的需求:<目录名或标题>" counts as a problem of that requirement.
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { PIPELINE_OUTPUTS } from "./features.js";
|
|
11
|
+
import { appendEvent, type EventRecord, type IndexEntry, readEvents, readIndex, requirementRef, runsFile } from "./records.js";
|
|
12
|
+
|
|
13
|
+
function git(repo: string, args: string[]): string | null {
|
|
14
|
+
try {
|
|
15
|
+
return execFileSync("git", args, { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The main line of a repository as known locally: origin's default branch, else main or master. */
|
|
22
|
+
export function mainLine(repo: string): string | null {
|
|
23
|
+
const head = git(repo, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])?.trim();
|
|
24
|
+
for (const ref of [head, "origin/main", "origin/master", "main", "master"]) {
|
|
25
|
+
if (ref && git(repo, ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]) !== null) return ref;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether the requirement's change is in the main line, with the commit that brought it there. */
|
|
31
|
+
export function mergeOf(repo: string, featureDir: string): NonNullable<EventRecord["merge"]> | null {
|
|
32
|
+
const ref = mainLine(repo);
|
|
33
|
+
if (!ref) return null;
|
|
34
|
+
const fd = featureDir.split(path.sep).join("/");
|
|
35
|
+
const marker = PIPELINE_OUTPUTS.find((f) => git(repo, ["cat-file", "-e", `${ref}:${fd}/${f}`]) !== null);
|
|
36
|
+
if (!marker) return null;
|
|
37
|
+
const line = git(repo, ["log", "--first-parent", "--diff-filter=A", "-1", "--format=%H%x09%cI", ref, "--", `${fd}/${marker}`])?.trim() ?? "";
|
|
38
|
+
const [commit, at] = line.split("\t");
|
|
39
|
+
return { into: ref, commit: commit ? commit.slice(0, 12) : null, mergedAt: at || null, how: "files" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The spec of a requirement: in its directory, or (worktree removed) in the main line. */
|
|
43
|
+
function specOf(entry: IndexEntry): string | null {
|
|
44
|
+
const file = path.join(entry.repo, entry.featureDir, "spec.md");
|
|
45
|
+
if (fs.existsSync(file)) return fs.readFileSync(file, "utf8");
|
|
46
|
+
const repo = [entry.sourceRepo, entry.repo].find((r): r is string => !!r && fs.existsSync(r));
|
|
47
|
+
const ref = repo ? mainLine(repo) : null;
|
|
48
|
+
return repo && ref ? git(repo, ["show", `${ref}:${entry.featureDir}/spec.md`]) : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CAUSED_BY = /^\s*[-*]\s*引入问题的需求[::]\s*(.+?)\s*$/m;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Looks at every requirement this machine knows (the index) and records what is new: merges
|
|
55
|
+
* into the main line, and defect requirements that name the requirement that caused them.
|
|
56
|
+
* Returns the events it added. Never throws.
|
|
57
|
+
*/
|
|
58
|
+
export function scanOutcomes(env: NodeJS.ProcessEnv = process.env): EventRecord[] {
|
|
59
|
+
const added: EventRecord[] = [];
|
|
60
|
+
try {
|
|
61
|
+
const events = readEvents(runsFile(env));
|
|
62
|
+
const merged = new Set(events.filter((e) => e.kind === "merge").map((e) => `${e.requirement.project}\0${e.requirement.key}`));
|
|
63
|
+
const issues = new Set(events.filter((e) => e.kind === "issue").map((e) => e.issue?.id));
|
|
64
|
+
const entries = readIndex(env);
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const repo = [entry.sourceRepo, entry.repo].find((r): r is string => !!r && fs.existsSync(r));
|
|
67
|
+
if (!repo) continue;
|
|
68
|
+
const ref = requirementRef(entry.repo, entry.featureDir, env);
|
|
69
|
+
if (!merged.has(`${ref.project}\0${ref.key}`)) {
|
|
70
|
+
const m = mergeOf(repo, entry.featureDir);
|
|
71
|
+
if (m) {
|
|
72
|
+
const e = { kind: "merge" as const, requirement: ref, merge: m };
|
|
73
|
+
appendEvent(e, env);
|
|
74
|
+
added.push({ schema: 1, machine: "", kitVersion: "", at: new Date().toISOString(), ...e });
|
|
75
|
+
merged.add(`${ref.project}\0${ref.key}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const cause = CAUSED_BY.exec(specOf(entry) ?? "")?.[1];
|
|
79
|
+
const id = `defect:${entry.key}`;
|
|
80
|
+
if (!cause || issues.has(id)) continue;
|
|
81
|
+
const target = entries.find((o) => o.key !== entry.key && (path.basename(o.featureDir) === cause || o.featureDir === cause || requirementRef(o.repo, o.featureDir, env).title === cause));
|
|
82
|
+
if (!target) continue;
|
|
83
|
+
const e = { kind: "issue" as const, requirement: requirementRef(target.repo, target.featureDir, env), issue: { text: ref.title, by: ref.name, id } };
|
|
84
|
+
appendEvent(e, env);
|
|
85
|
+
added.push({ schema: 1, machine: "", kitVersion: "", at: new Date().toISOString(), ...e });
|
|
86
|
+
issues.add(id);
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Outcomes are a convenience for the statistics.
|
|
90
|
+
}
|
|
91
|
+
return added;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const USAGE = `用法:
|
|
95
|
+
req outcome 查找已经合并进主干的需求、合并后发现的问题(缺陷需求里写了"引入问题的需求"),记入使用记录
|
|
96
|
+
req outcome <需求> --merged [--commit <提交>] 手动记一笔:这个需求已经合并(例如合并后删掉了需求目录)
|
|
97
|
+
req outcome <需求> --issue "<问题>" 手动记一笔:这个需求合并后发现的问题
|
|
98
|
+
|
|
99
|
+
合并是从本机仓库里已有的分支判断的,判断前可以先 git fetch。结果在 req stats 里看。`;
|
|
100
|
+
|
|
101
|
+
async function main(argv: string[]): Promise<number> {
|
|
102
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
103
|
+
console.log(USAGE);
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
const feature = argv.find((a, i) => !a.startsWith("--") && !["--issue", "--commit"].includes(argv[i - 1] ?? ""));
|
|
107
|
+
const value = (flag: string) => {
|
|
108
|
+
const i = argv.indexOf(flag);
|
|
109
|
+
return i >= 0 ? (argv[i + 1] ?? null) : null;
|
|
110
|
+
};
|
|
111
|
+
if (!feature) {
|
|
112
|
+
const added = scanOutcomes();
|
|
113
|
+
if (added.length === 0) console.log("没有新的合并或合并后问题。");
|
|
114
|
+
for (const e of added) {
|
|
115
|
+
console.log(e.kind === "merge" ? `已合并:${e.requirement.title}(${e.merge!.into}${e.merge!.commit ? `,${e.merge!.commit}` : ""})` : `合并后的问题:${e.requirement.title} ← ${e.issue!.by}「${e.issue!.text}」`);
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
const { locate } = await import("./api.js");
|
|
120
|
+
const { repo, featureDir } = locate(process.cwd(), feature);
|
|
121
|
+
const requirement = requirementRef(repo, featureDir);
|
|
122
|
+
if (argv.includes("--merged")) {
|
|
123
|
+
appendEvent({ kind: "merge", requirement, merge: { into: mainLine(repo) ?? "main", commit: value("--commit"), mergedAt: null, how: "manual" } });
|
|
124
|
+
console.log(`已记录:${requirement.title} 已合并。`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
const text = value("--issue");
|
|
128
|
+
if (text && !text.startsWith("--")) {
|
|
129
|
+
appendEvent({ kind: "issue", requirement, issue: { text: text.slice(0, 500), by: null, id: `manual:${Date.now()}` } });
|
|
130
|
+
console.log(`已记录:${requirement.title} 合并后的问题「${text.slice(0, 80)}」。`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
console.error(USAGE);
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
|
|
138
|
+
main(process.argv.slice(2)).then(
|
|
139
|
+
(code) => process.exit(code),
|
|
140
|
+
(error: Error) => {
|
|
141
|
+
console.error(error.message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PaseoAgentHandle, PaseoClient } from "@getpaseo/client";
|
|
2
|
-
import { runningToolOf, summarizeActivity } from "./activity.js";
|
|
2
|
+
import { runningToolOf, toolCallsOf, summarizeActivity } from "./activity.js";
|
|
3
3
|
import { type AgentProfile, isResolved, type PipelineConfig } from "./config.js";
|
|
4
4
|
import { profilesFromDaemonConfig } from "./paseo-profiles.js";
|
|
5
5
|
import { cronAt, localTimeZone } from "./schedule-time.js";
|
|
@@ -296,6 +296,14 @@ export function createRuntimeWithClient(
|
|
|
296
296
|
return null;
|
|
297
297
|
}
|
|
298
298
|
},
|
|
299
|
+
async toolCalls(agentId) {
|
|
300
|
+
try {
|
|
301
|
+
const page = await client.agents.ref(agentId).timeline.refetch({ direction: "tail", limit: 200 });
|
|
302
|
+
return toolCallsOf(page.entries);
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
},
|
|
299
307
|
async createWorktree(request) {
|
|
300
308
|
if (!client.workspaces) throw new Error("这个 Paseo 版本不支持新建工作区");
|
|
301
309
|
const workspace = await client.workspaces.create({
|
package/engine/src/pipeline.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { judge as judgeScan } from "./scan-guard.js";
|
|
2
|
+
import { dangerMessage, dangerOf } from "./danger-guard.js";
|
|
3
|
+
import { describeFinding, scanChange, secretFollowUp } from "./secret-scan.js";
|
|
2
4
|
import { execFileSync } from "node:child_process";
|
|
3
5
|
import fs from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
verifyStartPrompt,
|
|
30
32
|
milestoneMessage,
|
|
31
33
|
permissionMessage,
|
|
34
|
+
dangerNotice,
|
|
32
35
|
resultFollowUp,
|
|
33
36
|
} from "./messages.js";
|
|
34
37
|
import { KIT_ROOT, kitVersion } from "./locations.js";
|
|
@@ -38,7 +41,7 @@ import { describeExternal, type IntegrationEvent, renderArgs, resolveCommand, ty
|
|
|
38
41
|
import { cardSlug, planAppendix, publishCard, readSource } from "./card.js";
|
|
39
42
|
import { principlesSection, projectAdditions, type PromptLocations, readToolPrompt } from "./prompts.js";
|
|
40
43
|
import type { AgentRuntime, ManagedService, ServiceTarget, StepAgent, WaitOutcome } from "./runtime.js";
|
|
41
|
-
import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type Recovery, type RunPhases, type StepRecord, type StopKind, writeState } from "./state.js";
|
|
44
|
+
import { type CheckOutcome, type GateRun, type JointRepo, type PhaseName, type PipelineState, type Recovery, type SecretScanRun, type RunPhases, type StepRecord, type StopKind, writeState } from "./state.js";
|
|
42
45
|
import { type PanelStep, STEPS, type StepDef, type StepId, stepKey, validateResult, VERIFY_PHASE_STEPS } from "./steps.js";
|
|
43
46
|
|
|
44
47
|
export interface PipelineContext {
|
|
@@ -518,6 +521,15 @@ function pushBranch(ctx: PipelineContext): void {
|
|
|
518
521
|
const at = new Date().toISOString();
|
|
519
522
|
for (const repo of repos) {
|
|
520
523
|
let error: string | null = null;
|
|
524
|
+
// 1.14: nothing with a secret in it leaves the machine.
|
|
525
|
+
const target = (ctx.state.joint ?? []).map((j) => ({ joint: j, repo: j.repo })).find((t) => t.repo === repo);
|
|
526
|
+
const secrets = ctx.config.secretScan.enabled ? scanChange(repo, secretBase(ctx, target as StepTarget | undefined), ctx.config.secretScan) : [];
|
|
527
|
+
if (secrets && secrets.length > 0) {
|
|
528
|
+
error = `发现 ${secrets.length} 处疑似密钥,没有推送(例如 ${describeFinding(secrets[0])});去掉后继续开发会再推送`;
|
|
529
|
+
ctx.log(`推送 ${repo} 的分支:${error}`);
|
|
530
|
+
ctx.state.pushed = [...(ctx.state.pushed ?? []).filter((p) => p.repo !== repo), { repo, remote: push.remote, branch, at, error, mergeRequest: ctx.state.pushed?.find((p) => p.repo === repo)?.mergeRequest ?? null }];
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
521
533
|
try {
|
|
522
534
|
execFileSync("git", ["push", push.remote, `HEAD:refs/heads/${branch}`], { cwd: repo, stdio: ["ignore", "pipe", "pipe"], timeout: 180_000, windowsHide: true });
|
|
523
535
|
} catch (e) {
|
|
@@ -661,9 +673,57 @@ async function guardScan(ctx: PipelineContext, agent: StepAgent, def: StepDef, k
|
|
|
661
673
|
recover(ctx, key, "stop_scan", `「${def.title}」的命令${verdict.reason},已打断并告诉 agent 改用别的办法:${tool.text.replace(/\s+/g, " ").slice(0, 120)}`);
|
|
662
674
|
}
|
|
663
675
|
|
|
676
|
+
/** How often the danger guard looks at the agent's commands (it waits in chunks this long). */
|
|
677
|
+
const DANGER_CHECK_SECONDS = 15;
|
|
678
|
+
/** At most this many danger interruptions per turn. */
|
|
679
|
+
const DANGER_MAX_INTERRUPTS = 5;
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* 1.14: looks at the commands the step agent ran or is running; a dangerous one is recorded, the
|
|
683
|
+
* person is told, and the agent is interrupted (while its turn runs) with what to do instead.
|
|
684
|
+
* Each tool call is handled once, also across turns and pipeline restarts.
|
|
685
|
+
*/
|
|
686
|
+
async function guardDanger(ctx: PipelineContext, agent: StepAgent, def: StepDef, agentTitle: string, key: string, turnRunning: boolean, memory: { count: number }): Promise<void> {
|
|
687
|
+
const settings = ctx.config.dangerGuard;
|
|
688
|
+
if (!settings.enabled || !ctx.runtime.toolCalls) return;
|
|
689
|
+
const calls = await ctx.runtime.toolCalls(agent.id).catch(() => null);
|
|
690
|
+
if (!calls) return;
|
|
691
|
+
const seen = new Set((ctx.state.dangers ?? []).filter((d) => d.agentId === agent.id).map((d) => d.callId));
|
|
692
|
+
for (const call of calls) {
|
|
693
|
+
if (seen.has(call.callId) || !["shell", "fetch", "unknown"].includes(call.kind)) continue;
|
|
694
|
+
const danger = dangerOf(call.text, settings, { repo: agentRepo(ctx, agent) });
|
|
695
|
+
if (!danger) continue;
|
|
696
|
+
const ran = call.status !== "running";
|
|
697
|
+
seen.add(call.callId);
|
|
698
|
+
ctx.state.dangers = [...(ctx.state.dangers ?? []), { at: new Date().toISOString(), key, stepTitle: def.title, command: call.text.slice(0, 500), label: danger.label, ran, agentId: agent.id, callId: call.callId }].slice(-50);
|
|
699
|
+
save(ctx);
|
|
700
|
+
ctx.log(`危险命令(${key}):${danger.label},${ran ? "已运行完" : "正在运行,打断"}:${call.text.replace(/\s+/g, " ").slice(0, 160)}`);
|
|
701
|
+
if (turnRunning && memory.count < DANGER_MAX_INTERRUPTS) {
|
|
702
|
+
memory.count += 1;
|
|
703
|
+
await ctx.runtime.interruptAgent(agent.id, dangerMessage(call.text, danger, ran)).catch((error: Error) => ctx.log(`打断「${def.title}」失败:${error.message}`));
|
|
704
|
+
}
|
|
705
|
+
await notifyOrigin(ctx, dangerNotice({ stepTitle: def.title, agentTitle, command: call.text, label: danger.label, ran }));
|
|
706
|
+
await notifyExternal(ctx, "alert", `「${def.title}」步骤的 agent ${ran ? "运行了" : "开始运行"}不允许的命令(${danger.label})${ran ? ",请检查有没有造成影响" : ",已打断"}。`, {
|
|
707
|
+
kind: "danger",
|
|
708
|
+
stepTitle: def.title,
|
|
709
|
+
command: call.text.slice(0, 500),
|
|
710
|
+
label: danger.label,
|
|
711
|
+
ran,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** The directory the agent works in: the joint repository's worktree when it is one of theirs. */
|
|
717
|
+
function agentRepo(ctx: PipelineContext, agent: StepAgent): string {
|
|
718
|
+
const record = ctx.state.history.find((h) => h.agentId === agent.id);
|
|
719
|
+
const slug = record?.key.includes("@") ? record.key.split("@").pop() : null;
|
|
720
|
+
return (slug && ctx.state.joint?.find((j) => j.slug === slug)?.repo) || ctx.repo;
|
|
721
|
+
}
|
|
722
|
+
|
|
664
723
|
async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef, agentTitle: string, deadline: Deadline): Promise<AgentUsage | null> {
|
|
665
724
|
const notifiedPermissions = new Set<string>();
|
|
666
725
|
const guard: ScanGuardMemory = { interrupted: new Set(), count: 0 };
|
|
726
|
+
const danger = { count: 0 };
|
|
667
727
|
let consecutiveErrors = 0;
|
|
668
728
|
for (;;) {
|
|
669
729
|
let remaining = deadline.at - Date.now();
|
|
@@ -686,7 +746,9 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
686
746
|
let outcome;
|
|
687
747
|
try {
|
|
688
748
|
// 1.13: with the scan guard on, look at the running tool call about once a minute.
|
|
689
|
-
|
|
749
|
+
// 1.14: with the danger guard on, look at its commands every 15 seconds.
|
|
750
|
+
const guardSeconds = ctx.config.dangerGuard.enabled ? DANGER_CHECK_SECONDS : ctx.config.scanGuard.enabled ? SCAN_CHECK_SECONDS : Infinity;
|
|
751
|
+
const chunk = Math.min(ctx.config.waitChunkSeconds, guardSeconds);
|
|
690
752
|
outcome = await agent.wait(Math.min(chunk * 1000, remaining));
|
|
691
753
|
consecutiveErrors = 0;
|
|
692
754
|
} catch (error) {
|
|
@@ -706,8 +768,10 @@ async function waitForTurn(ctx: PipelineContext, agent: StepAgent, def: StepDef,
|
|
|
706
768
|
}
|
|
707
769
|
switch (outcome.status) {
|
|
708
770
|
case "idle":
|
|
771
|
+
await guardDanger(ctx, agent, def, agentTitle, deadline.key, false, danger);
|
|
709
772
|
return outcome.usage ?? null;
|
|
710
773
|
case "timeout":
|
|
774
|
+
await guardDanger(ctx, agent, def, agentTitle, deadline.key, true, danger);
|
|
711
775
|
await guardScan(ctx, agent, def, deadline.key, guard);
|
|
712
776
|
continue;
|
|
713
777
|
case "error":
|
|
@@ -924,12 +988,14 @@ async function runStepOnce(
|
|
|
924
988
|
|
|
925
989
|
if (extras.gate) {
|
|
926
990
|
// The same agent fixes what its own change broke: it still has the context.
|
|
927
|
-
|
|
991
|
+
const fix = async (text: string) => {
|
|
928
992
|
await agent.followUp(text);
|
|
929
993
|
record.usage = (await waitForTurn(ctx, agent, def, agentTitle, deadline)) ?? record.usage ?? null;
|
|
930
994
|
const again = readResultFile(resultAbs, def);
|
|
931
995
|
if (again.ok) validation = again;
|
|
932
|
-
}
|
|
996
|
+
};
|
|
997
|
+
await qualityGate(ctx, key, target, fix);
|
|
998
|
+
await secretGate(ctx, key, target, fix);
|
|
933
999
|
}
|
|
934
1000
|
|
|
935
1001
|
record.outcome = "completed";
|
|
@@ -1703,6 +1769,69 @@ async function qualityGate(ctx: PipelineContext, key: string, target: StepTarget
|
|
|
1703
1769
|
}
|
|
1704
1770
|
}
|
|
1705
1771
|
|
|
1772
|
+
/** The commit the secret scan starts from: the attempt's code base, else where the branch left the main line. */
|
|
1773
|
+
function secretBase(ctx: PipelineContext, target?: StepTarget): string | null {
|
|
1774
|
+
const repo = target?.repo ?? ctx.repo;
|
|
1775
|
+
const recorded = codeBaseCommit(ctx, target);
|
|
1776
|
+
if (recorded) return recorded;
|
|
1777
|
+
for (const ref of ["origin/HEAD", "origin/main", "origin/master", "main", "master"]) {
|
|
1778
|
+
try {
|
|
1779
|
+
const mb = execFileSync("git", ["merge-base", "HEAD", ref], { cwd: repo, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim();
|
|
1780
|
+
if (mb && mb !== headCommit(repo)) return mb;
|
|
1781
|
+
} catch {
|
|
1782
|
+
/* try the next */
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
return null;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* 1.14: after a step that changed code, looks for secrets the requirement added (secret-scan.ts);
|
|
1790
|
+
* what it finds goes back to the step's agent, like a failing check, up to qualityGate.fixAttempts
|
|
1791
|
+
* times. The outcome is kept for the review, the report and the push.
|
|
1792
|
+
*/
|
|
1793
|
+
async function secretGate(ctx: PipelineContext, key: string, target: StepTarget | undefined, fix: (text: string) => Promise<void>): Promise<void> {
|
|
1794
|
+
if (!ctx.config.secretScan.enabled) return;
|
|
1795
|
+
const repo = target?.repo ?? ctx.repo;
|
|
1796
|
+
const base = secretBase(ctx, target);
|
|
1797
|
+
const max = ctx.config.qualityGate.fixAttempts;
|
|
1798
|
+
for (let fixRounds = 0; ; fixRounds += 1) {
|
|
1799
|
+
const findings = scanChange(repo, base, ctx.config.secretScan);
|
|
1800
|
+
if (findings === null) {
|
|
1801
|
+
ctx.log(`密钥扫描(${key} 之后):找不到改动的起点,没有扫描`);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (findings.length === 0 || fixRounds >= max) {
|
|
1805
|
+
const run: SecretScanRun = { key, attempt: ctx.state.attempt, at: new Date().toISOString(), findings, fixRounds, passed: findings.length === 0 };
|
|
1806
|
+
ctx.state.secretScans = [...(ctx.state.secretScans ?? []).filter((r) => !(r.key === key && r.attempt === run.attempt)), run].slice(-30);
|
|
1807
|
+
save(ctx);
|
|
1808
|
+
ctx.log(`密钥扫描(${key} 之后):${run.passed ? (fixRounds > 0 ? `要求处理 ${fixRounds} 次后没有发现` : "没有发现") : `要求处理 ${fixRounds} 次后仍有 ${findings.length} 处`}`);
|
|
1809
|
+
if (!run.passed) {
|
|
1810
|
+
await notifyOrigin(ctx, milestoneMessage(`「${key}」之后的密钥扫描仍发现 ${findings.length} 处疑似密钥(例如 ${describeFinding(findings[0])})。这些提交不会被推送;请用户看一下是不是真实的密钥。`));
|
|
1811
|
+
}
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
ctx.log(`密钥扫描(${key} 之后):发现 ${findings.length} 处疑似密钥,第 ${fixRounds + 1} 次要求处理`);
|
|
1815
|
+
await fix(secretFollowUp({ findings, base: base!, round: fixRounds + 1, max }));
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/** What the secret scan found in this attempt, for the code review and the report. */
|
|
1820
|
+
function secretLines(ctx: PipelineContext): string[] {
|
|
1821
|
+
const runs = (ctx.state.secretScans ?? []).filter((r) => r.attempt === ctx.state.attempt);
|
|
1822
|
+
const last = runs.at(-1);
|
|
1823
|
+
if (!last) return [];
|
|
1824
|
+
if (last.passed) return [`密钥扫描(${last.key} 之后,流水线运行):没有发现疑似密钥${last.fixRounds > 0 ? `(要求处理 ${last.fixRounds} 次后)` : ""}`];
|
|
1825
|
+
return [`密钥扫描(${last.key} 之后,流水线运行):仍有 ${last.findings.length} 处疑似密钥,分支不会被推送:`, ...last.findings.slice(0, 10).map((f) => `- ${describeFinding(f)}`)];
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/** Dangerous commands step agents ran in this run, for the report. */
|
|
1829
|
+
function dangerLines(ctx: PipelineContext): string[] {
|
|
1830
|
+
const list = ctx.state.dangers ?? [];
|
|
1831
|
+
if (list.length === 0) return [];
|
|
1832
|
+
return [`流水线拦下的危险命令(${list.length} 次,已通知用户):`, ...list.slice(-10).map((d) => `- 「${d.stepTitle}」${d.ran ? "运行了" : "开始运行(已打断)"}:${d.label}`)];
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1706
1835
|
function describeGateRun(run: GateRun): string {
|
|
1707
1836
|
const ran = run.checks.filter((c) => c.result !== "skipped");
|
|
1708
1837
|
const skipped = run.checks.length - ran.length;
|
|
@@ -1963,7 +2092,7 @@ async function developPhase(ctx: PipelineContext): Promise<Record<string, unknow
|
|
|
1963
2092
|
|
|
1964
2093
|
const reviewLoop = async (target?: StepTarget) => {
|
|
1965
2094
|
for (let r = 1; r <= rounds.codeReview; r++) {
|
|
1966
|
-
const lines = [...gateLines(ctx, target), ...(target ? [] : scopeLines(ctx)), ...acceptanceLines(ctx, target)];
|
|
2095
|
+
const lines = [...gateLines(ctx, target), ...(target ? [] : [...secretLines(ctx), ...scopeLines(ctx)]), ...acceptanceLines(ctx, target)];
|
|
1967
2096
|
const review = await runReview(ctx, "code_review", r, rounds.codeReview, lines, target);
|
|
1968
2097
|
if (review.verdict === "pass") break;
|
|
1969
2098
|
await runStep(ctx, "review_fix", r, rounds.codeReview, { gate: true, target });
|
|
@@ -2142,7 +2271,7 @@ function externalLines(ctx: PipelineContext): string[] {
|
|
|
2142
2271
|
}
|
|
2143
2272
|
|
|
2144
2273
|
function reportLines(ctx: PipelineContext): string[] {
|
|
2145
|
-
const lines = [...usageLines(ctx), ...gateLines(ctx), ...scopeLines(ctx), ...acceptanceLines(ctx), ...jointLines(ctx), ...externalLines(ctx)];
|
|
2274
|
+
const lines = [...usageLines(ctx), ...gateLines(ctx), ...secretLines(ctx), ...dangerLines(ctx), ...scopeLines(ctx), ...acceptanceLines(ctx), ...jointLines(ctx), ...externalLines(ctx)];
|
|
2146
2275
|
if (ctx.config.checks.length === 0 && ctx.state.type !== "assessment") {
|
|
2147
2276
|
lines.push("质量门:项目没有配置检查(checks),流水线没有自己运行检查,以各步骤记录的检查结果为准");
|
|
2148
2277
|
}
|
package/engine/src/records.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// on the machine. The overview itself is still read live (ADR 0010).
|
|
7
7
|
// usage/runs.jsonl one line per pipeline run (a process from start to stop): steps,
|
|
8
8
|
// time, usage, gate and review rounds. What `req stats` reports on.
|
|
9
|
+
// 1.14: also one line per event after the run: a person's code review,
|
|
10
|
+
// the acceptance, the merge into the main line, a problem found after it.
|
|
9
11
|
//
|
|
10
12
|
// Recording never affects a run: every write is best effort. REQ_KIT_NO_RECORD=1 turns both
|
|
11
13
|
// off (evaluation runs are not real requirements).
|
|
@@ -321,3 +323,72 @@ export function readRuns(file: string): RunRecord[] {
|
|
|
321
323
|
}
|
|
322
324
|
return out;
|
|
323
325
|
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Events after the run (1.14)
|
|
329
|
+
|
|
330
|
+
export type EventKind = "review" | "acceptance" | "merge" | "issue";
|
|
331
|
+
|
|
332
|
+
/** Something that happened to a requirement outside its runs (contracts/usage-record.md). */
|
|
333
|
+
export interface EventRecord {
|
|
334
|
+
schema: 1;
|
|
335
|
+
kind: EventKind;
|
|
336
|
+
machine: string;
|
|
337
|
+
kitVersion: string;
|
|
338
|
+
at: string;
|
|
339
|
+
requirement: { key: string; project: string; name: string; title: string };
|
|
340
|
+
/** A person's code review in the plugin. */
|
|
341
|
+
review?: { verdict: "approved" | "changes_requested"; reviewer: string; items: number; head: string | null };
|
|
342
|
+
/** The requester's acceptance on the checklist page. */
|
|
343
|
+
acceptance?: { verdict: "通过" | "不通过"; reviewer: string; total: number; passed: number; failed: number; unchecked: number };
|
|
344
|
+
/** The requirement's change reached the main line. how: found its files there ("files"), or a person said so ("manual"). */
|
|
345
|
+
merge?: { into: string; commit: string | null; mergedAt: string | null; how: "files" | "manual" };
|
|
346
|
+
/** A problem found after the merge: from a defect requirement that names this one, or a person's note. */
|
|
347
|
+
issue?: { text: string; by: string | null; id: string };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** The requirement as events name it: the index entry when there is one (same key as its runs). */
|
|
351
|
+
export function requirementRef(repo: string, featureDir: string, env: NodeJS.ProcessEnv = process.env): EventRecord["requirement"] {
|
|
352
|
+
const fd = featureDir.split(path.sep).join("/");
|
|
353
|
+
const r = real(repo);
|
|
354
|
+
const entry = readIndex(env).find((e) => e.featureDir === fd && (real(e.repo) === r || (e.sourceRepo && real(e.sourceRepo) === r)));
|
|
355
|
+
const source = entry?.sourceRepo ?? entry?.repo ?? repo;
|
|
356
|
+
let title = path.basename(fd);
|
|
357
|
+
try {
|
|
358
|
+
const m = /^#\s+(.+)$/m.exec(fs.readFileSync(path.join(entry?.repo ?? repo, fd, "spec.md"), "utf8"));
|
|
359
|
+
if (m) title = m[1].trim();
|
|
360
|
+
} catch {
|
|
361
|
+
/* no spec: the directory name */
|
|
362
|
+
}
|
|
363
|
+
return { key: entry?.key ?? requirementKey(source, fd), project: entry?.project ?? path.basename(real(source)), name: path.basename(fd), title };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Appends an event to the usage records. Never throws. */
|
|
367
|
+
export function appendEvent(input: Omit<EventRecord, "schema" | "machine" | "kitVersion" | "at"> & { at?: string }, env: NodeJS.ProcessEnv = process.env): void {
|
|
368
|
+
if (recordingOff(env)) return;
|
|
369
|
+
const record: EventRecord = { schema: 1, machine: os.hostname(), kitVersion: kitVersion(), at: input.at ?? new Date().toISOString(), ...input } as EventRecord;
|
|
370
|
+
try {
|
|
371
|
+
fs.mkdirSync(usageDir(env), { recursive: true });
|
|
372
|
+
fs.appendFileSync(runsFile(env), `${JSON.stringify(record)}\n`);
|
|
373
|
+
} catch {
|
|
374
|
+
// Usage records are a convenience.
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const EVENT_KINDS = new Set<string>(["review", "acceptance", "merge", "issue"]);
|
|
379
|
+
|
|
380
|
+
/** Event records from a JSONL file (this machine's, or an export). */
|
|
381
|
+
export function readEvents(file: string): EventRecord[] {
|
|
382
|
+
if (!fs.existsSync(file)) return [];
|
|
383
|
+
const out: EventRecord[] = [];
|
|
384
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
385
|
+
if (!line.trim()) continue;
|
|
386
|
+
try {
|
|
387
|
+
const r = JSON.parse(line) as EventRecord;
|
|
388
|
+
if (r && EVENT_KINDS.has(r.kind) && r.schema === RECORD_SCHEMA && r.requirement && typeof r.at === "string") out.push(r);
|
|
389
|
+
} catch {
|
|
390
|
+
// A torn line.
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
package/engine/src/reply.ts
CHANGED
|
@@ -6,12 +6,15 @@ import { DEFAULT_CONFIG, loadConfig, type PipelineConfig } from "./config.js";
|
|
|
6
6
|
import { defaultConfigPath } from "./locations.js";
|
|
7
7
|
import { pluginReplyMessage } from "./messages.js";
|
|
8
8
|
import { loadRuntimeFactory } from "./runtime-loader.js";
|
|
9
|
+
import { readIndex } from "./records.js";
|
|
9
10
|
import { readState, statePath } from "./state.js";
|
|
10
11
|
|
|
11
12
|
async function main(argvIn: string[]): Promise<number> {
|
|
12
13
|
// 1.14 --acceptance: the checklist page found problems; the text is the whole notice.
|
|
14
|
+
// 1.14 --notice: a notice of the background check (no run needed: the chat it was aligned in).
|
|
13
15
|
const acceptance = argvIn.includes("--acceptance");
|
|
14
|
-
const
|
|
16
|
+
const notice = argvIn.includes("--notice");
|
|
17
|
+
const argv = argvIn.filter((a) => a !== "--acceptance" && a !== "--notice");
|
|
15
18
|
const [feature, ...words] = argv;
|
|
16
19
|
const text = words.join(" ").trim();
|
|
17
20
|
if (!feature || !text) {
|
|
@@ -20,6 +23,14 @@ async function main(argvIn: string[]): Promise<number> {
|
|
|
20
23
|
}
|
|
21
24
|
const { repo, featureDir } = locate(process.cwd(), feature);
|
|
22
25
|
const state = readState(statePath(repo, featureDir));
|
|
26
|
+
if (notice) {
|
|
27
|
+
const chat = state?.originAgentId ?? readIndex().find((e) => e.featureDir === featureDir && [e.repo, e.sourceRepo].includes(repo))?.chatAgentId ?? null;
|
|
28
|
+
if (!chat) {
|
|
29
|
+
console.error("这个需求没有登记过对话,没有可以转达的对话。");
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
return deliver(repo, chat, text.slice(0, 8000), "已转给这个需求的对话。");
|
|
33
|
+
}
|
|
23
34
|
if (!state || (!acceptance && state.status !== "waiting_for_business" && state.status !== "waiting_for_approval")) {
|
|
24
35
|
console.error(`${featureDir} 现在没有在等待回答或确认,不用回复。`);
|
|
25
36
|
return 1;
|
|
@@ -28,6 +39,22 @@ async function main(argvIn: string[]): Promise<number> {
|
|
|
28
39
|
console.error("这个需求不是从 Paseo 对话中开始的,没有可以转达的对话。请在终端或卡片中处理。");
|
|
29
40
|
return 1;
|
|
30
41
|
}
|
|
42
|
+
return deliver(
|
|
43
|
+
repo,
|
|
44
|
+
state.originAgentId,
|
|
45
|
+
acceptance
|
|
46
|
+
? text.slice(0, 8000)
|
|
47
|
+
: pluginReplyMessage({
|
|
48
|
+
featureDir,
|
|
49
|
+
waitingFor: state.status === "waiting_for_approval" ? "approval" : "business",
|
|
50
|
+
text: text.slice(0, 8000),
|
|
51
|
+
questions: state.pendingQuestions ?? [],
|
|
52
|
+
}),
|
|
53
|
+
acceptance ? "已转给这个需求的对话。" : "已转给这个需求的对话,它会按回复更新需求说明并继续开发。",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function deliver(repo: string, chat: string, message: string, done: string): Promise<number> {
|
|
31
58
|
let config: PipelineConfig;
|
|
32
59
|
try {
|
|
33
60
|
config = loadConfig(defaultConfigPath(), repo).config;
|
|
@@ -37,19 +64,11 @@ async function main(argvIn: string[]): Promise<number> {
|
|
|
37
64
|
}
|
|
38
65
|
const runtime = await (await loadRuntimeFactory())({ config, log: () => undefined });
|
|
39
66
|
try {
|
|
40
|
-
await runtime.notifyAgent(
|
|
41
|
-
state.originAgentId,
|
|
42
|
-
acceptance ? text.slice(0, 8000) : pluginReplyMessage({
|
|
43
|
-
featureDir,
|
|
44
|
-
waitingFor: state.status === "waiting_for_approval" ? "approval" : "business",
|
|
45
|
-
text: text.slice(0, 8000),
|
|
46
|
-
questions: state.pendingQuestions ?? [],
|
|
47
|
-
}),
|
|
48
|
-
);
|
|
67
|
+
await runtime.notifyAgent(chat, message);
|
|
49
68
|
} finally {
|
|
50
69
|
await runtime.close().catch(() => undefined);
|
|
51
70
|
}
|
|
52
|
-
console.log(
|
|
71
|
+
console.log(done);
|
|
53
72
|
return 0;
|
|
54
73
|
}
|
|
55
74
|
|
package/engine/src/review.ts
CHANGED
|
@@ -17,7 +17,7 @@ import path from "node:path";
|
|
|
17
17
|
import { ENGINE_DIR, KIT_ROOT, kitHome, kitVersion } from "./locations.js";
|
|
18
18
|
import { buildCodeReview, recordHumanReview, startReviewFix, type Submission } from "./code-review.js";
|
|
19
19
|
import { type Answer, acceptanceFailMessage, acceptanceText, buildChecklist, recordAcceptance, verdictOf } from "./checklist.js";
|
|
20
|
-
import { readIndex } from "./records.js";
|
|
20
|
+
import { appendEvent, readIndex, requirementRef } from "./records.js";
|
|
21
21
|
import { readState, statePath } from "./state.js";
|
|
22
22
|
|
|
23
23
|
const ASSETS = path.join(ENGINE_DIR, "src", "review-assets");
|
|
@@ -546,6 +546,20 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
|
|
|
546
546
|
const text = acceptanceText(list, answers, { verdict: v.verdict, reviewer: reviewer.slice(0, 60), date: new Date().toISOString().slice(0, 10), previous });
|
|
547
547
|
const { committed } = recordAcceptance(entry.repo, entry.featureDir, text);
|
|
548
548
|
fs.rmSync(path.join(checkDir, "draft.json"), { force: true });
|
|
549
|
+
// 1.14: the acceptance also goes into the usage records, for req stats.
|
|
550
|
+
const marks = new Map(answers.map((a) => [a.id, a.mark]));
|
|
551
|
+
appendEvent({
|
|
552
|
+
kind: "acceptance",
|
|
553
|
+
requirement: requirementRef(entry.repo, entry.featureDir),
|
|
554
|
+
acceptance: {
|
|
555
|
+
verdict: v.verdict,
|
|
556
|
+
reviewer: reviewer.slice(0, 60),
|
|
557
|
+
total: list.items.length,
|
|
558
|
+
passed: list.items.filter((i) => marks.get(i.id) === "pass").length,
|
|
559
|
+
failed: list.items.filter((i) => marks.get(i.id) === "fail").length,
|
|
560
|
+
unchecked: list.items.filter((i) => !marks.get(i.id) || marks.get(i.id) === "skip").length,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
549
563
|
if (v.verdict === "通过") {
|
|
550
564
|
return json(res, 200, { ok: true, verdict: v.verdict, committed, message: `已记录:验收通过${committed ? ",验收记录已提交" : ""}。下一步请开发同事审阅代码并合并。` });
|
|
551
565
|
}
|
package/engine/src/runtime.ts
CHANGED
|
@@ -145,6 +145,8 @@ export interface AgentRuntime {
|
|
|
145
145
|
recentActivity(agentId: string, limit: number): Promise<ActivityItem[] | null>;
|
|
146
146
|
/** 1.13: the tool call the agent is running now (the latest one still running), or null. */
|
|
147
147
|
runningTool(agentId: string): Promise<import("./scan-guard.js").RunningTool | null>;
|
|
148
|
+
/** 1.14: the agent's latest tool calls, running or done (for the danger guard); null when they cannot be read. */
|
|
149
|
+
toolCalls?(agentId: string): Promise<import("./danger-guard.js").ToolCall[] | null>;
|
|
148
150
|
/** Which Paseo tools step agents get (Settings → Agents); null when unknown. */
|
|
149
151
|
capabilities(): Promise<{ paseoTools: boolean; browser: boolean } | null>;
|
|
150
152
|
/** 1.12: whether each agent provider is installed and logged in ("ready"); null when the daemon does not say. */
|