paseo-req-kit 1.14.0-beta.3 → 1.14.0-beta.5

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/req.mjs +14 -0
  3. package/contracts/cli-json.md +1 -1
  4. package/contracts/notifier-event.md +1 -1
  5. package/contracts/spec.md +1 -0
  6. package/contracts/state.md +3 -1
  7. package/contracts/usage-record.md +16 -2
  8. package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +11 -1
  9. 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
  10. package/engine/prompts/00-shared-context.md +3 -2
  11. package/engine/prompts/06-code-review.md +12 -1
  12. package/engine/prompts/incremental.md +2 -0
  13. package/engine/src/activity.ts +21 -0
  14. package/engine/src/api.ts +44 -7
  15. package/engine/src/cli-args.ts +4 -1
  16. package/engine/src/code-review.ts +422 -0
  17. package/engine/src/codeowners.ts +87 -0
  18. package/engine/src/config-view.ts +10 -0
  19. package/engine/src/config.ts +27 -0
  20. package/engine/src/danger-guard.ts +180 -0
  21. package/engine/src/features.ts +1 -1
  22. package/engine/src/main.ts +13 -3
  23. package/engine/src/messages.ts +29 -0
  24. package/engine/src/outcomes.ts +145 -0
  25. package/engine/src/paseo-runtime.ts +9 -1
  26. package/engine/src/pipeline.ts +135 -6
  27. package/engine/src/preflight.ts +10 -1
  28. package/engine/src/records.ts +71 -0
  29. package/engine/src/reply.ts +30 -11
  30. package/engine/src/review-assets/code.css +93 -0
  31. package/engine/src/review-assets/code.html +43 -0
  32. package/engine/src/review-assets/code.js +402 -0
  33. package/engine/src/review.ts +98 -2
  34. package/engine/src/runtime.ts +2 -0
  35. package/engine/src/secret-scan.ts +177 -0
  36. package/engine/src/state.ts +30 -0
  37. package/engine/src/stats.ts +143 -9
  38. package/engine/src/watchdog.ts +38 -4
  39. package/package.json +1 -1
  40. package/paseo-plugin/client/data.ts +2 -2
  41. package/paseo-plugin/client/detail.tsx +7 -2
  42. package/paseo-plugin/client/nav.ts +5 -0
  43. package/paseo-plugin/client/review.tsx +16 -6
  44. package/paseo-plugin/client/stats.tsx +15 -0
  45. package/paseo-plugin/package.json +1 -1
  46. package/paseo-plugin/server/handlers.ts +1 -1
  47. package/paseo-plugin/shared/contracts.ts +11 -2
  48. package/site-dist/404.html +2 -2
  49. package/site-dist/assets/{app.BpC99f5a.js → app.TlOHPtfE.js} +1 -1
  50. package/site-dist/assets/changelog.md.DiGi8kHU.js +1 -0
  51. package/site-dist/assets/{changelog.md.m4cSU5e4.lean.js → changelog.md.DiGi8kHU.lean.js} +1 -1
  52. package/site-dist/assets/chunks/@localSearchIndexroot.Dk_-Yka5.js +1 -0
  53. package/site-dist/assets/chunks/{VPLocalSearchBox.CI43rhTA.js → VPLocalSearchBox.BUVNy7_o.js} +1 -1
  54. package/site-dist/assets/chunks/{theme.S6pNEF23.js → theme.BFeiG1US.js} +2 -2
  55. package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.js → guide_getting-started.md.UhkLCUoY.js} +1 -1
  56. package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.lean.js → guide_getting-started.md.UhkLCUoY.lean.js} +1 -1
  57. package/site-dist/assets/{guide_project.md.Csi5wiyt.js → guide_project.md.DIjDRoAq.js} +1 -1
  58. package/site-dist/assets/{guide_project.md.Csi5wiyt.lean.js → guide_project.md.DIjDRoAq.lean.js} +1 -1
  59. package/site-dist/assets/{guide_user.md.D9GpGJ7y.js → guide_user.md.UV0WxdJa.js} +1 -1
  60. package/site-dist/assets/{guide_user.md.D9GpGJ7y.lean.js → guide_user.md.UV0WxdJa.lean.js} +1 -1
  61. package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.js → reference_contracts_cli-json.md.CCvKLasM.js} +1 -1
  62. package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.lean.js → reference_contracts_cli-json.md.CCvKLasM.lean.js} +1 -1
  63. package/site-dist/assets/{reference_contracts_notifier-event.md.CCfrkm_A.js → reference_contracts_notifier-event.md.B9WqeZdO.js} +1 -1
  64. package/site-dist/assets/{reference_contracts_notifier-event.md.CCfrkm_A.lean.js → reference_contracts_notifier-event.md.B9WqeZdO.lean.js} +1 -1
  65. package/site-dist/assets/{reference_contracts_spec.md.BwwS0L_j.js → reference_contracts_spec.md.Dh5XczZL.js} +2 -2
  66. package/site-dist/assets/{reference_contracts_spec.md.BwwS0L_j.lean.js → reference_contracts_spec.md.Dh5XczZL.lean.js} +2 -2
  67. package/site-dist/assets/reference_contracts_state.md.STlqkPPS.js +1 -0
  68. package/site-dist/assets/{reference_contracts_usage-record.md.D5krNLW7.lean.js → reference_contracts_usage-record.md.MjVps4CA.js} +2 -2
  69. package/site-dist/assets/{reference_contracts_usage-record.md.D5krNLW7.js → reference_contracts_usage-record.md.MjVps4CA.lean.js} +2 -2
  70. package/site-dist/changelog.html +5 -5
  71. package/site-dist/guide/getting-started.html +5 -5
  72. package/site-dist/guide/how-it-works.html +4 -4
  73. package/site-dist/guide/index.html +4 -4
  74. package/site-dist/guide/install.html +4 -4
  75. package/site-dist/guide/maintainer.html +4 -4
  76. package/site-dist/guide/phases.html +4 -4
  77. package/site-dist/guide/project.html +6 -6
  78. package/site-dist/guide/user.html +5 -5
  79. package/site-dist/hashmap.json +1 -1
  80. package/site-dist/index.html +4 -4
  81. package/site-dist/reference/adr/0001-step-agents-under-origin-chat.html +4 -4
  82. package/site-dist/reference/adr/0002-no-directory-switch-in-chat.html +4 -4
  83. package/site-dist/reference/adr/0003-script-engine-not-hub.html +4 -4
  84. package/site-dist/reference/adr/0004-result-files.html +4 -4
  85. package/site-dist/reference/adr/0005-no-default-branch-local-commits-only.html +4 -4
  86. package/site-dist/reference/adr/0006-skill-install-targets.html +4 -4
  87. package/site-dist/reference/adr/0007-own-interview-skill.html +4 -4
  88. package/site-dist/reference/adr/0008-entries-do-not-call-entries.html +4 -4
  89. package/site-dist/reference/adr/0009-versioned-install-layout.html +4 -4
  90. package/site-dist/reference/adr/0010-generated-feature-overview.html +4 -4
  91. package/site-dist/reference/adr/0011-resume-after-completion.html +4 -4
  92. package/site-dist/reference/adr/0012-review-panel.html +4 -4
  93. package/site-dist/reference/adr/0013-no-release-tooling.html +4 -4
  94. package/site-dist/reference/adr/0014-notifier-slot.html +4 -4
  95. package/site-dist/reference/adr/0015-incremental-continue.html +4 -4
  96. package/site-dist/reference/adr/0016-upgrade-from-tags.html +4 -4
  97. package/site-dist/reference/adr/0017-use-paseo-capabilities.html +4 -4
  98. package/site-dist/reference/adr/0018-own-worktree-option.html +4 -4
  99. package/site-dist/reference/adr/0019-parallel-orchestration.html +4 -4
  100. package/site-dist/reference/adr/0020-scheduling-through-the-chat.html +4 -4
  101. package/site-dist/reference/adr/0021-npm-version-manager.html +4 -4
  102. package/site-dist/reference/adr/0022-develop-and-verify-phases.html +4 -4
  103. package/site-dist/reference/adr/0023-types-gate-slices.html +4 -4
  104. package/site-dist/reference/adr/0024-confirm-early-review-axes.html +4 -4
  105. package/site-dist/reference/adr/0025-joint-requirements.html +4 -4
  106. package/site-dist/reference/adr/0026-machine-records-and-api.html +4 -4
  107. package/site-dist/reference/adr/0027-paseo-plugin.html +4 -4
  108. package/site-dist/reference/adr/0028-external-integrations.html +4 -4
  109. package/site-dist/reference/adr/0029-requirements-from-cards.html +4 -4
  110. package/site-dist/reference/adr/0030-card-intake-process.html +4 -4
  111. package/site-dist/reference/adr/0031-fewer-interventions.html +4 -4
  112. package/site-dist/reference/adr/0032-npm-only-install.html +4 -4
  113. package/site-dist/reference/adr/0033-prototype-review-and-ui-check.html +4 -4
  114. package/site-dist/reference/adr/index.html +4 -4
  115. package/site-dist/reference/commands.html +4 -4
  116. package/site-dist/reference/config.html +4 -4
  117. package/site-dist/reference/contracts/cli-json.html +5 -5
  118. package/site-dist/reference/contracts/feature-files.html +4 -4
  119. package/site-dist/reference/contracts/index.html +4 -4
  120. package/site-dist/reference/contracts/notifier-event.html +6 -6
  121. package/site-dist/reference/contracts/result-file.html +4 -4
  122. package/site-dist/reference/contracts/spec.html +6 -6
  123. package/site-dist/reference/contracts/state.html +5 -5
  124. package/site-dist/reference/contracts/usage-record.html +6 -6
  125. package/site-dist/reference/contracts/verify.html +4 -4
  126. package/site-dist/reference/scope.html +4 -4
  127. package/skills/req/SKILL.md +2 -0
  128. package/skills/req/playbooks/incident.md +1 -1
  129. package/skills/spec-writing/SKILL.md +2 -1
  130. package/site-dist/assets/changelog.md.m4cSU5e4.js +0 -1
  131. package/site-dist/assets/chunks/@localSearchIndexroot.D6ywcTJ5.js +0 -1
  132. package/site-dist/assets/reference_contracts_state.md.EEBd_l2P.js +0 -1
  133. /package/site-dist/assets/{reference_contracts_state.md.EEBd_l2P.lean.js → reference_contracts_state.md.STlqkPPS.lean.js} +0 -0
