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,402 @@
1
+ // The code review page (1.14). Plain script, no framework: the server serves it next to its JSON
2
+ // API (api/info, api/draft, api/submit). Everything shown is set with textContent.
3
+ (function () {
4
+ "use strict";
5
+ var state = { info: null, comments: [], decisions: {}, reviewer: "", reviewers: "", note: "", saveTimer: null, busy: false, open: null };
6
+ var $ = function (id) {
7
+ return document.getElementById(id);
8
+ };
9
+
10
+ function el(tag, attrs, children) {
11
+ var node = document.createElement(tag);
12
+ Object.keys(attrs || {}).forEach(function (k) {
13
+ if (attrs[k] === null || attrs[k] === undefined) return;
14
+ if (k === "text") node.textContent = attrs[k];
15
+ else if (k === "className") node.className = attrs[k];
16
+ else if (k.indexOf("on") === 0) node.addEventListener(k.slice(2), attrs[k]);
17
+ else node.setAttribute(k, attrs[k]);
18
+ });
19
+ (children || []).forEach(function (c) {
20
+ if (c) node.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
21
+ });
22
+ return node;
23
+ }
24
+
25
+ function toast(text) {
26
+ var t = $("toast");
27
+ t.textContent = text;
28
+ t.hidden = false;
29
+ clearTimeout(toast.timer);
30
+ toast.timer = setTimeout(function () {
31
+ t.hidden = true;
32
+ }, 8000);
33
+ }
34
+
35
+ function api(pathname, method, body) {
36
+ return fetch(pathname, { method: method || "GET", headers: body ? { "content-type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined }).then(function (r) {
37
+ return r.json().then(function (data) {
38
+ if (!r.ok && data && data.message) throw new Error(data.message);
39
+ return data;
40
+ });
41
+ });
42
+ }
43
+
44
+ function reviewersList() {
45
+ return state.reviewers
46
+ .split(/[,,、\s]+/)
47
+ .map(function (s) {
48
+ return s.trim();
49
+ })
50
+ .filter(Boolean);
51
+ }
52
+
53
+ function save() {
54
+ if (state.info.readOnly) return;
55
+ clearTimeout(state.saveTimer);
56
+ state.saveTimer = setTimeout(function () {
57
+ api("api/draft", "PUT", { comments: state.comments, decisions: state.decisions, reviewer: state.reviewer, reviewers: state.reviewers, note: state.note }).catch(function () {});
58
+ }, 600);
59
+ count();
60
+ }
61
+
62
+ function fixes() {
63
+ return Object.keys(state.decisions)
64
+ .filter(function (id) {
65
+ return state.decisions[id].mark === "fix";
66
+ })
67
+ .map(function (id) {
68
+ return { id: id, note: state.decisions[id].note || "" };
69
+ });
70
+ }
71
+
72
+ function count() {
73
+ var comments = state.comments.filter(function (c) {
74
+ return c.text.trim();
75
+ }).length;
76
+ var fix = fixes().length;
77
+ var total = comments + fix + (state.note.trim() ? 1 : 0);
78
+ $("count").textContent = "代码行上的意见 " + comments + " 条,AI 意见标为要改 " + fix + " 条";
79
+ var blocked = state.info.readOnly || state.info.running || state.busy;
80
+ $("changes").disabled = blocked || total === 0;
81
+ $("changes").textContent = total ? "退回修改(" + total + " 条意见)" : "退回修改";
82
+ $("approve").disabled = blocked;
83
+ }
84
+
85
+ var SEVERITY_CLASS = { "阻塞": "danger", "重要": "warn", "建议": "", "待核实": "" };
86
+ var STATUS_TEXT = { fixed: "已修复", rejected: "修复时判为不成立", wontfix: "决定不修复", open: "未处理" };
87
+
88
+ function findingNode(f, compact) {
89
+ var d = state.decisions[f.id] || {};
90
+ var note = el("input", { className: "note-input", placeholder: "补充一句(可不填)", value: d.note || "", hidden: d.mark === "fix" ? null : "" });
91
+ note.addEventListener("input", function (e) {
92
+ state.decisions[f.id] = { mark: "fix", note: e.target.value };
93
+ save();
94
+ });
95
+ function mark(m) {
96
+ return function () {
97
+ var cur = state.decisions[f.id] && state.decisions[f.id].mark;
98
+ if (cur === m) delete state.decisions[f.id];
99
+ else state.decisions[f.id] = { mark: m, note: (state.decisions[f.id] && state.decisions[f.id].note) || "" };
100
+ render();
101
+ save();
102
+ };
103
+ }
104
+ var where = f.file ? f.file + (f.line ? ":" + f.line : "") : "";
105
+ return el("div", { className: "finding " + (SEVERITY_CLASS[f.severity] || "") + (d.mark ? " decided-" + d.mark : "") }, [
106
+ el("div", { className: "finding-head" }, [
107
+ el("span", { className: "badge " + (SEVERITY_CLASS[f.severity] || ""), text: "AI · " + f.severity }),
108
+ el("b", { text: f.id }),
109
+ f.status !== "open" ? el("span", { className: "badge " + (f.status === "fixed" ? "ok" : ""), text: STATUS_TEXT[f.status] }) : null,
110
+ !compact && where ? el("a", { href: "#", className: "where", text: where, onclick: function (e) {
111
+ e.preventDefault();
112
+ jump(f.file, f.line);
113
+ } }) : null,
114
+ ]),
115
+ el("div", { className: "finding-text", text: f.title }),
116
+ f.suggestion ? el("div", { className: "muted small", text: "建议:" + f.suggestion }) : null,
117
+ state.info.readOnly
118
+ ? null
119
+ : el("div", { className: "marks" }, [
120
+ el("button", { className: "btn small" + (d.mark === "fix" ? " on-fix" : ""), text: "要改", onclick: mark("fix") }),
121
+ el("button", { className: "btn small" + (d.mark === "ok" ? " on-ok" : ""), text: "不用改", onclick: mark("ok") }),
122
+ ]),
123
+ note,
124
+ ]);
125
+ }
126
+
127
+ function jump(file, line) {
128
+ var target = document.querySelector('[data-file="' + CSS.escape(file) + '"]' + (line ? '[data-n="' + line + '"]' : ""));
129
+ if (!target) target = document.querySelector('[data-file-head="' + CSS.escape(file) + '"]');
130
+ if (target) {
131
+ var box = target.closest(".file");
132
+ if (box && box.classList.contains("folded")) box.classList.remove("folded");
133
+ target.scrollIntoView({ block: "center", behavior: "smooth" });
134
+ target.classList.add("flash");
135
+ setTimeout(function () {
136
+ target.classList.remove("flash");
137
+ }, 1600);
138
+ }
139
+ }
140
+
141
+ function commentBox(file, line) {
142
+ var existing = state.comments.find(function (c) {
143
+ return c.file === file && c.line === line;
144
+ });
145
+ if (!existing) {
146
+ existing = { file: file, line: line, text: "" };
147
+ state.comments.push(existing);
148
+ }
149
+ var ta = el("textarea", { rows: "2", placeholder: "这一行哪里不对、应该怎么改" });
150
+ ta.value = existing.text;
151
+ ta.addEventListener("input", function (e) {
152
+ existing.text = e.target.value;
153
+ save();
154
+ });
155
+ var remove = el("button", { className: "btn small", text: "删除", onclick: function () {
156
+ state.comments = state.comments.filter(function (c) {
157
+ return c !== existing;
158
+ });
159
+ state.open = null;
160
+ render();
161
+ save();
162
+ } });
163
+ return el("tr", { className: "comment-row" }, [el("td", { colspan: "3" }, [el("div", { className: "comment" }, [el("div", { className: "muted small", text: "你的意见(" + file + ":" + line + ")" }), ta, state.info.readOnly ? null : remove])])]);
164
+ }
165
+
166
+ function renderFile(f, findingsByFile) {
167
+ var mine = findingsByFile[f.path] || [];
168
+ var owners = (state.info.reviewers.byFile || {})[f.path] || [];
169
+ var head = el("div", { className: "file-head", "data-file-head": f.path, onclick: function () {
170
+ box.classList.toggle("folded");
171
+ } }, [
172
+ el("span", { className: "path", text: f.path + (f.oldPath ? "(原 " + f.oldPath + ")" : "") }),
173
+ el("span", { className: "badge", text: { added: "新增", deleted: "删除", modified: "修改", renamed: "改名" }[f.status] }),
174
+ el("span", { className: "plus", text: "+" + f.additions }),
175
+ el("span", { className: "minus", text: "−" + f.deletions }),
176
+ f.doc ? el("span", { className: "badge", text: "需求文档" }) : null,
177
+ mine.length ? el("span", { className: "badge warn", text: "AI 意见 " + mine.length }) : null,
178
+ owners.length ? el("span", { className: "muted small", text: "负责人 " + owners.join(" ") }) : null,
179
+ ]);
180
+ var body = el("div", { className: "file-body" });
181
+ var unplaced = mine.filter(function (x) {
182
+ return !x.line || !f.hunks.some(function (h) {
183
+ return h.lines.some(function (l) {
184
+ return l.n === x.line;
185
+ });
186
+ });
187
+ });
188
+ unplaced.forEach(function (x) {
189
+ body.appendChild(findingNode(x, true));
190
+ });
191
+ if (f.binary) body.appendChild(el("div", { className: "muted pad", text: "二进制文件,不显示内容。" }));
192
+ f.hunks.forEach(function (h) {
193
+ var table = el("table", { className: "diff" });
194
+ table.appendChild(el("tr", { className: "hunk" }, [el("td", { colspan: "3", text: h.header })]));
195
+ h.lines.forEach(function (l) {
196
+ var line = l.n || l.o;
197
+ var row = el("tr", { className: "line " + (l.t === "+" ? "add" : l.t === "-" ? "del" : ""), "data-file": f.path, "data-n": l.n || "", title: state.info.readOnly ? "" : "点这一行写意见" }, [
198
+ el("td", { className: "num", text: l.o === null ? "" : String(l.o) }),
199
+ el("td", { className: "num", text: l.n === null ? "" : String(l.n) }),
200
+ el("td", { className: "code", text: (l.t === " " ? " " : l.t) + " " + l.s }),
201
+ ]);
202
+ if (!state.info.readOnly) {
203
+ row.addEventListener("click", function () {
204
+ state.open = f.path + ":" + line;
205
+ render();
206
+ });
207
+ }
208
+ table.appendChild(row);
209
+ mine
210
+ .filter(function (x) {
211
+ return x.line && l.n === x.line;
212
+ })
213
+ .forEach(function (x) {
214
+ table.appendChild(el("tr", { className: "finding-row" }, [el("td", { colspan: "3" }, [findingNode(x, true)])]));
215
+ });
216
+ var hasComment = state.comments.some(function (c) {
217
+ return c.file === f.path && c.line === line;
218
+ });
219
+ if (hasComment || state.open === f.path + ":" + line) table.appendChild(commentBox(f.path, line));
220
+ });
221
+ body.appendChild(table);
222
+ });
223
+ if (f.truncated) body.appendChild(el("div", { className: "muted pad", text: "改动太多,只显示了一部分。完整改动请在开发工具里看。" }));
224
+ var box = el("section", { className: "file" + (f.doc && !mine.length ? " folded" : ""), id: "file-" + f.path }, [head, body]);
225
+ return box;
226
+ }
227
+
228
+ function render() {
229
+ var info = state.info;
230
+ var byFile = {};
231
+ info.findings.forEach(function (x) {
232
+ if (x.file && info.files.some(function (f) {
233
+ return f.path === x.file;
234
+ })) (byFile[x.file] = byFile[x.file] || []).push(x);
235
+ });
236
+ var folded = {};
237
+ document.querySelectorAll(".file").forEach(function (n) {
238
+ folded[n.id] = n.classList.contains("folded");
239
+ });
240
+ var diffs = $("diffs");
241
+ var y = window.scrollY;
242
+ diffs.textContent = "";
243
+ if (!info.files.length) diffs.appendChild(el("div", { className: "card muted", text: info.base.commit ? "没有找到改动。" : "找不到这个需求是从哪个提交开始的,没法列出改动。" }));
244
+ info.files.forEach(function (f) {
245
+ var node = renderFile(f, byFile);
246
+ if (node.id in folded) node.classList.toggle("folded", folded[node.id]);
247
+ diffs.appendChild(node);
248
+ });
249
+ // The AI findings not on a changed file: listed at the top.
250
+ var ai = $("ai");
251
+ ai.textContent = "";
252
+ var elsewhere = info.findings.filter(function (x) {
253
+ return !byFile[x.file] || byFile[x.file].indexOf(x) < 0;
254
+ });
255
+ if (info.findings.length) {
256
+ ai.hidden = false;
257
+ var open = info.findings.filter(function (x) {
258
+ return x.status === "open";
259
+ }).length;
260
+ ai.appendChild(el("h2", { text: "AI 评审意见(" + info.findings.length + " 条,未处理 " + open + " 条)" }));
261
+ ai.appendChild(el("p", { className: "muted small", text: "在代码旁边也能看到。阻塞、重要的未处理意见建议标为要改;拿不准的请开发同事看一下。" }));
262
+ info.findings.forEach(function (x) {
263
+ if (elsewhere.indexOf(x) >= 0 || x.severity === "阻塞" || x.severity === "重要") ai.appendChild(findingNode(x, false));
264
+ });
265
+ } else if (info.rounds.length) {
266
+ ai.hidden = false;
267
+ ai.appendChild(el("h2", { text: "AI 评审" }));
268
+ ai.appendChild(el("p", { className: "muted", text: "AI 评审没有留下结构化的意见,详细内容见 code-review.md。" }));
269
+ }
270
+ var nav = $("files");
271
+ nav.textContent = "";
272
+ nav.appendChild(el("div", { className: "nav-title", text: "改动的文件(" + info.files.length + ")" }));
273
+ info.files.forEach(function (f) {
274
+ nav.appendChild(el("a", { href: "#", className: f.doc ? "doc" : "", onclick: function (e) {
275
+ e.preventDefault();
276
+ jump(f.path, null);
277
+ } }, [el("span", { text: f.path }), (byFile[f.path] || []).length ? el("span", { className: "badge warn", text: String(byFile[f.path].length) }) : null]));
278
+ });
279
+ window.scrollTo(0, y);
280
+ count();
281
+ }
282
+
283
+ function submit(verdict) {
284
+ if (state.busy) return;
285
+ var text = verdict === "approve" ? "确认评审通过?会记入 code-review.md 并提交。" : "确认退回修改?流水线会按这些意见修改(需求说明不变),改完再评审、再验证。";
286
+ if (!window.confirm(text)) return;
287
+ state.busy = true;
288
+ count();
289
+ api("api/submit", "POST", {
290
+ verdict: verdict,
291
+ reviewer: state.reviewer,
292
+ reviewers: reviewersList(),
293
+ comments: state.comments.filter(function (c) {
294
+ return c.text.trim();
295
+ }),
296
+ fix: fixes(),
297
+ note: state.note,
298
+ })
299
+ .then(function (r) {
300
+ toast(r.message);
301
+ return load();
302
+ })
303
+ .catch(function (e) {
304
+ toast(e.message);
305
+ })
306
+ .then(function () {
307
+ state.busy = false;
308
+ count();
309
+ });
310
+ }
311
+
312
+ function load() {
313
+ return api("api/info").then(function (info) {
314
+ state.info = info;
315
+ $("project").textContent = info.featureDir;
316
+ $("title").textContent = "代码审查:" + info.title;
317
+ var adds = 0,
318
+ dels = 0;
319
+ info.files.forEach(function (f) {
320
+ if (!f.doc) {
321
+ adds += f.additions;
322
+ dels += f.deletions;
323
+ }
324
+ });
325
+ var last = info.rounds.length ? info.rounds[info.rounds.length - 1] : null;
326
+ $("sub").textContent =
327
+ (info.branch ? "分支 " + info.branch : "") +
328
+ (info.base.ref ? ",相对 " + info.base.ref : "") +
329
+ ":改了 " + info.files.filter(function (f) {
330
+ return !f.doc;
331
+ }).length + " 个代码文件(+" + adds + " −" + dels + ")" +
332
+ (last ? ";AI 评审 " + info.rounds.length + " 轮,最后一轮" + (last.verdict === "pass" ? "通过" : "需要修改") : "");
333
+ var banner = $("status");
334
+ banner.hidden = true;
335
+ banner.className = "banner";
336
+ if (info.running) {
337
+ banner.hidden = false;
338
+ banner.textContent = "流水线正在运行,改动还会变化。可以先看,结束后再提交评审。";
339
+ } else if (info.human) {
340
+ banner.hidden = false;
341
+ var h = info.human;
342
+ if (h.status === "approved") {
343
+ banner.className = "banner " + (h.stale ? "warn" : "ok");
344
+ banner.textContent = (h.stale ? "评审通过后代码又有改动,需要重新评审。" : "已评审通过") + "(" + h.reviewer + "," + h.at.slice(0, 10) + (h.reviewers.length ? ",审核人 " + h.reviewers.join(" ") : "") + ")";
345
+ } else {
346
+ banner.className = "banner warn";
347
+ banner.textContent = "上次退回了 " + h.items.length + " 条意见(" + h.reviewer + "," + h.at.slice(0, 10) + ")" + (h.consumedAttempt ? ",第 " + h.consumedAttempt + " 次尝试在处理" : ",还没开始修改");
348
+ }
349
+ }
350
+ var sum = $("summary");
351
+ sum.textContent = "";
352
+ if (info.summary.length) {
353
+ sum.hidden = false;
354
+ sum.appendChild(el("h2", { text: "这次改了什么(来自提测说明)" }));
355
+ info.summary.forEach(function (l) {
356
+ sum.appendChild(el("div", { text: l.replace(/^\s*[-*]\s+/, "• ") }));
357
+ });
358
+ }
359
+ var d = info.draft || {};
360
+ state.comments = Array.isArray(d.comments) ? d.comments : [];
361
+ state.decisions = d.decisions && typeof d.decisions === "object" ? d.decisions : {};
362
+ state.reviewer = d.reviewer || "";
363
+ state.reviewers = typeof d.reviewers === "string" ? d.reviewers : info.reviewers.suggested.join(", ");
364
+ state.note = d.note || "";
365
+ $("reviewer").value = state.reviewer;
366
+ $("reviewers").value = state.reviewers;
367
+ $("reviewers").title = info.reviewers.source ? "默认取自 " + info.reviewers.source + ",可以改" : "项目里没有 CODEOWNERS,请填写";
368
+ $("note").value = state.note;
369
+ if (info.readOnly) ["reviewer", "reviewers", "note"].forEach(function (id) {
370
+ $(id).disabled = true;
371
+ });
372
+ render();
373
+ });
374
+ }
375
+
376
+ function start() {
377
+ load().catch(function (e) {
378
+ $("title").textContent = "打不开代码审查页";
379
+ $("sub").textContent = e.message;
380
+ });
381
+ $("reviewer").addEventListener("input", function (e) {
382
+ state.reviewer = e.target.value;
383
+ save();
384
+ });
385
+ $("reviewers").addEventListener("input", function (e) {
386
+ state.reviewers = e.target.value;
387
+ save();
388
+ });
389
+ $("note").addEventListener("input", function (e) {
390
+ state.note = e.target.value;
391
+ save();
392
+ });
393
+ $("changes").addEventListener("click", function () {
394
+ submit("changes");
395
+ });
396
+ $("approve").addEventListener("click", function () {
397
+ submit("approve");
398
+ });
399
+ }
400
+
401
+ start();
402
+ })();
@@ -15,8 +15,9 @@ import os from "node:os";
15
15
  import http from "node:http";
16
16
  import path from "node:path";
17
17
  import { ENGINE_DIR, KIT_ROOT, kitHome, kitVersion } from "./locations.js";
18
+ import { buildCodeReview, recordHumanReview, startReviewFix, type Submission } from "./code-review.js";
18
19
  import { type Answer, acceptanceFailMessage, acceptanceText, buildChecklist, recordAcceptance, verdictOf } from "./checklist.js";
19
- import { readIndex } from "./records.js";
20
+ import { appendEvent, readIndex, requirementRef } from "./records.js";
20
21
  import { readState, statePath } from "./state.js";
21
22
 
22
23
  const ASSETS = path.join(ENGINE_DIR, "src", "review-assets");
@@ -204,6 +205,8 @@ export interface ReviewServerOptions {
204
205
  env?: NodeJS.ProcessEnv;
205
206
  /** Runs `req approve|reply …` in the requirement's checkout; tests replace it. */
206
207
  run?: (command: "approve" | "reply", repo: string, featureDir: string, extra: string[]) => { ok: boolean; message: string };
208
+ /** 1.14: `req review-fix <需求>` after the code review page asked for changes; tests replace it. */
209
+ codeFix?: (repo: string, featureDir: string) => { ok: boolean; message: string };
207
210
  /** 1.14: `req try <需求> --json [--stop]` for the checklist page's "试用" button; tests replace it. */
208
211
  tryRun?: (repo: string, featureDir: string, stop: boolean) => { ok: boolean; message: string; services?: unknown[]; howTo?: string[] };
209
212
  onRequest?: () => void;
@@ -329,6 +332,7 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
329
332
  const env = opts.env ?? process.env;
330
333
  const run = opts.run ?? defaultRun;
331
334
  const tryRun = opts.tryRun ?? defaultTry;
335
+ const codeFixRun = opts.codeFix ?? startReviewFix;
332
336
  return http.createServer((req, res) => {
333
337
  opts.onRequest?.();
334
338
  handle(req, res).catch((error: Error) => json(res, 500, { ok: false, message: error.message }));
@@ -359,6 +363,11 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
359
363
  return res.end();
360
364
  }
361
365
  if (rest.startsWith("check/")) return handleChecklist(req, res, rest.slice("check/".length), entry, featureAbs, full);
366
+ if (rest === "code") {
367
+ res.writeHead(301, { location: `${url.pathname}/` });
368
+ return res.end();
369
+ }
370
+ if (rest.startsWith("code/")) return handleCodeReview(req, res, rest.slice("code/".length), entry, featureAbs, full);
362
371
  if (rest === "") return sendFile(res, path.join(ASSETS, "app.html"));
363
372
  if (rest === "app.js" || rest === "app.css") return sendFile(res, path.join(ASSETS, rest));
364
373
  if (rest.startsWith("proto/cur/")) return sendFile(res, inside(prototypeDir, rest.slice("proto/cur/".length)), PROTOTYPE_CSP);
@@ -428,6 +437,63 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
428
437
  return send(res, 404, "没有这个页面");
429
438
  }
430
439
 
440
+ // 1.14: the code review page, /r/<token>/f/<key>/code/…
441
+ async function handleCodeReview(req: http.IncomingMessage, res: http.ServerResponse, rest: string, entry: { repo: string; featureDir: string }, featureAbs: string, full: boolean) {
442
+ const draftFile = path.join(featureAbs, ".pipeline", "code-review", "draft.json");
443
+ if (rest === "") return sendFile(res, path.join(ASSETS, "code.html"));
444
+ if (rest === "code.js" || rest === "code.css") return sendFile(res, path.join(ASSETS, rest));
445
+ if (rest === "api/info" && req.method === "GET") {
446
+ let draft: unknown = null;
447
+ try {
448
+ draft = JSON.parse(fs.readFileSync(draftFile, "utf8"));
449
+ } catch {
450
+ draft = null;
451
+ }
452
+ return json(res, 200, { ...buildCodeReview(entry.repo, entry.featureDir), readOnly: !full, draft });
453
+ }
454
+ if (rest === "api/draft" && req.method === "PUT") {
455
+ if (!full) return json(res, 403, { ok: false, message: "这是只读链接" });
456
+ const body = await readBody(req);
457
+ if (!body || typeof body !== "object") return json(res, 400, { ok: false, message: "格式不对" });
458
+ fs.mkdirSync(path.dirname(draftFile), { recursive: true });
459
+ fs.writeFileSync(draftFile, JSON.stringify({ ...(body as object), savedAt: new Date().toISOString() }, null, 2));
460
+ return json(res, 200, { ok: true });
461
+ }
462
+ if (rest === "api/submit" && req.method === "POST") {
463
+ if (!full) return json(res, 403, { ok: false, message: "这是只读链接,不能提交" });
464
+ const body = (await readBody(req)) as Partial<Submission> | null;
465
+ const verdict = body?.verdict === "approve" ? "approve" : body?.verdict === "changes" ? "changes" : null;
466
+ if (!verdict) return json(res, 400, { ok: false, message: "请选择评审通过或退回修改" });
467
+ const data = buildCodeReview(entry.repo, entry.featureDir);
468
+ if (data.running) return json(res, 409, { ok: false, message: "流水线正在运行,等它结束后再提交评审。" });
469
+ const sub: Submission = {
470
+ verdict,
471
+ reviewer: typeof body?.reviewer === "string" ? body.reviewer : "",
472
+ reviewers: Array.isArray(body?.reviewers) ? body.reviewers.filter((r): r is string => typeof r === "string") : [],
473
+ comments: (Array.isArray(body?.comments) ? body.comments : []).filter((c) => c && typeof c.text === "string").map((c) => ({ file: typeof c.file === "string" ? c.file : null, line: typeof c.line === "number" ? c.line : null, text: c.text })),
474
+ fix: (Array.isArray(body?.fix) ? body.fix : []).filter((f) => f && typeof f.id === "string").map((f) => ({ id: f.id, note: typeof f.note === "string" ? f.note : undefined })),
475
+ note: typeof body?.note === "string" ? body.note : "",
476
+ };
477
+ if (verdict === "changes" && sub.comments.length === 0 && sub.fix.length === 0 && !sub.note.trim()) {
478
+ return json(res, 400, { ok: false, message: "退回修改需要至少一条意见:在代码行上写意见,或把 AI 的意见标为\"要改\"。" });
479
+ }
480
+ const { review, committed } = recordHumanReview(entry.repo, entry.featureDir, sub);
481
+ if (verdict === "approve") {
482
+ return json(res, 200, { ok: true, verdict, committed, message: `已记录:评审通过${committed ? ",已写入 code-review.md 并提交" : ""}。${review.reviewers.length ? `审核人:${review.reviewers.join("、")}。` : ""}合并请求接入后会用这些审核人。` });
483
+ }
484
+ const started = codeFixRun(entry.repo, entry.featureDir);
485
+ return json(res, 200, {
486
+ ok: true,
487
+ verdict,
488
+ committed,
489
+ message: started.ok
490
+ ? `已记录 ${review.items.length} 条修改意见,流水线开始按意见修改(需求说明不变,从方案设计开始只处理这些意见),进展会发回需求的对话。`
491
+ : `已记录 ${review.items.length} 条修改意见,但没能开始修改:${started.message.split("\n")[0]}。可以在需求的对话里说"按代码评审意见修改",或执行 req review-fix ${path.basename(entry.featureDir)}。`,
492
+ });
493
+ }
494
+ return send(res, 404, "没有这个页面");
495
+ }
496
+
431
497
  // 1.14: the acceptance checklist, /r/<token>/f/<key>/check/…
432
498
  async function handleChecklist(req: http.IncomingMessage, res: http.ServerResponse, rest: string, entry: { repo: string; featureDir: string; key: string; project: string }, featureAbs: string, full: boolean) {
433
499
  const checkDir = path.join(featureAbs, ".pipeline", "check");
@@ -480,6 +546,20 @@ export function createReviewServer(opts: ReviewServerOptions): http.Server {
480
546
  const text = acceptanceText(list, answers, { verdict: v.verdict, reviewer: reviewer.slice(0, 60), date: new Date().toISOString().slice(0, 10), previous });
481
547
  const { committed } = recordAcceptance(entry.repo, entry.featureDir, text);
482
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
+ });
483
563
  if (v.verdict === "通过") {
484
564
  return json(res, 200, { ok: true, verdict: v.verdict, committed, message: `已记录:验收通过${committed ? ",验收记录已提交" : ""}。下一步请开发同事审阅代码并合并。` });
485
565
  }
@@ -641,7 +721,23 @@ async function main(argv: string[]): Promise<number> {
641
721
  }
642
722
  const open = !argv.includes("--no-open");
643
723
  const readOnly = argv.includes("--read-only");
644
- const feature = argv.find((a) => !a.startsWith("--"));
724
+ // 1.14: --page code opens the code review page of the same server.
725
+ const pageAt = argv.indexOf("--page");
726
+ const page = pageAt >= 0 ? argv[pageAt + 1] : null;
727
+ const feature = argv.find((a, i) => !a.startsWith("--") && !(pageAt >= 0 && i === pageAt + 1));
728
+ if (page === "code" && feature && !argv.includes("--help")) {
729
+ const { locate } = await import("./api.js");
730
+ const { repo, featureDir } = locate(process.cwd(), feature);
731
+ const key = keyOf(repo, featureDir);
732
+ if (!key) {
733
+ console.error(`${featureDir} 不在本机的需求列表中,先运行一次 req index --rebuild`);
734
+ return 1;
735
+ }
736
+ const url = `${reviewUrl(await ensureReviewServer(), key, readOnly)}code/`;
737
+ console.log(`代码审查页:${url}`);
738
+ if (open) openBrowser(url);
739
+ return 0;
740
+ }
645
741
  if (!feature || argv.includes("--help") || argv.includes("-h")) {
646
742
  console.log("用法:req review <需求> [--no-open] [--read-only]\n\n在浏览器中打开这个需求的原型审查页:点选要改的地方、直接改文字,然后提交修改意见或确认方案。");
647
743
  return feature ? 0 : 2;
@@ -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. */