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.
- package/CHANGELOG.md +32 -0
- package/bin/req.mjs +14 -0
- package/contracts/cli-json.md +1 -1
- 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 +11 -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/prompts/06-code-review.md +12 -1
- package/engine/prompts/incremental.md +2 -0
- package/engine/src/activity.ts +21 -0
- package/engine/src/api.ts +44 -7
- package/engine/src/cli-args.ts +4 -1
- package/engine/src/code-review.ts +422 -0
- package/engine/src/codeowners.ts +87 -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/main.ts +13 -3
- 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/preflight.ts +10 -1
- package/engine/src/records.ts +71 -0
- package/engine/src/reply.ts +30 -11
- package/engine/src/review-assets/code.css +93 -0
- package/engine/src/review-assets/code.html +43 -0
- package/engine/src/review-assets/code.js +402 -0
- package/engine/src/review.ts +98 -2
- 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/data.ts +2 -2
- package/paseo-plugin/client/detail.tsx +7 -2
- package/paseo-plugin/client/nav.ts +5 -0
- package/paseo-plugin/client/review.tsx +16 -6
- package/paseo-plugin/client/stats.tsx +15 -0
- package/paseo-plugin/package.json +1 -1
- package/paseo-plugin/server/handlers.ts +1 -1
- package/paseo-plugin/shared/contracts.ts +11 -2
- package/site-dist/404.html +2 -2
- package/site-dist/assets/{app.BpC99f5a.js → app.TlOHPtfE.js} +1 -1
- package/site-dist/assets/changelog.md.DiGi8kHU.js +1 -0
- package/site-dist/assets/{changelog.md.m4cSU5e4.lean.js → changelog.md.DiGi8kHU.lean.js} +1 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.Dk_-Yka5.js +1 -0
- package/site-dist/assets/chunks/{VPLocalSearchBox.CI43rhTA.js → VPLocalSearchBox.BUVNy7_o.js} +1 -1
- package/site-dist/assets/chunks/{theme.S6pNEF23.js → theme.BFeiG1US.js} +2 -2
- package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.js → guide_getting-started.md.UhkLCUoY.js} +1 -1
- package/site-dist/assets/{guide_getting-started.md.a7iNP1Ky.lean.js → guide_getting-started.md.UhkLCUoY.lean.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.D9GpGJ7y.js → guide_user.md.UV0WxdJa.js} +1 -1
- package/site-dist/assets/{guide_user.md.D9GpGJ7y.lean.js → guide_user.md.UV0WxdJa.lean.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.js → reference_contracts_cli-json.md.CCvKLasM.js} +1 -1
- package/site-dist/assets/{reference_contracts_cli-json.md.BV1HtTmr.lean.js → reference_contracts_cli-json.md.CCvKLasM.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 +5 -5
- 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 +2 -0
- package/skills/req/playbooks/incident.md +1 -1
- package/skills/spec-writing/SKILL.md +2 -1
- package/site-dist/assets/changelog.md.m4cSU5e4.js +0 -1
- package/site-dist/assets/chunks/@localSearchIndexroot.D6ywcTJ5.js +0 -1
- package/site-dist/assets/reference_contracts_state.md.EEBd_l2P.js +0 -1
- /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
|
+
})();
|
package/engine/src/review.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
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. */
|