@shiyuphan/follow-me 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -55
- package/dist/src/cli.js +740 -194
- package/dist/src/comments.js +109 -6
- package/dist/src/diff.js +11 -0
- package/dist/src/evolve.js +123 -0
- package/dist/src/git.js +12 -0
- package/dist/src/host.js +39 -8
- package/dist/src/install.js +21 -0
- package/dist/src/plan.js +1 -1
- package/dist/src/prompt.js +384 -51
- package/dist/src/server.js +195 -67
- package/dist/src/session.js +53 -12
- package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-byISxePP.js +1 -0
- package/dist/ui/dist/assets/arc-CBzsAsP2.js +1 -0
- package/dist/ui/dist/assets/architecture-WOLXFQ4H-0OSkIwdB.js +1 -0
- package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-ixHybNqO.js +36 -0
- package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-1F8XL_Ou.js +140 -0
- package/dist/ui/dist/assets/c4Diagram-XDPPPE54-D9PFBXnc.js +38 -0
- package/dist/ui/dist/assets/channel-B-fwsHf1.js +1 -0
- package/dist/ui/dist/assets/chunk-2E7WHVMD-zZdNfMkj.js +88 -0
- package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/dist/assets/chunk-4EMNKR2T-CTKoxSSj.js +1 -0
- package/dist/ui/dist/assets/chunk-55XCU76K-ifFnDlcS.js +1 -0
- package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/dist/assets/chunk-7S6XSTFO-BCMVdi_1.js +2 -0
- package/dist/ui/dist/assets/chunk-AQDTSKZB-DC-5Whju.js +72 -0
- package/dist/ui/dist/assets/chunk-DSNLSHJZ-DASbe_gb.js +127 -0
- package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/ui/dist/assets/chunk-HAGFOTED-CArytDjj.js +62 -0
- package/dist/ui/dist/assets/chunk-HNE4Q36P-Dnl-Wtud.js +1 -0
- package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/dist/ui/dist/assets/chunk-NQOKRVIF-wvSLaeaK.js +213 -0
- package/dist/ui/dist/assets/chunk-UHW7PFBP-CckaOGiQ.js +1 -0
- package/dist/ui/dist/assets/chunk-VQ5W62QI-kQBMDJ7V.js +1 -0
- package/dist/ui/dist/assets/chunk-WQQL4CDK-CMg6bbLU.js +10 -0
- package/dist/ui/dist/assets/chunk-XXDRQBXY-CYlFr19K.js +1 -0
- package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/dist/assets/chunk-YZO67HFG-CqkG1vVk.js +1 -0
- package/dist/ui/dist/assets/chunk-ZAUX2LTX-BUNEdx-e.js +1 -0
- package/dist/ui/dist/assets/chunk-ZCHMQSOR-CFTbvcrm.js +2 -0
- package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-t2OdJ0LA.js +217 -0
- package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-FGm030gc.js +1 -0
- package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtU33Lsu.js +1 -0
- package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUr-I7hS.js +62 -0
- package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/ui/dist/assets/dagre-NK4GIZVX-BWTHwJKt.js +4 -0
- package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/dist/assets/diagram-CBK3MWTC-PcHrsEBn.js +24 -0
- package/dist/ui/dist/assets/diagram-I5XIIV2D-CkGtKjYj.js +24 -0
- package/dist/ui/dist/assets/diagram-ODIL2UCW-B_HdBB1w.js +30 -0
- package/dist/ui/dist/assets/diagram-PFBEJUXE-DL9zayE6.js +3 -0
- package/dist/ui/dist/assets/diagram-RJA3I4VV-By5wl8nK.js +41 -0
- package/dist/ui/dist/assets/diagram-TYUNFSNR-ziWPVayL.js +200 -0
- package/dist/ui/dist/assets/dist-NDVqCM7k.js +1 -0
- package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-DH4YGkPc.js +1 -0
- package/dist/ui/dist/assets/elk-QONTBCTQ-BL2RlCEv.js +27 -0
- package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BKbTMaRp.js +99 -0
- package/dist/ui/dist/assets/eventmodeling-K75KTNOO-Dk5RCWc-.js +1 -0
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +1 -0
- package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-3vM9gxeT.js +292 -0
- package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/dist/assets/gitGraph-VSP46ZUC-CXGFhz54.js +1 -0
- package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js +106 -0
- package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/dist/assets/index-B8TYcxF8.css +1 -0
- package/dist/ui/dist/assets/index-CBbx_pJk.js +134 -0
- package/dist/ui/dist/assets/info-OHQRW6UA-DfSsZ8ib.js +1 -0
- package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-B_7tyrL5.js +2 -0
- package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-DUvHljpg.js +70 -0
- package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-BCgqzrb8.js +139 -0
- package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-DOzZ3NmE.js +89 -0
- package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/ui/dist/assets/line-B-mPFVll.js +1 -0
- package/dist/ui/dist/assets/linear-k7o0jout.js +1 -0
- package/dist/ui/dist/assets/mermaid-parser.core-CAyLSQpH.js +7 -0
- package/dist/ui/dist/assets/mermaid.core-vfVMjgx2.js +44 -0
- package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-B6y-B_r4.js +98 -0
- package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/dist/assets/packet-JDAUHWVQ-C_r4ed30.js +1 -0
- package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-B8VUm3Bh.js +1 -0
- package/dist/ui/dist/assets/pie-XZMESJXO-DcwtbH9y.js +1 -0
- package/dist/ui/dist/assets/pieDiagram-GNMPY74T-CvUNBi2F.js +39 -0
- package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
- package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
- package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
- package/dist/ui/dist/assets/prism-core-_2ju85Ya.js +1 -0
- package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
- package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
- package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
- package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
- package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
- package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
- package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
- package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
- package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
- package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
- package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
- package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
- package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
- package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
- package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
- package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
- package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
- package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
- package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
- package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
- package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
- package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-Dg2eVS3j.js +7 -0
- package/dist/ui/dist/assets/radar-ABXABTNO-xCNTcZE9.js +1 -0
- package/dist/ui/dist/assets/railroad-I3PHUGI6-BI-sIL2Q.js +1 -0
- package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-Ddvk40Wx.js +1 -0
- package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-h35gE183.js +1 -0
- package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-Zim9iPdp.js +1 -0
- package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-Cz6fdmOQ.js +1 -0
- package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-DTJkCrcD.js +84 -0
- package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-Bg1iiVla.js +40 -0
- package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-9XfetsRA.js +169 -0
- package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/ui/dist/assets/src-Dym5afQ5.js +1 -0
- package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-CMmcMV8z.js +291 -0
- package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-B2PcshTY.js +1 -0
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +8 -0
- package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-Cr1hCNtW.js +120 -0
- package/dist/ui/dist/assets/treeView-D4JQ5SDB-jH5ksKnh.js +1 -0
- package/dist/ui/dist/assets/treemap-SAJKECNS-BV90e8QL.js +1 -0
- package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-BNwq5aJV.js +328 -0
- package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D-lLyZCL.js +34 -0
- package/dist/ui/dist/assets/wardley-7MLQ67FV-CWfHQUGt.js +1 -0
- package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-BL_qNKVd.js +78 -0
- package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-BHPK5Snp.js +7 -0
- package/dist/ui/dist/index.html +15 -0
- package/dist/ui/dist/style.css +121 -0
- package/package.json +31 -10
- package/skills/follow-me/SKILL.md +19 -89
- package/dist/src/anthropic.js +0 -133
- package/dist/ui/app.js +0 -2201
- package/dist/ui/concerns.js +0 -165
- package/dist/ui/highlight.js +0 -221
- package/dist/ui/index.html +0 -94
- package/dist/ui/live.js +0 -184
- package/dist/ui/playback.js +0 -579
- package/dist/ui/questions.js +0 -189
- package/dist/ui/style.css +0 -1331
- package/dist/ui/summary.js +0 -45
package/dist/src/prompt.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tipStartOf } from "./diff.js";
|
|
1
2
|
/** Consecutive runs collapse: [0,1,2,3,7] becomes "0-3,7". */
|
|
2
3
|
function formatRanges(changes) {
|
|
3
4
|
const sorted = [...new Set(changes)].sort((a, b) => a - b);
|
|
@@ -53,7 +54,7 @@ function renderPreviousPlan(plan) {
|
|
|
53
54
|
})
|
|
54
55
|
.join("\n");
|
|
55
56
|
}
|
|
56
|
-
function describeAnchor(anchor) {
|
|
57
|
+
function describeAnchor(anchor, ctx) {
|
|
57
58
|
switch (anchor.kind) {
|
|
58
59
|
case "plan":
|
|
59
60
|
return "the whole plan";
|
|
@@ -62,7 +63,16 @@ function describeAnchor(anchor) {
|
|
|
62
63
|
case "position":
|
|
63
64
|
return `the position of commit ${anchor.commit + 1} (the reviewer thinks it belongs elsewhere)`;
|
|
64
65
|
case "hunk": {
|
|
65
|
-
const
|
|
66
|
+
const pick = ctx === null ? null : pickRows(ctx, anchor);
|
|
67
|
+
if (pick !== null) {
|
|
68
|
+
const rows = formatRanges(pick.offsets);
|
|
69
|
+
return `${pick.label} in commit ${anchor.commit + 1} (${anchor.unit} rows ${rows}; ${pick.counts})`;
|
|
70
|
+
}
|
|
71
|
+
const where = anchor.offsets !== undefined
|
|
72
|
+
? `rows ${formatRanges(anchor.offsets)}`
|
|
73
|
+
: anchor.changes === null
|
|
74
|
+
? "the whole unit"
|
|
75
|
+
: `change lines ${formatRanges(anchor.changes)}`;
|
|
66
76
|
return `${anchor.unit} (${where}) in commit ${anchor.commit + 1}`;
|
|
67
77
|
}
|
|
68
78
|
default: {
|
|
@@ -71,8 +81,92 @@ function describeAnchor(anchor) {
|
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
/** The hunk around the picked rows with the picked ones marked `>`; empty without offsets. */
|
|
85
|
+
function anchorRows(ctx, anchor) {
|
|
86
|
+
return (ctx === null ? null : pickRows(ctx, anchor))?.rows ?? [];
|
|
87
|
+
}
|
|
88
|
+
const PICK_MARGIN = 3;
|
|
89
|
+
function plural(n, noun) {
|
|
90
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolves offsets against the hunk. Line numbers are the branch tip's, or the base's when a
|
|
94
|
+
* picked row is a removal, since a removed line has no tip number. Rows are shown as the commit
|
|
95
|
+
* writes them: its own changes marked +/-, everything else as context.
|
|
96
|
+
*/
|
|
97
|
+
function pickRows(ctx, anchor) {
|
|
98
|
+
if (anchor.kind !== "hunk" || anchor.offsets === undefined)
|
|
99
|
+
return null;
|
|
100
|
+
const hunk = ctx.units.get(anchor.unit);
|
|
101
|
+
const { offsets } = anchor;
|
|
102
|
+
const first = offsets[0];
|
|
103
|
+
const last = offsets.at(-1);
|
|
104
|
+
if (hunk?.kind !== "hunk" || first === undefined || last === undefined)
|
|
105
|
+
return null;
|
|
106
|
+
if (last >= hunk.body.length)
|
|
107
|
+
return null;
|
|
108
|
+
// Without the projected commit, the hunk is quoted as git shows it.
|
|
109
|
+
const projected = anchor.commit < ctx.commits.length;
|
|
110
|
+
const owners = ownersOf(ctx.commits, hunk);
|
|
111
|
+
const picked = new Set(offsets);
|
|
112
|
+
let tip = tipStartOf(hunk, ctx.units);
|
|
113
|
+
let base = hunk.oldStart + 1;
|
|
114
|
+
const rows = [];
|
|
115
|
+
const lines = [];
|
|
116
|
+
hunk.body.forEach((line, i) => {
|
|
117
|
+
lines.push({ op: line.op, tip, base });
|
|
118
|
+
let op = line.op;
|
|
119
|
+
let shown = true;
|
|
120
|
+
if (projected && line.op !== " ") {
|
|
121
|
+
const owner = owners[line.change] ?? null;
|
|
122
|
+
const applied = owner !== null && owner < anchor.commit;
|
|
123
|
+
if (owner !== anchor.commit) {
|
|
124
|
+
shown = line.op === "+" ? applied : !applied;
|
|
125
|
+
op = " ";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (picked.has(i) && !shown) {
|
|
129
|
+
shown = true;
|
|
130
|
+
op = line.op;
|
|
131
|
+
}
|
|
132
|
+
const inWindow = i >= first - PICK_MARGIN && i <= last + PICK_MARGIN;
|
|
133
|
+
if (shown && inWindow)
|
|
134
|
+
rows.push(`${picked.has(i) ? ">" : " "} ${op}${stripEol(line.text)}`);
|
|
135
|
+
if (line.op !== "-")
|
|
136
|
+
tip += 1;
|
|
137
|
+
if (line.op !== "+")
|
|
138
|
+
base += 1;
|
|
139
|
+
});
|
|
140
|
+
const chosen = offsets.flatMap((i) => lines[i] ?? []);
|
|
141
|
+
const removal = chosen.some((l) => l.op === "-");
|
|
142
|
+
const numbers = removal
|
|
143
|
+
? chosen.filter((l) => l.op !== "+").map((l) => l.base)
|
|
144
|
+
: chosen.map((l) => l.tip);
|
|
145
|
+
const from = Math.min(...numbers);
|
|
146
|
+
const to = Math.max(...numbers);
|
|
147
|
+
const range = from === to ? `${from}` : `${from}-${to}`;
|
|
148
|
+
const count = (op) => chosen.filter((l) => l.op === op).length;
|
|
149
|
+
const counts = [
|
|
150
|
+
count(" ") === 0 ? null : plural(count(" "), "context line"),
|
|
151
|
+
count("+") === 0 ? null : `${count("+")} added`,
|
|
152
|
+
count("-") === 0 ? null : `${count("-")} removed`,
|
|
153
|
+
].filter((part) => part !== null);
|
|
154
|
+
return {
|
|
155
|
+
offsets,
|
|
156
|
+
label: `${hunk.path}:${range}${removal ? " (base side)" : ""}`,
|
|
157
|
+
range,
|
|
158
|
+
baseSide: removal,
|
|
159
|
+
line: from,
|
|
160
|
+
counts: counts.join(", "),
|
|
161
|
+
rows,
|
|
162
|
+
touchesContext: count(" ") > 0,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Said once wherever comments reach the agent, with the same words. */
|
|
166
|
+
const CONTEXT_RULE = "A comment on a context row (marked `>` with no + or -) is about code this branch leaves alone or depends on. The plan cannot move it, so do not move lines because of it. Answer a question with an explanation, or point at the commit that last touched those lines. Answer a change request as a code issue, not by reordering.";
|
|
167
|
+
function renderComment(comment, ctx) {
|
|
168
|
+
const lines = [`- ${comment.id} on ${describeAnchor(comment.anchor, ctx)}`];
|
|
169
|
+
const rows = anchorRows(ctx, comment.anchor);
|
|
76
170
|
if (comment.follows)
|
|
77
171
|
lines.push(` asks again after ${comment.follows}`);
|
|
78
172
|
if (comment.context.commitMessage) {
|
|
@@ -80,7 +174,10 @@ function renderComment(comment) {
|
|
|
80
174
|
}
|
|
81
175
|
if (comment.context.path)
|
|
82
176
|
lines.push(` path: ${comment.context.path}`);
|
|
83
|
-
if (
|
|
177
|
+
if (rows.length > 0) {
|
|
178
|
+
lines.push(" rows asked about, picked ones marked >:", ...rows.map((r) => ` ${r}`));
|
|
179
|
+
}
|
|
180
|
+
else if (comment.context.lines && comment.context.lines.length > 0) {
|
|
84
181
|
lines.push(" lines asked about:", ...comment.context.lines.map((l) => ` ${l}`));
|
|
85
182
|
}
|
|
86
183
|
if (comment.severity)
|
|
@@ -91,8 +188,8 @@ function renderComment(comment) {
|
|
|
91
188
|
}
|
|
92
189
|
return lines.join("\n");
|
|
93
190
|
}
|
|
94
|
-
const QUESTIONS = `A question is not a request to change the plan. Answer it by explaining, not by moving lines. Where the asked-about lines now live, add or rewrite the \`narration\` of that commit or unit so it answers the question, in plain
|
|
95
|
-
const CONCERNS = `You are also a careful reviewer. Add a concern only for something you would raise in a pull request: a bug or unhandled case, a change with no test where one is expected, a name that misleads, or a risky change such as a migration, concurrency or security. Pin it to the change lines with \`lines\`. Say what goes wrong and when, in one or two sentences. Severity: \`blocker\` breaks behaviour or loses data, \`major\` should be fixed before merge, \`minor\` is worth a note. Do not flag style, formatting or taste. Do not repeat the
|
|
191
|
+
const QUESTIONS = `A question is not a request to change the plan. Answer it by explaining, not by moving lines. Where the asked-about lines now live, add or rewrite the \`narration\` (the page shows it as your note) of that commit or unit so it answers the question, in plain sentences. When the answer is about flow, structure or order across files, add a \`diagram\` there too. Then write one response with \`"status": "answered"\`, a \`note\` of one to three sentences that answers the question directly, and \`"at"\` naming the commit (1-based) and unit that carry the explanation. Move lines only if answering shows the order itself was wrong. Say so in the note, and answer the question as well. If the diff cannot answer it, use \`"deferred"\` and say what is missing.`;
|
|
192
|
+
const CONCERNS = `You are also a careful reviewer. Add a concern only for something you would raise in a pull request: a bug or unhandled case, a change with no test where one is expected, a name that misleads, or a risky change such as a migration, concurrency or security. Pin it to the change lines with \`lines\`. Say what goes wrong and when, in one or two sentences. Severity: \`blocker\` breaks behaviour or loses data, \`major\` should be fixed before merge, \`minor\` is worth a note. Do not flag style, formatting or taste. Do not repeat the note. Most commits deserve no concern, and a plan with none is fine. Never write more than one concern per unit. Concerns are in your voice as a reviewer, while the note stays the author's.
|
|
96
193
|
|
|
97
194
|
A concern is \`{ "severity": "blocker" | "major" | "minor", "kind": "bug" | "missing-test" | "naming" | "risk", "text": "...", "lines": [0, 1] }\`. \`lines\` holds change indices of that unit and is allowed only on a unit ref. Write \`"concerns": [ ... ]\` on a commit, or on a unit written as an object: \`{ "unit": "h_x:0-3", "concerns": [ ... ] }\`. Never write an id; follow-me derives one.`;
|
|
98
195
|
const ORDERING = `Order the commits so the story reads naturally:
|
|
@@ -104,7 +201,8 @@ const ORDERING = `Order the commits so the story reads naturally:
|
|
|
104
201
|
- The last commit must bring the tree to exactly the tip, which follows from covering every unit.
|
|
105
202
|
|
|
106
203
|
Write each commit message as a short imperative title, then a blank line and a body that
|
|
107
|
-
says why.
|
|
204
|
+
says why. Put the why in the body first, before anything else. Keep the title to 50
|
|
205
|
+
characters or fewer; put detail in the body.`;
|
|
108
206
|
const GOALS = {
|
|
109
207
|
split: {
|
|
110
208
|
title: "plan the commits",
|
|
@@ -116,17 +214,16 @@ ${ORDERING}`,
|
|
|
116
214
|
},
|
|
117
215
|
walk: {
|
|
118
216
|
title: "tell the story of this branch",
|
|
119
|
-
goal: `The reader is a reviewer meeting this branch for the first time. They will
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
grouped and ordered, and what the author is thinking while writing them.
|
|
217
|
+
goal: `The reader is a reviewer meeting this branch for the first time. They will read it commit
|
|
218
|
+
by commit and comment on the code itself. The final tree is fixed. You decide how its change
|
|
219
|
+
lines are grouped and ordered, and why each step is there.
|
|
123
220
|
|
|
124
221
|
${ORDERING}
|
|
125
222
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
223
|
+
Add a \`narration\` to a commit, or to a unit, only when it says something the commit message
|
|
224
|
+
body does not. The page shows it as your note. A note is one plain paragraph on why, in plain
|
|
225
|
+
text with no markdown. Say why, not what the lines already show. Leave it out when the body
|
|
226
|
+
already says it.`,
|
|
130
227
|
},
|
|
131
228
|
};
|
|
132
229
|
const FORMAT = (draftPath, base, tip) => `Write the plan as JSON to ${draftPath}. Write only the file, in this format:
|
|
@@ -139,7 +236,7 @@ const FORMAT = (draftPath, base, tip) => `Write the plan as JSON to ${draftPath}
|
|
|
139
236
|
{ "message": "Add the Token type\\n\\nThe parser needs it.", "units": ["h_0123456789ab", "h_ba9876543210:0-3,7"] },
|
|
140
237
|
{
|
|
141
238
|
"message": "Parse tokens",
|
|
142
|
-
"narration": "
|
|
239
|
+
"narration": "Tokens are read in one pass, so the loop below never backtracks.",
|
|
143
240
|
"units": [{ "unit": "h_ba9876543210", "narration": "The loop is boring on purpose." }, "f_0123456789ab"]
|
|
144
241
|
}
|
|
145
242
|
],
|
|
@@ -148,19 +245,18 @@ const FORMAT = (draftPath, base, tip) => `Write the plan as JSON to ${draftPath}
|
|
|
148
245
|
]
|
|
149
246
|
}
|
|
150
247
|
|
|
151
|
-
\`narration\` is optional on a commit and on a unit
|
|
152
|
-
\`{ "unit": "<ref>", "narration": "..." }\` instead of the plain ref string.
|
|
248
|
+
\`narration\` is optional on a commit and on a unit, and the page shows it as your note. A unit
|
|
249
|
+
with a note is written as \`{ "unit": "<ref>", "narration": "..." }\` instead of the plain ref string.
|
|
153
250
|
|
|
154
251
|
\`diagram\` is optional on a commit and on a unit, written the same way:
|
|
155
252
|
\`{ "unit": "<ref>", "diagram": "..." }\`. It is Mermaid source that the review page draws beside
|
|
156
|
-
the code: a commit's diagram
|
|
157
|
-
unit. Add one only where a picture says what the code and prose cannot: data flowing between
|
|
253
|
+
the code: a commit's diagram under its commit, a unit's under its hunk. Add one only where a picture says what the code and prose cannot: data flowing between
|
|
158
254
|
modules, a state machine, a call graph, or the before and after shape of a structure. Most commits
|
|
159
255
|
need none. The first line names the diagram type, for example:
|
|
160
256
|
|
|
161
257
|
"diagram": "flowchart LR\\n parse --> validate --> apply"
|
|
162
258
|
|
|
163
|
-
|
|
259
|
+
The note stays plain text even when a diagram is present.
|
|
164
260
|
\`responses\` is [] when there are no review comments. \`status\` is one of "addressed",
|
|
165
261
|
"declined", "deferred" or, for a question, "answered", and \`note\` says what you did or why not.
|
|
166
262
|
An "answered" response also carries \`"at": { "commit": 3, "unit": "h_x" }\`: the commit, counted from 1,
|
|
@@ -174,6 +270,35 @@ const RULES = (checkCommand) => `- A unit is "h_…" (a text hunk), "f_…" (a w
|
|
|
174
270
|
- Every commit needs a non-empty message and at least one unit.
|
|
175
271
|
- Keep every file syntactically whole in every commit: a function with its closing brace, a block with both ends, an import before or with its first use. Commits need not build or pass tests, but each file a commit writes must parse.
|
|
176
272
|
- Check your work with \`${checkCommand}\` and fix every issue it prints, syntax errors included.`;
|
|
273
|
+
function renderUnit(unit) {
|
|
274
|
+
return unit.kind === "hunk" ? renderHunk(unit) : renderOtherUnit(unit);
|
|
275
|
+
}
|
|
276
|
+
/** The vanished and new units, and the reviewer's comments on hunks that vanished, quoted. */
|
|
277
|
+
function renderChanges(changes, input, ctx) {
|
|
278
|
+
const { since, vanished, added } = changes;
|
|
279
|
+
const gone = new Set(vanished.map((v) => v.unit.id));
|
|
280
|
+
const review = input.mode === "split" ? input.review : null;
|
|
281
|
+
const orphans = (review?.comments ?? []).filter((c) => c.author !== "agent" && c.anchor.kind === "hunk" && gone.has(c.anchor.unit));
|
|
282
|
+
const parts = [
|
|
283
|
+
`## Changes since v${since}\n`,
|
|
284
|
+
`The branch moved after plan v${since}. ${input.draftPath} already holds that plan on the new tip: every unit whose hunk did not change keeps its commit, and any commit left with no unit was dropped. Put each new unit below into a commit, an existing one or a new one, and keep the rest as it is.`,
|
|
285
|
+
];
|
|
286
|
+
if (vanished.length > 0) {
|
|
287
|
+
parts.push(`\nThese units of v${since} changed, so their ids are gone:`, ...vanished.map(({ unit, commits }) => {
|
|
288
|
+
const where = commits
|
|
289
|
+
.map((c) => `commit ${c.index + 1} ${JSON.stringify(c.message.split("\n")[0] ?? "")}`)
|
|
290
|
+
.join(", ");
|
|
291
|
+
return `- ${unit.id} ${unit.path}, was in ${where}`;
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
if (added.length > 0) {
|
|
295
|
+
parts.push("\nNew units, in no commit yet:\n", added.map(renderUnit).join("\n\n"));
|
|
296
|
+
}
|
|
297
|
+
if (orphans.length > 0) {
|
|
298
|
+
parts.push(`\nThe reviewer commented on hunks that changed. Write a response for each saying what you changed, and post the same with \`follow-me reply <id>\`:\n`, orphans.map((c) => renderComment(c, ctx)).join("\n"));
|
|
299
|
+
}
|
|
300
|
+
return parts.join("\n");
|
|
301
|
+
}
|
|
177
302
|
/** The planner prompt for a coding agent. Pure and deterministic for the same input. */
|
|
178
303
|
export function renderPrompt(input) {
|
|
179
304
|
const { diff, draftPath } = input;
|
|
@@ -187,22 +312,34 @@ export function renderPrompt(input) {
|
|
|
187
312
|
if (input.mode === "walk") {
|
|
188
313
|
const sections = [
|
|
189
314
|
...head,
|
|
315
|
+
...(input.changes === undefined
|
|
316
|
+
? []
|
|
317
|
+
: [renderChanges(input.changes, input, { commits: [], units: diff.units })]),
|
|
190
318
|
`## Concerns\n\n${CONCERNS}`,
|
|
191
|
-
`## Questions from the reviewer\n\n${QUESTIONS}`,
|
|
319
|
+
`## Questions from the reviewer\n\n${QUESTIONS}\n\n${CONTEXT_RULE}`,
|
|
192
320
|
units,
|
|
193
321
|
];
|
|
194
322
|
return `${sections.join("\n\n")}\n`;
|
|
195
323
|
}
|
|
196
|
-
const
|
|
324
|
+
const ctx = {
|
|
325
|
+
commits: input.previousCommits ?? [],
|
|
326
|
+
units: diff.units,
|
|
327
|
+
};
|
|
328
|
+
const sections = [
|
|
329
|
+
...head,
|
|
330
|
+
...(input.changes === undefined ? [] : [renderChanges(input.changes, input, ctx)]),
|
|
331
|
+
units,
|
|
332
|
+
];
|
|
197
333
|
const { previousPlan, review } = input;
|
|
198
334
|
if (previousPlan) {
|
|
199
335
|
sections.push(`## Previous plan\n\nCommits are numbered from 1.\n\n${renderPreviousPlan(previousPlan)}`);
|
|
200
336
|
}
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
337
|
+
const theirs = review?.comments.filter((c) => c.author !== "agent") ?? [];
|
|
338
|
+
const asked = theirs.filter((c) => c.kind === "question");
|
|
339
|
+
const changes = theirs.filter((c) => c.kind !== "question");
|
|
340
|
+
const questions = [`## Questions from the reviewer\n`, QUESTIONS, `\n${CONTEXT_RULE}`];
|
|
204
341
|
if (review && asked.length > 0) {
|
|
205
|
-
questions.push(`\nThe reviewer asked ${asked.length === 1 ? "this question" : "these questions"} about plan v${review.planVersion}. Commits are numbered from 1. Write one response for each.\n`, asked.map(renderComment).join("\n"));
|
|
342
|
+
questions.push(`\nThe reviewer asked ${asked.length === 1 ? "this question" : "these questions"} about plan v${review.planVersion}. Commits are numbered from 1. Write one response for each.\n`, asked.map((c) => renderComment(c, ctx)).join("\n"));
|
|
206
343
|
}
|
|
207
344
|
sections.push(questions.join("\n"));
|
|
208
345
|
if (review && changes.length > 0) {
|
|
@@ -210,7 +347,7 @@ export function renderPrompt(input) {
|
|
|
210
347
|
`## Open review comments\n`,
|
|
211
348
|
`A reviewer commented on plan v${review.planVersion}. Commits are numbered from 1.`,
|
|
212
349
|
"Answer every comment below with one entry in `responses`, changing the plan where you agree.\n",
|
|
213
|
-
changes.map(renderComment).join("\n"),
|
|
350
|
+
changes.map((c) => renderComment(c, ctx)).join("\n"),
|
|
214
351
|
].join("\n"));
|
|
215
352
|
}
|
|
216
353
|
return `${sections.join("\n\n")}\n`;
|
|
@@ -253,17 +390,6 @@ function excerpt(hunk, tipStart, owners, k) {
|
|
|
253
390
|
}
|
|
254
391
|
return lines;
|
|
255
392
|
}
|
|
256
|
-
/** Where the hunk starts in the tip: its base position shifted by earlier hunks of the file. */
|
|
257
|
-
function tipStartOf(hunk, units) {
|
|
258
|
-
let shift = 0;
|
|
259
|
-
for (const u of units.values()) {
|
|
260
|
-
if (u.kind !== "hunk" || u.path !== hunk.path || u.oldStart >= hunk.oldStart)
|
|
261
|
-
continue;
|
|
262
|
-
for (const line of u.body)
|
|
263
|
-
shift += line.op === "+" ? 1 : line.op === "-" ? -1 : 0;
|
|
264
|
-
}
|
|
265
|
-
return hunk.oldStart + shift + 1;
|
|
266
|
-
}
|
|
267
393
|
/** A fence longer than any backtick run in the content, so quoted code cannot close it. */
|
|
268
394
|
function fence(lines) {
|
|
269
395
|
const longest = Math.max(0, ...lines.flatMap((l) => (l.match(/`+/g) ?? []).map((r) => r.length)));
|
|
@@ -299,6 +425,15 @@ function whereComment(ctx, c) {
|
|
|
299
425
|
case "position":
|
|
300
426
|
return { path: null, line: null, label: `commit ${anchor.commit + 1} position`, quote: [] };
|
|
301
427
|
case "hunk": {
|
|
428
|
+
const pick = pickRows(ctx, anchor);
|
|
429
|
+
if (pick !== null) {
|
|
430
|
+
return {
|
|
431
|
+
path: ctx.units.get(anchor.unit)?.path ?? c.context.path,
|
|
432
|
+
line: pick.line,
|
|
433
|
+
label: `${pick.range.includes("-") ? "lines" : "line"} ${pick.range}${pick.baseSide ? " (base side)" : ""}`,
|
|
434
|
+
quote: pick.rows,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
302
437
|
const quoted = quoteHunk(ctx.units, ctx.commits, anchor.unit, anchor.commit, anchor.changes);
|
|
303
438
|
const path = ctx.units.get(anchor.unit)?.path ?? c.context.path;
|
|
304
439
|
if (quoted === null) {
|
|
@@ -514,10 +649,24 @@ function renderQuestions(ctx, questions, responses) {
|
|
|
514
649
|
}
|
|
515
650
|
return out;
|
|
516
651
|
}
|
|
652
|
+
/** Threads the agent opened, with what the reviewer said back. */
|
|
653
|
+
function renderThreads(ctx, threads) {
|
|
654
|
+
const out = ["## Threads you started", ""];
|
|
655
|
+
for (const c of threads) {
|
|
656
|
+
const where = whereComment(ctx, c);
|
|
657
|
+
const place = where.path === null ? where.label : `${where.path} · ${where.label}`;
|
|
658
|
+
out.push(`### ${c.id} · ${place}`, "", ...quoteBlock(where.quote));
|
|
659
|
+
if (where.quote.length > 0)
|
|
660
|
+
out.push("");
|
|
661
|
+
out.push(`Agent: ${c.body}`, ...renderReplies(c), "");
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
517
665
|
/** Plan feedback for a split review: what the planner answers through `responses`. */
|
|
518
|
-
function renderPlanFeedback(
|
|
666
|
+
function renderPlanFeedback(ctx, comments) {
|
|
667
|
+
const { commits } = ctx;
|
|
519
668
|
const out = [];
|
|
520
|
-
const line = (c) => [`- ${c.id} on ${describeAnchor(c.anchor)}`];
|
|
669
|
+
const line = (c) => [`- ${c.id} on ${describeAnchor(c.anchor, ctx)}`];
|
|
521
670
|
const planLevel = comments.filter((c) => c.anchor.kind === "plan");
|
|
522
671
|
if (planLevel.length > 0) {
|
|
523
672
|
out.push("## Whole plan", "");
|
|
@@ -535,6 +684,9 @@ function renderPlanFeedback(commits, comments) {
|
|
|
535
684
|
out.push(...line(c));
|
|
536
685
|
if (c.context.path)
|
|
537
686
|
out.push(` - file: ${c.context.path}`);
|
|
687
|
+
const rows = anchorRows(ctx, c.anchor);
|
|
688
|
+
if (rows.length > 0)
|
|
689
|
+
out.push(" - picked rows are marked >:", ...rows.map((r) => ` ${r}`));
|
|
538
690
|
out.push(` - ${c.body.split("\n").join("\n ")}`);
|
|
539
691
|
for (const reply of renderReplies(c))
|
|
540
692
|
out.push(` - ${reply}`);
|
|
@@ -549,6 +701,155 @@ function renderPlanFeedback(commits, comments) {
|
|
|
549
701
|
}
|
|
550
702
|
return out;
|
|
551
703
|
}
|
|
704
|
+
/** The rows `want` picks, numbered as the quoting code numbers them. */
|
|
705
|
+
function numberedRows(hunk, units, want) {
|
|
706
|
+
let tip = tipStartOf(hunk, units);
|
|
707
|
+
let base = hunk.oldStart + 1;
|
|
708
|
+
const rows = [];
|
|
709
|
+
hunk.body.forEach((line, i) => {
|
|
710
|
+
if (want(i, line.op === " " ? null : line.change))
|
|
711
|
+
rows.push({ op: line.op, tip, base });
|
|
712
|
+
if (line.op !== "-")
|
|
713
|
+
tip += 1;
|
|
714
|
+
if (line.op !== "+")
|
|
715
|
+
base += 1;
|
|
716
|
+
});
|
|
717
|
+
return rows;
|
|
718
|
+
}
|
|
719
|
+
const NO_SPAN = { newLine: null, oldLine: null, startNewLine: null, startOldLine: null };
|
|
720
|
+
/** The last row names the line: both sides for context, the tip for an addition, the base for a removal. */
|
|
721
|
+
function spanOf(rows) {
|
|
722
|
+
const first = rows[0];
|
|
723
|
+
const last = rows.at(-1);
|
|
724
|
+
if (first === undefined || last === undefined)
|
|
725
|
+
return NO_SPAN;
|
|
726
|
+
const range = rows.length > 1;
|
|
727
|
+
return {
|
|
728
|
+
newLine: last.op === "-" ? null : last.tip,
|
|
729
|
+
oldLine: last.op === "+" ? null : last.base,
|
|
730
|
+
startNewLine: range && first.op !== "-" ? first.tip : null,
|
|
731
|
+
startOldLine: range && first.op !== "+" ? first.base : null,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function commentSpan(ctx, c) {
|
|
735
|
+
const { anchor } = c;
|
|
736
|
+
if (anchor.kind !== "hunk")
|
|
737
|
+
return NO_SPAN;
|
|
738
|
+
const hunk = ctx.units.get(anchor.unit);
|
|
739
|
+
if (hunk?.kind !== "hunk")
|
|
740
|
+
return NO_SPAN;
|
|
741
|
+
const { offsets, changes } = anchor;
|
|
742
|
+
if (offsets !== undefined)
|
|
743
|
+
return spanOf(numberedRows(hunk, ctx.units, (i) => offsets.includes(i)));
|
|
744
|
+
return spanOf(numberedRows(hunk, ctx.units, (_, n) => n !== null && (changes === null || changes.includes(n))));
|
|
745
|
+
}
|
|
746
|
+
function concernSpan(ctx, concern) {
|
|
747
|
+
const hunk = concern.unit === undefined ? undefined : ctx.units.get(concern.unit);
|
|
748
|
+
if (hunk?.kind !== "hunk")
|
|
749
|
+
return NO_SPAN;
|
|
750
|
+
const lines = concern.lines;
|
|
751
|
+
return spanOf(numberedRows(hunk, ctx.units, (_, n) => n !== null && (lines === undefined || lines.includes(n))));
|
|
752
|
+
}
|
|
753
|
+
function threadOf(c) {
|
|
754
|
+
return (c.replies ?? []).map((r) => ({ author: r.author, body: r.body }));
|
|
755
|
+
}
|
|
756
|
+
function withSeverity(severity, text) {
|
|
757
|
+
return severity === null ? text : `**${severity}**\n\n${text}`;
|
|
758
|
+
}
|
|
759
|
+
function collectItems(review, commits, units, responses) {
|
|
760
|
+
const ctx = { commits, units };
|
|
761
|
+
const fromReviewer = review.comments.filter((c) => c.author !== "agent");
|
|
762
|
+
const walk = review.mode === "walk";
|
|
763
|
+
const rest = fromReviewer.filter((c) => c.kind !== "question");
|
|
764
|
+
const issues = walk ? rest : rest.filter((c) => c.severity !== undefined);
|
|
765
|
+
const noComments = new Set(review.excluded?.comments ?? []);
|
|
766
|
+
const noConcerns = new Set(review.excluded?.concerns ?? []);
|
|
767
|
+
const resolved = new Set(review.resolved ?? []);
|
|
768
|
+
const out = buildItems(ctx, keptConcerns(review, commits), issues).map((it) => {
|
|
769
|
+
const { kept } = it;
|
|
770
|
+
const first = it.comments[0];
|
|
771
|
+
if (kept !== null) {
|
|
772
|
+
const note = kept.note === null ? "" : `\n\nReviewer: ${kept.note}`;
|
|
773
|
+
const thread = it.comments.flatMap((c) => [
|
|
774
|
+
{ author: "reviewer", body: c.body },
|
|
775
|
+
...threadOf(c),
|
|
776
|
+
]);
|
|
777
|
+
return {
|
|
778
|
+
where: it.where,
|
|
779
|
+
item: {
|
|
780
|
+
...concernSpan(ctx, kept.concern),
|
|
781
|
+
path: it.where.path,
|
|
782
|
+
oldPath: it.where.path,
|
|
783
|
+
severity: it.severity,
|
|
784
|
+
kind: "concern",
|
|
785
|
+
body: withSeverity(it.severity, `${kept.concern.text}${note}`),
|
|
786
|
+
thread,
|
|
787
|
+
resolved: false,
|
|
788
|
+
excluded: noConcerns.has(kept.concern.id),
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
if (first === undefined)
|
|
793
|
+
throw new Error("an item with neither a concern nor a comment");
|
|
794
|
+
return {
|
|
795
|
+
where: it.where,
|
|
796
|
+
item: {
|
|
797
|
+
...commentSpan(ctx, first),
|
|
798
|
+
path: it.where.path,
|
|
799
|
+
oldPath: it.where.path,
|
|
800
|
+
severity: it.severity,
|
|
801
|
+
kind: "flag",
|
|
802
|
+
body: withSeverity(it.severity, first.body),
|
|
803
|
+
thread: threadOf(first),
|
|
804
|
+
resolved: resolved.has(first.id),
|
|
805
|
+
excluded: noComments.has(first.id),
|
|
806
|
+
},
|
|
807
|
+
};
|
|
808
|
+
});
|
|
809
|
+
for (const q of fromReviewer.filter((c) => c.kind === "question")) {
|
|
810
|
+
const where = whereComment(ctx, q);
|
|
811
|
+
const answer = responses.find((r) => r.comment === q.id)?.note;
|
|
812
|
+
const lastAgent = (q.replies ?? []).findLast((r) => r.author === "agent")?.body;
|
|
813
|
+
const said = answer ?? lastAgent;
|
|
814
|
+
const details = said === undefined ? "" : `\n\n<details><summary>Answer</summary>\n\n${said}\n\n</details>`;
|
|
815
|
+
out.push({
|
|
816
|
+
where,
|
|
817
|
+
item: {
|
|
818
|
+
...commentSpan(ctx, q),
|
|
819
|
+
path: where.path,
|
|
820
|
+
oldPath: where.path,
|
|
821
|
+
severity: q.severity ?? null,
|
|
822
|
+
kind: "question",
|
|
823
|
+
body: `${q.body}${details}`,
|
|
824
|
+
thread: threadOf(q),
|
|
825
|
+
resolved: resolved.has(q.id),
|
|
826
|
+
excluded: noComments.has(q.id),
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return sortItems(out, units);
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* The items of a review for the merge request, in file order then line order, general items last.
|
|
834
|
+
* Excluded ones are present and marked; whoever posts them skips those.
|
|
835
|
+
*/
|
|
836
|
+
export function reportItems(review, commits, units, opts) {
|
|
837
|
+
return collectItems(review, commits, units, opts.responses).map((c) => c.item);
|
|
838
|
+
}
|
|
839
|
+
/** A short list of what the reviewer left out, so nothing vanishes without a trace. */
|
|
840
|
+
function renderExcluded(collected) {
|
|
841
|
+
const left = collected.filter((c) => c.item.excluded);
|
|
842
|
+
if (left.length === 0)
|
|
843
|
+
return [];
|
|
844
|
+
const out = ["## Excluded", "", "The reviewer left these out of the hand-off:", ""];
|
|
845
|
+
for (const { item, where } of left) {
|
|
846
|
+
const place = where.path === null ? where.label : `${where.path} · ${where.label}`;
|
|
847
|
+
const first = item.body.split("\n").find((l) => l !== "" && !l.startsWith("**")) ?? "";
|
|
848
|
+
out.push(`- ${place} · ${item.kind}${item.severity === null ? "" : ` · ${item.severity}`}: ${first}`);
|
|
849
|
+
}
|
|
850
|
+
out.push("");
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
552
853
|
/**
|
|
553
854
|
* The consolidated report of a review, in the voices of both the reviewer and the agent: the
|
|
554
855
|
* reviewer's comments and the agent's concerns the reviewer kept, grouped by file and ordered by
|
|
@@ -556,11 +857,29 @@ function renderPlanFeedback(commits, comments) {
|
|
|
556
857
|
* split review is plan feedback for the planner, followed by the same code issues and questions.
|
|
557
858
|
* Concerns the reviewer has not decided are left out, as are those dismissed.
|
|
558
859
|
*/
|
|
559
|
-
export function renderReport(
|
|
860
|
+
export function renderReport(full, commits, units, opts) {
|
|
861
|
+
const excluded = full.excluded;
|
|
862
|
+
if (excluded === undefined)
|
|
863
|
+
return renderIncluded(full, commits, units, opts);
|
|
864
|
+
const noComments = new Set(excluded.comments);
|
|
865
|
+
const noConcerns = new Set(excluded.concerns);
|
|
866
|
+
const review = {
|
|
867
|
+
...full,
|
|
868
|
+
comments: full.comments.filter((c) => !noComments.has(c.id)),
|
|
869
|
+
concerns: (full.concerns ?? []).filter((d) => !noConcerns.has(d.concern)),
|
|
870
|
+
};
|
|
871
|
+
const tail = renderExcluded(collectItems(full, commits, units, opts.responses));
|
|
872
|
+
const main = renderIncluded(review, commits, units, opts);
|
|
873
|
+
return tail.length === 0 ? main : `${main}${main.endsWith("\n") ? "" : "\n"}${tail.join("\n")}`;
|
|
874
|
+
}
|
|
875
|
+
function renderIncluded(review, commits, units, opts) {
|
|
560
876
|
const ctx = { commits, units };
|
|
561
877
|
const walk = review.mode === "walk";
|
|
562
|
-
const
|
|
563
|
-
|
|
878
|
+
const fromReviewer = review.comments.filter((c) => c.author !== "agent");
|
|
879
|
+
// A thread the agent started matters to it only once the reviewer has answered.
|
|
880
|
+
const started = review.comments.filter((c) => c.author === "agent" && (c.replies ?? []).some((r) => r.author === "reviewer"));
|
|
881
|
+
const questions = fromReviewer.filter((c) => c.kind === "question");
|
|
882
|
+
const rest = fromReviewer.filter((c) => c.kind !== "question");
|
|
564
883
|
const issues = walk ? rest : rest.filter((c) => c.severity !== undefined);
|
|
565
884
|
const feedback = walk ? [] : rest.filter((c) => c.severity === undefined);
|
|
566
885
|
const items = buildItems(ctx, keptConcerns(review, commits), issues);
|
|
@@ -573,36 +892,47 @@ export function renderReport(review, commits, units, opts) {
|
|
|
573
892
|
}
|
|
574
893
|
else {
|
|
575
894
|
out.push(`# Review of plan v${review.planVersion}`, "");
|
|
576
|
-
if (feedback.length === 0 &&
|
|
895
|
+
if (feedback.length === 0 &&
|
|
896
|
+
items.length === 0 &&
|
|
897
|
+
questions.length === 0 &&
|
|
898
|
+
started.length === 0) {
|
|
577
899
|
out.push("No comments.", "");
|
|
578
900
|
return out.join("\n");
|
|
579
901
|
}
|
|
580
|
-
out.push(...renderPlanFeedback(
|
|
902
|
+
out.push(...renderPlanFeedback(ctx, feedback));
|
|
581
903
|
if (items.length > 0) {
|
|
582
904
|
out.push(`## Code issues (${summarizeIssues(items)})`, "", ...renderItems(items, units, 3));
|
|
583
905
|
}
|
|
584
906
|
}
|
|
585
907
|
if (questions.length > 0)
|
|
586
908
|
out.push(...renderQuestions(ctx, questions, opts.responses));
|
|
909
|
+
if (started.length > 0)
|
|
910
|
+
out.push(...renderThreads(ctx, started));
|
|
587
911
|
if (walk && items.length > 0) {
|
|
588
912
|
out.push("Address each issue above in the code on this branch, then tell the reviewer what you changed and what you chose not to change and why.", "");
|
|
589
913
|
}
|
|
590
914
|
if (!walk && (feedback.length > 0 || questions.length > 0)) {
|
|
591
915
|
out.push("Answer every comment id above in `responses` of the next draft, then run `follow-me check` and `follow-me apply`.", "");
|
|
592
916
|
}
|
|
917
|
+
const onContext = review.comments.some((c) => pickRows(ctx, c.anchor)?.touchesContext);
|
|
918
|
+
if (onContext)
|
|
919
|
+
out.push(CONTEXT_RULE, "");
|
|
593
920
|
if (!walk && items.length > 0) {
|
|
594
921
|
out.push("Fix the code issues on the original branch, then run `follow-me extract` again.", "");
|
|
595
922
|
}
|
|
596
923
|
return out.join("\n");
|
|
597
924
|
}
|
|
598
|
-
function renderEvent(event) {
|
|
925
|
+
function renderEvent(event, ctx) {
|
|
599
926
|
switch (event.type) {
|
|
600
927
|
case "comment":
|
|
601
928
|
case "comment-updated": {
|
|
602
929
|
const { comment: c } = event;
|
|
603
930
|
const what = c.kind === "question" ? "question" : "comment";
|
|
604
931
|
const verb = event.type === "comment" ? "new" : "edited";
|
|
605
|
-
const lines = [`- ${verb} ${what} ${c.id} on ${describeAnchor(c.anchor)}`];
|
|
932
|
+
const lines = [`- ${verb} ${what} ${c.id} on ${describeAnchor(c.anchor, ctx)}`];
|
|
933
|
+
const rows = anchorRows(ctx, c.anchor);
|
|
934
|
+
if (rows.length > 0)
|
|
935
|
+
lines.push(...rows.map((r) => ` ${r}`));
|
|
606
936
|
if (c.context.path)
|
|
607
937
|
lines.push(` - file: ${c.context.path}`);
|
|
608
938
|
if (c.severity)
|
|
@@ -625,6 +955,8 @@ function renderEvent(event) {
|
|
|
625
955
|
return [`- question ${event.commentId} marked ${event.resolved ? "resolved" : "open again"}`];
|
|
626
956
|
case "version":
|
|
627
957
|
return [`- plan v${event.version} is now open`];
|
|
958
|
+
case "submitted":
|
|
959
|
+
return ["- the reviewer submitted the review"];
|
|
628
960
|
case "ended":
|
|
629
961
|
return [`- the reviewer ended the review; the report is next to ${event.review}`];
|
|
630
962
|
default: {
|
|
@@ -634,9 +966,10 @@ function renderEvent(event) {
|
|
|
634
966
|
}
|
|
635
967
|
}
|
|
636
968
|
/** A markdown digest of what happened on the review page, for `follow-me watch`. */
|
|
637
|
-
export function renderEvents(events) {
|
|
969
|
+
export function renderEvents(events, ctx = null) {
|
|
638
970
|
const first = events[0]?.seq;
|
|
639
971
|
const last = events.at(-1)?.seq;
|
|
640
972
|
const range = first === last ? `event ${first}` : `events ${first}-${last}`;
|
|
641
|
-
|
|
973
|
+
const body = events.flatMap((event) => renderEvent(event, ctx));
|
|
974
|
+
return [`# Review activity (${range})`, "", ...body, ""].join("\n");
|
|
642
975
|
}
|