@@ -0,0 +1,180 @@
1
+ // Stopping dangerous commands (1.14): step agents usually run commands without asking (the
2
+ // provider's "no confirmation" mode), so the rules "only local commits, never touch production"
3
+ // were only words in the prompt. While a step runs, the pipeline looks at the commands the agent
4
+ // has run or is running (its Paseo timeline, every few seconds to a minute); a command that
5
+ // pushes, publishes, changes a cluster, connects to a remote database or host, deletes outside
6
+ // the repository, runs a downloaded script, or mentions a configured production address is
7
+ // interrupted (if still running) and reported to the person. A plain check of the command
8
+ // text, no model; it catches the common cases, it is not a sandbox.
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ export interface DangerGuardSettings {
13
+ enabled: boolean;
14
+ /** Text that marks a production system (host names, addresses, database names); any command containing it is stopped. */
15
+ production: string[];
16
+ /** Regular expressions of commands that are allowed after all (e.g. "^ssh test-box"). */
17
+ allow: string[];
18
+ }
19
+
20
+ export interface ToolCall {
21
+ callId: string;
22
+ kind: string;
23
+ text: string;
24
+ status: string;
25
+ startedAt: string;
26
+ }
27
+
28
+ export interface Danger {
29
+ /** What kind of operation it is, for the person (e.g. "推送到远程仓库"). */
30
+ label: string;
31
+ /** Why it is not allowed in a pipeline step. */
32
+ why: string;
33
+ }
34
+
35
+ const LOCAL_HOSTS = /^(localhost|127(\.\d+){3}|0\.0\.0\.0|::1|\[::1\]|host\.docker\.internal)$/i;
36
+
37
+ /** Splits a command line into simple commands (on ; && || | and newlines), each a list of words without quotes. */
38
+ export function segments(command: string): string[][] {
39
+ const out: string[][] = [];
40
+ for (const part of command.split(/\|\||&&|[;|\n]|\r/)) {
41
+ const words = (part.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((w) => w.replace(/^["']|["']$/g, ""));
42
+ // Wrappers that run the rest as a command.
43
+ while (words.length && /^(sudo|env|nohup|time|exec|command|xargs|npx|cmd(\.exe)?|\/c|powershell(\.exe)?|pwsh|-Command|-c|bash|sh|zsh|-lc|-l)$/i.test(words[0])) words.shift();
44
+ while (words.length && /^\w+=/.test(words[0])) words.shift();
45
+ // A command given as one quoted argument (bash -lc "git push"): look inside it.
46
+ if (words.length && /\s/.test(words[0])) out.push(...segments(words.join(" ")));
47
+ else if (words.length) out.push(words);
48
+ }
49
+ return out;
50
+ }
51
+
52
+ const base = (w: string) => w.replace(/\\/g, "/").split("/").pop()!.replace(/\.(exe|cmd|bat)$/i, "").toLowerCase();
53
+
54
+ function hostOf(value: string): string {
55
+ const v = value.replace(/^\w+:\/\//, "").replace(/^[^@/]*@/, "");
56
+ return v.replace(/[:/].*$/, "");
57
+ }
58
+
59
+ /** A host that is clearly another machine: an address or a dotted name that is not this machine. Service names of docker compose ("db") count as local. */
60
+ function remoteHost(host: string): boolean {
61
+ if (!host || LOCAL_HOSTS.test(host)) return false;
62
+ return /\./.test(host) || /^\d+(\.\d+){3}$/.test(host);
63
+ }
64
+
65
+ function deletesOutside(words: string[], repo: string | null, home: string): string | null {
66
+ const targets = words.slice(1).filter((w) => !w.startsWith("-") && !/^\/[a-z]$/i.test(w));
67
+ const h = home.replace(/\\/g, "/").replace(/\/+$/, "");
68
+ for (const raw of targets) {
69
+ const t = raw.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
70
+ if (/^(\/|\/\*|~|~\/\*|\$HOME|\$\{HOME\}|%USERPROFILE%|\$env:USERPROFILE|[a-z]:|[a-z]:\/\*|\.\.(\/\.\.)*(\/\*)?|\*|\/\*\.\*)$/i.test(t)) return raw;
71
+ if (t === h) return raw;
72
+ if (/^(\/|[a-z]:\/)/i.test(t)) {
73
+ const abs = path.resolve(t);
74
+ const inside = (dir: string | null) => !!dir && (abs === path.resolve(dir) || abs.startsWith(path.resolve(dir) + path.sep));
75
+ if (!inside(repo) && !inside(os.tmpdir()) && !/^\/(tmp|var\/tmp)(\/|$)/.test(t)) return raw;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function judgeSegment(words: string[], repo: string | null, home: string): Danger | null {
82
+ const cmd = base(words[0]);
83
+ const rest = words.slice(1);
84
+ const sub = rest.find((w) => !w.startsWith("-"))?.toLowerCase() ?? "";
85
+ const has = (re: RegExp) => rest.some((w) => re.test(w));
86
+
87
+ if (cmd === "git") {
88
+ // Skip global options (-C dir, -c k=v) to find the git command.
89
+ let i = 0;
90
+ while (i < rest.length && rest[i].startsWith("-")) i += /^-[Cc]$/.test(rest[i]) ? 2 : 1;
91
+ const g = rest[i]?.toLowerCase();
92
+ const args = rest.slice(i + 1);
93
+ if (g === "push") {
94
+ const force = args.some((w) => /^(--force|--force-with-lease|-f|--mirror|--delete|-d)$/.test(w) || /^\+/.test(w) || /^--force-with-lease=/.test(w) || /^:/.test(w));
95
+ return force
96
+ ? { label: "强制推送或删除远程分支", why: "会改写或删除远程仓库的历史,别人的提交可能丢失" }
97
+ : { label: "推送到远程仓库", why: "流水线只做本地提交,推送和合并由人决定(接单器模式由流水线自己推送需求分支)" };
98
+ }
99
+ if (g === "remote" && /^(add|set-url|remove|rm)$/.test(args[0] ?? "")) return { label: "修改远程仓库地址", why: "会改变代码推送到哪里" };
100
+ return null;
101
+ }
102
+ if (cmd === "rm" && rest.some((w) => /^-[a-z]*r/i.test(w) || w === "--recursive")) {
103
+ const t = deletesOutside(words, repo, home);
104
+ if (t) return { label: `删除仓库以外的目录(${t})`, why: "会删除仓库以外的文件,无法恢复" };
105
+ }
106
+ if (/^(remove-item|ri|rd|rmdir|del|erase)$/.test(cmd) && has(/^(-recurse|-r|\/s)$/i)) {
107
+ const t = deletesOutside(words.filter((w) => !/^-(recurse|force|r)$/i.test(w) && !/^\/[sq]$/i.test(w)), repo, home);
108
+ if (t) return { label: `删除仓库以外的目录(${t})`, why: "会删除仓库以外的文件,无法恢复" };
109
+ }
110
+ if (/^(npm|pnpm|yarn|cargo|gem|twine|poetry|flit)$/.test(cmd) && (sub === "publish" || (sub === "npm" && rest[1] === "publish") || (cmd === "gem" && sub === "push") || (cmd === "twine" && sub === "upload")))
111
+ return { label: "发布包到制品仓库", why: "发布出去的版本别人马上能用到,撤不回来" };
112
+ if (/^(mvn|mvnw)$/.test(cmd) && has(/^(deploy|release:perform|deploy:deploy(-file)?)$/)) return { label: "发布制品(mvn deploy)", why: "发布出去的版本别人马上能用到,撤不回来" };
113
+ if (/^(gradle|gradlew)$/.test(cmd) && has(/^(publish\w*|uploadArchives)$/)) return { label: "发布制品(gradle publish)", why: "发布出去的版本别人马上能用到,撤不回来" };
114
+ if (cmd === "docker" && sub === "push") return { label: "推送镜像到镜像仓库", why: "镜像仓库是共享的,部署可能直接用到" };
115
+ if (cmd === "docker" && sub === "login") return { label: "登录镜像仓库", why: "会在这台机器上保存镜像仓库的凭据" };
116
+ if (cmd === "dotnet" && sub === "nuget" && rest[1] === "push") return { label: "发布包到制品仓库", why: "发布出去的版本别人马上能用到,撤不回来" };
117
+ if (/^(kubectl|oc)$/.test(cmd) && /^(apply|create|delete|replace|patch|scale|rollout|edit|drain|cordon|exec|cp|set|label|annotate|run)$/.test(sub))
118
+ return { label: "修改集群(kubectl)", why: "会改动共享的环境,流水线只在本地或测试环境验证" };
119
+ if (cmd === "helm" && /^(install|upgrade|uninstall|delete|rollback)$/.test(sub)) return { label: "修改集群(helm)", why: "会改动共享的环境,流水线只在本地或测试环境验证" };
120
+ if (cmd === "terraform" && /^(apply|destroy|import|taint)$/.test(sub)) return { label: "修改基础设施(terraform)", why: "会改动共享的环境,流水线只在本地或测试环境验证" };
121
+ if (cmd === "ansible-playbook" || (cmd === "ansible" && !has(/^(--check|-C)$/))) return { label: "在服务器上执行(ansible)", why: "会改动其他机器" };
122
+ if (/^(mysql|mysqldump|psql|pg_dump|mongo|mongosh|redis-cli|sqlcmd|sqlplus|clickhouse-client|clickhouse)$/.test(cmd)) {
123
+ for (let i = 0; i < rest.length; i++) {
124
+ const w = rest[i];
125
+ let host: string | null = null;
126
+ const m = /^(-h|--host|-H|-S|--server)(=?)(.*)$/.exec(w);
127
+ if (m) host = m[3] || rest[i + 1] || "";
128
+ else if (/^\w+:\/\//.test(w)) host = hostOf(w);
129
+ else if (cmd === "sqlplus" && /@/.test(w)) host = hostOf(w.split("@").pop() ?? "");
130
+ if (host !== null && remoteHost(host.replace(/,\d+$/, ""))) return { label: `连接其他机器上的数据库(${host})`, why: "可能是共享或生产的数据库;流水线只用本机或测试用的数据库" };
131
+ }
132
+ }
133
+ if (/^(ssh|scp|sftp|rsync)$/.test(cmd)) {
134
+ const target = rest.find((w) => !w.startsWith("-") && (cmd === "ssh" || cmd === "sftp" || /:/.test(w)));
135
+ if (target && !/^git@/.test(target)) {
136
+ const host = cmd === "ssh" || cmd === "sftp" ? hostOf(target) : hostOf(target.split(":")[0]);
137
+ if (host && !LOCAL_HOSTS.test(host)) return { label: `登录或复制文件到其他机器(${host})`, why: "会改动其他机器;流水线只在本地或测试环境验证" };
138
+ }
139
+ }
140
+ if (/^(shutdown|reboot|halt|poweroff|mkfs|diskpart|format|stop-computer|restart-computer)$/.test(cmd) || /^mkfs\./.test(cmd)) return { label: "系统级操作", why: "会影响整台机器" };
141
+ if (cmd === "dd" && has(/^of=\/dev\//)) return { label: "直接写磁盘", why: "会破坏磁盘上的数据" };
142
+ if (/^(chmod|chown)$/.test(cmd) && has(/^-R$/) && rest.some((w) => /^(\/|~|\$HOME)\/?$/.test(w))) return { label: "修改整个系统的权限", why: "会影响整台机器" };
143
+ return null;
144
+ }
145
+
146
+ /** Whether a command (or a fetched address) is one a pipeline step must not run, and why. */
147
+ export function dangerOf(text: string, settings: Pick<DangerGuardSettings, "production" | "allow">, options: { repo?: string | null; home?: string } = {}): Danger | null {
148
+ if (!text.trim()) return null;
149
+ for (const a of settings.allow) {
150
+ try {
151
+ if (new RegExp(a, "i").test(text)) return null;
152
+ } catch {
153
+ /* validated with the configuration */
154
+ }
155
+ }
156
+ const lower = text.toLowerCase();
157
+ for (const p of settings.production) {
158
+ if (p.trim() && lower.includes(p.trim().toLowerCase())) return { label: `访问生产环境(${p.trim()})`, why: "流水线不连接生产环境、不使用生产凭据" };
159
+ }
160
+ if (/\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(ba|z|da)?sh\b/i.test(text) || /\b(iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(iex|invoke-expression)\b/i.test(text))
161
+ return { label: "下载脚本直接执行", why: "执行的内容没有经过检查" };
162
+ const home = options.home ?? os.homedir();
163
+ for (const words of segments(text)) {
164
+ const d = judgeSegment(words, options.repo ?? null, home);
165
+ if (d) return d;
166
+ }
167
+ return null;
168
+ }
169
+
170
+ /** What the agent is told when the command is stopped (or was found after it ran). */
171
+ export function dangerMessage(command: string, danger: Danger, ran: boolean): string {
172
+ const one = command.replace(/\s+/g, " ").trim();
173
+ const clip = one.length > 160 ? `${one.slice(0, 159)}…` : one;
174
+ return [
175
+ `【流水线提示】${ran ? "你刚才运行了" : "已停止你正在运行的"}命令「${clip}」:这是${danger.label},${danger.why}。需求开发流水线里不允许这类操作,用户已收到通知。`,
176
+ "- 不要再运行这条命令,也不要换一种写法做同样的事。",
177
+ `- ${ran ? "如果它已经改动了仓库以外或远程的东西,不要自己试图撤销;" : ""}在本步骤产物里写一节\"风险操作\":运行了什么、为什么、${ran ? "造成了什么影响" : "是否已经产生影响"}。`,
178
+ "- 然后继续完成本步骤的任务:需要推送、发布、部署或连接其他环境的,写进产物的\"需要人处理\",由人来做。",
179
+ ].join("\n");
180
+ }
@@ -83,7 +83,7 @@ interface RawState {
83
83
  * Files only the pipeline writes: their presence means it ran, even where its state is not (a
84
84
  * copy merged into the main branch: the state file under .pipeline/ is never committed).
85
85
  */
86
- const PIPELINE_OUTPUTS = ["design.md", "assessment.md", "diagnosis.md", "implementation.md", "handoff.md", "verification.md", "report.md"];
86
+ export const PIPELINE_OUTPUTS = ["design.md", "assessment.md", "diagnosis.md", "implementation.md", "handoff.md", "verification.md", "report.md"];
87
87
 
88
88
  export function developedIn(dir: string): boolean {
89
89
  return PIPELINE_OUTPUTS.some((f) => fs.existsSync(path.join(dir, f)));
@@ -1,6 +1,7 @@
1
1
  // The long-running pipeline process. Normally started by launch.ts, detached
2
2
  // from the chat agent's shell; can also be run in the foreground for debugging.
3
3
 
4
+ import { pendingHumanReview, writeHumanReview } from "./code-review.js";
4
5
  import { execFileSync } from "node:child_process";
5
6
  import fs from "node:fs";
6
7
  import path from "node:path";
@@ -191,11 +192,20 @@ async function main(): Promise<number> {
191
192
  approval:
192
193
  args.approve && mode.previous.status === "waiting_for_approval"
193
194
  ? { attempt: mode.previous.attempt, status: "approved", at: now }
194
- : startOver
195
- ? null
196
- : (mode.previous.approval ?? null),
195
+ : // 1.14: code review comments do not change what the requester confirmed: no second confirmation.
196
+ args.reviewFix && mode.previous.approval?.status === "approved"
197
+ ? { ...mode.previous.approval, attempt: mode.previous.attempt + 1 }
198
+ : startOver
199
+ ? null
200
+ : (mode.previous.approval ?? null),
197
201
  gate: mode.kind === "restart" ? null : (mode.previous.gate ?? null),
198
202
  };
203
+ // 1.14: the person's code review comments are this attempt's to handle.
204
+ if (args.reviewFix && mode.kind === "resume-from-design") {
205
+ const featureAbs = path.join(repo, featureDir);
206
+ const pending = pendingHumanReview(featureAbs);
207
+ if (pending) writeHumanReview(featureAbs, { ...pending, consumedAttempt: mode.previous.attempt + 1 });
208
+ }
199
209
  if (checked.entersVerify) {
200
210
  // A develop-only run that ends with its development report gets a full report after verification.
201
211
  delete state.results.report;
@@ -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({