@shiyuphan/follow-me 0.4.1 → 0.5.1

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 (99) hide show
  1. package/dist/src/cli.js +171 -14
  2. package/dist/src/comments.js +62 -9
  3. package/dist/src/evolve.js +146 -26
  4. package/dist/src/harness.js +4 -1
  5. package/dist/src/host.js +29 -4
  6. package/dist/src/install.js +1 -1
  7. package/dist/src/plan.js +14 -0
  8. package/dist/src/prompt.js +270 -178
  9. package/dist/src/server.js +106 -29
  10. package/dist/src/session.js +23 -0
  11. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-byISxePP.js → abnfDiagram-WEYOIA3C-8UsLPznO.js} +1 -1
  12. package/dist/ui/dist/assets/{arc-CBzsAsP2.js → arc-vovjGalz.js} +1 -1
  13. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-0OSkIwdB.js → architecture-WOLXFQ4H-BBMK-PUu.js} +1 -1
  14. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-ixHybNqO.js → architectureDiagram-ZHVYKT53-Bw_ehaQk.js} +1 -1
  15. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-1F8XL_Ou.js → blockDiagram-RJAE7QA3-CAmD0wKf.js} +1 -1
  16. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-D9PFBXnc.js → c4Diagram-XDPPPE54-DCP-3zgt.js} +1 -1
  17. package/dist/ui/dist/assets/channel-B7CT_oIp.js +1 -0
  18. package/dist/ui/dist/assets/{chunk-2E7WHVMD-zZdNfMkj.js → chunk-2E7WHVMD-BiHSeJMu.js} +1 -1
  19. package/dist/ui/dist/assets/{chunk-4EMNKR2T-CTKoxSSj.js → chunk-4EMNKR2T-CR3lHKf_.js} +1 -1
  20. package/dist/ui/dist/assets/{chunk-55XCU76K-ifFnDlcS.js → chunk-55XCU76K-CtaoloTT.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-7S6XSTFO-BCMVdi_1.js → chunk-7S6XSTFO-eABdc7fu.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-AQDTSKZB-DC-5Whju.js → chunk-AQDTSKZB-CPlbJWi-.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-DASbe_gb.js → chunk-DSNLSHJZ-YiMuxsb-.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-HAGFOTED-CArytDjj.js → chunk-HAGFOTED-1VcdL78a.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-HNE4Q36P-Dnl-Wtud.js → chunk-HNE4Q36P-u-sr_0Qu.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-NQOKRVIF-wvSLaeaK.js → chunk-NQOKRVIF-C_QlWSze.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-UHW7PFBP-CckaOGiQ.js → chunk-UHW7PFBP-CNuZhsbU.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-VQ5W62QI-kQBMDJ7V.js → chunk-VQ5W62QI-DAHWbLqU.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-WQQL4CDK-CMg6bbLU.js → chunk-WQQL4CDK-DWQnxFCd.js} +1 -1
  30. package/dist/ui/dist/assets/{chunk-XXDRQBXY-CYlFr19K.js → chunk-XXDRQBXY-B0lGgSOR.js} +1 -1
  31. package/dist/ui/dist/assets/{chunk-YZO67HFG-CqkG1vVk.js → chunk-YZO67HFG-BDw7efkN.js} +1 -1
  32. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-BUNEdx-e.js → chunk-ZAUX2LTX-B5wfHsZv.js} +1 -1
  33. package/dist/ui/dist/assets/{chunk-ZCHMQSOR-CFTbvcrm.js → chunk-ZCHMQSOR--rWtYtAG.js} +1 -1
  34. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-t2OdJ0LA.js → classDiagram-v2-4MYWR3UA-BKuQ_xoS.js} +1 -1
  35. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-FGm030gc.js → cose-bilkent-JH36ORCC-Dk5fIpU0.js} +1 -1
  36. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-BtU33Lsu.js → cynefin-EF2NZ3EQ-DIykJb2g.js} +1 -1
  37. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-DUr-I7hS.js → cynefinDiagram-WF24UDRP-Cg8tBqUb.js} +1 -1
  38. package/dist/ui/dist/assets/{dagre-NK4GIZVX-BWTHwJKt.js → dagre-NK4GIZVX-BC_tnnjz.js} +1 -1
  39. package/dist/ui/dist/assets/{diagram-CBK3MWTC-PcHrsEBn.js → diagram-CBK3MWTC-B_MQGixU.js} +1 -1
  40. package/dist/ui/dist/assets/{diagram-I5XIIV2D-CkGtKjYj.js → diagram-I5XIIV2D-DO681pZ5.js} +1 -1
  41. package/dist/ui/dist/assets/{diagram-ODIL2UCW-B_HdBB1w.js → diagram-ODIL2UCW-D14jLzU6.js} +1 -1
  42. package/dist/ui/dist/assets/{diagram-PFBEJUXE-DL9zayE6.js → diagram-PFBEJUXE-cML48gUS.js} +1 -1
  43. package/dist/ui/dist/assets/{diagram-RJA3I4VV-By5wl8nK.js → diagram-RJA3I4VV-DSoq8jFk.js} +1 -1
  44. package/dist/ui/dist/assets/{diagram-TYUNFSNR-ziWPVayL.js → diagram-TYUNFSNR-CaJ4pvnl.js} +1 -1
  45. package/dist/ui/dist/assets/{dist-NDVqCM7k.js → dist-D3j-DODP.js} +1 -1
  46. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DH4YGkPc.js → ebnfDiagram-V4P3TTPT-CyRpjSix.js} +1 -1
  47. package/dist/ui/dist/assets/{elk-QONTBCTQ-BL2RlCEv.js → elk-QONTBCTQ-ICJdzVFL.js} +1 -1
  48. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BKbTMaRp.js → erDiagram-KYQIGQAA-B57hNTfw.js} +1 -1
  49. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-Dk5RCWc-.js → eventmodeling-K75KTNOO-BG8tUVIQ.js} +1 -1
  50. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU--HPNoVQJ.js +1 -0
  51. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-3vM9gxeT.js → ganttDiagram-R7GPNTJY-Czt6Klik.js} +1 -1
  52. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-CXGFhz54.js → gitGraph-VSP46ZUC-De_ZWJLj.js} +1 -1
  53. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js → gitGraphDiagram-PDYL7XHY-BMCJoOnT.js} +1 -1
  54. package/dist/ui/dist/assets/index-CYKbk4QS.js +134 -0
  55. package/dist/ui/dist/assets/{info-OHQRW6UA-DfSsZ8ib.js → info-OHQRW6UA-D75H9kUq.js} +1 -1
  56. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-B_7tyrL5.js → infoDiagram-MVDRKAX5-Bsm_I9dt.js} +1 -1
  57. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DUvHljpg.js → ishikawaDiagram-HPEHV2GE-98xZAJ9y.js} +1 -1
  58. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-BCgqzrb8.js → journeyDiagram-BOSHW7O7-BWVGuc0P.js} +1 -1
  59. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DOzZ3NmE.js → kanban-definition-FNSA4HC5-DgUE_KDd.js} +1 -1
  60. package/dist/ui/dist/assets/{line-B-mPFVll.js → line-i__8Rvas.js} +1 -1
  61. package/dist/ui/dist/assets/{linear-k7o0jout.js → linear-BXKj0Fip.js} +1 -1
  62. package/dist/ui/dist/assets/{mermaid-parser.core-CAyLSQpH.js → mermaid-parser.core-DxvaorTa.js} +3 -3
  63. package/dist/ui/dist/assets/{mermaid.core-vfVMjgx2.js → mermaid.core-ujYTsRZz.js} +4 -4
  64. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-B6y-B_r4.js → mindmap-definition-6OY57UG3-B7QD6sjk.js} +1 -1
  65. package/dist/ui/dist/assets/{packet-JDAUHWVQ-C_r4ed30.js → packet-JDAUHWVQ-B6aIwfD1.js} +1 -1
  66. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-B8VUm3Bh.js → pegDiagram-ECIUJNBX-_RStHj-a.js} +1 -1
  67. package/dist/ui/dist/assets/{pie-XZMESJXO-DcwtbH9y.js → pie-XZMESJXO-CNLC-pTe.js} +1 -1
  68. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-CvUNBi2F.js → pieDiagram-GNMPY74T-0uT7MT-u.js} +1 -1
  69. package/dist/ui/dist/assets/{prism-core-_2ju85Ya.js → prism-core-DtxziK1i.js} +1 -1
  70. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-Dg2eVS3j.js → quadrantDiagram-D57K4MC3-CTMKb5hl.js} +1 -1
  71. package/dist/ui/dist/assets/{radar-ABXABTNO-xCNTcZE9.js → radar-ABXABTNO-D53h6I0N.js} +1 -1
  72. package/dist/ui/dist/assets/{railroad-I3PHUGI6-BI-sIL2Q.js → railroad-I3PHUGI6-CwjhW4mc.js} +1 -1
  73. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-Ddvk40Wx.js → railroad-abnf-LNEFI6M7-DDbQ3RmB.js} +1 -1
  74. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-h35gE183.js → railroad-ebnf-DWYD2UWJ-Am7YJAqO.js} +1 -1
  75. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Zim9iPdp.js → railroad-peg-JR7BVNR7-Ci32KoVW.js} +1 -1
  76. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-Cz6fdmOQ.js → railroadDiagram-Y72NRBL7-BPU5A6XW.js} +1 -1
  77. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-DTJkCrcD.js → requirementDiagram-EQYSW5RA-_0Pq4QdV.js} +1 -1
  78. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-Bg1iiVla.js → sankeyDiagram-SY24JHO5-c9h-7uf8.js} +1 -1
  79. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-9XfetsRA.js → sequenceDiagram-BLEHNHQU-B9-S4Rc9.js} +1 -1
  80. package/dist/ui/dist/assets/{src-Dym5afQ5.js → src-BZ3kG7lO.js} +1 -1
  81. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMmcMV8z.js → stateDiagram-v2-A6IESKWZ-py1smSen.js} +1 -1
  82. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-B2PcshTY.js → swimlanes-B3ENRKFQ-C1E90aH4.js} +1 -1
  83. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-I98Q8AqP.js +8 -0
  84. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-Cr1hCNtW.js → timeline-definition-JXYKGLGB-DNe1To6B.js} +1 -1
  85. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-jH5ksKnh.js → treeView-D4JQ5SDB-D6MG89i6.js} +1 -1
  86. package/dist/ui/dist/assets/{treemap-SAJKECNS-BV90e8QL.js → treemap-SAJKECNS-CZpt7Mlh.js} +1 -1
  87. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-BNwq5aJV.js → usecaseDiagram-WPOLBNNX-CKsv-03M.js} +1 -1
  88. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-D-lLyZCL.js → vennDiagram-Q3Y3IDCW-DGJ1qlyJ.js} +1 -1
  89. package/dist/ui/dist/assets/{wardley-7MLQ67FV-CWfHQUGt.js → wardley-7MLQ67FV-ClOzBemC.js} +1 -1
  90. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-BL_qNKVd.js → wardleyDiagram-IVAIDKWA-C68ahPyF.js} +1 -1
  91. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-BHPK5Snp.js → xychartDiagram-4URNHDU4-DWDamCoH.js} +1 -1
  92. package/dist/ui/dist/index.html +1 -1
  93. package/package.json +1 -1
  94. package/skills/follow-me/SKILL.md +69 -7
  95. package/skills/follow-me/porting.md +7 -0
  96. package/dist/ui/dist/assets/channel-B-fwsHf1.js +0 -1
  97. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +0 -1
  98. package/dist/ui/dist/assets/index-CBbx_pJk.js +0 -134
  99. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +0 -8
@@ -1,4 +1,5 @@
1
1
  import { tipStartOf } from "./diff.js";
2
+ import { anchorOn, concernOn } from "./evolve.js";
2
3
  /** Consecutive runs collapse: [0,1,2,3,7] becomes "0-3,7". */
3
4
  function formatRanges(changes) {
4
5
  const sorted = [...new Set(changes)].sort((a, b) => a - b);
@@ -22,11 +23,13 @@ function formatSelection(unit, select) {
22
23
  function stripEol(text) {
23
24
  return text.replace(/\r?\n$/, "");
24
25
  }
25
- function renderHunk(hunk) {
26
+ /** Under a "### path" heading the path is left out of each hunk's own line. */
27
+ function renderHunk(hunk, withPath = true) {
26
28
  const lines = hunk.body.map((line) => line.op === " "
27
29
  ? ` ${stripEol(line.text)}`
28
30
  : `[${line.change}]${line.op} ${stripEol(line.text)}`);
29
- return [`${hunk.id} ${hunk.path} (${hunk.changeCount} change lines)`, ...lines].join("\n");
31
+ const where = withPath ? ` ${hunk.path}` : "";
32
+ return [`${hunk.id}${where} (${hunk.changeCount} change lines)`, ...lines].join("\n");
30
33
  }
31
34
  function renderOtherUnit(unit) {
32
35
  if (unit.kind === "mode") {
@@ -41,7 +44,7 @@ function renderUnits(diff) {
41
44
  byPath.set(unit.path, [...(byPath.get(unit.path) ?? []), unit]);
42
45
  }
43
46
  const files = [...byPath].map(([path, units]) => {
44
- const rendered = units.map((unit) => unit.kind === "hunk" ? renderHunk(unit) : renderOtherUnit(unit));
47
+ const rendered = units.map((unit) => unit.kind === "hunk" ? renderHunk(unit, false) : renderOtherUnit(unit));
45
48
  return [`### ${path}`, ...rendered].join("\n\n");
46
49
  });
47
50
  return files.join("\n\n");
@@ -162,8 +165,6 @@ function pickRows(ctx, anchor) {
162
165
  touchesContext: count(" ") > 0,
163
166
  };
164
167
  }
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
168
  function renderComment(comment, ctx) {
168
169
  const lines = [`- ${comment.id} on ${describeAnchor(comment.anchor, ctx)}`];
169
170
  const rows = anchorRows(ctx, comment.anchor);
@@ -188,165 +189,136 @@ function renderComment(comment, ctx) {
188
189
  }
189
190
  return lines.join("\n");
190
191
  }
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.
193
-
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.`;
195
- const ORDERING = `Order the commits so the story reads naturally:
196
- - Scaffolding and types come before the code that uses them.
197
- - A failing test comes before the fix that makes it pass.
198
- - Renames and moves come before the edits that build on them.
199
- - One idea per commit. Prefer many small commits over a few large ones.
200
- - Commits do not need to compile or pass tests on their own.
201
- - The last commit must bring the tree to exactly the tip, which follows from covering every unit.
202
-
203
- Write each commit message as a short imperative title, then a blank line and a body that
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.`;
206
- const GOALS = {
207
- split: {
208
- title: "plan the commits",
209
- goal: `You are reordering a finished branch into a series of commits that a reader can follow
210
- as the author's thought process. The final tree is fixed. You only decide how its change
211
- lines are grouped and ordered.
212
-
213
- ${ORDERING}`,
214
- },
215
- walk: {
216
- title: "tell the story of this branch",
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.
220
-
221
- ${ORDERING}
222
-
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.`,
227
- },
192
+ const TITLES = {
193
+ split: "plan the commits",
194
+ walk: "tell the story of this branch",
228
195
  };
229
- const FORMAT = (draftPath, base, tip) => `Write the plan as JSON to ${draftPath}. Write only the file, in this format:
230
-
231
- {
232
- "schema": "follow-me/plan@1",
233
- "base": "${base}",
234
- "tip": "${tip}",
235
- "commits": [
236
- { "message": "Add the Token type\\n\\nThe parser needs it.", "units": ["h_0123456789ab", "h_ba9876543210:0-3,7"] },
237
- {
238
- "message": "Parse tokens",
239
- "narration": "Tokens are read in one pass, so the loop below never backtracks.",
240
- "units": [{ "unit": "h_ba9876543210", "narration": "The loop is boring on purpose." }, "f_0123456789ab"]
241
- }
242
- ],
243
- "responses": [
244
- { "comment": "c_abc234", "status": "addressed", "note": "Split the hunk and moved it earlier." }
245
- ]
246
- }
247
-
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.
250
-
251
- \`diagram\` is optional on a commit and on a unit, written the same way:
252
- \`{ "unit": "<ref>", "diagram": "..." }\`. It is Mermaid source that the review page draws beside
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
254
- modules, a state machine, a call graph, or the before and after shape of a structure. Most commits
255
- need none. The first line names the diagram type, for example:
256
-
257
- "diagram": "flowchart LR\\n parse --> validate --> apply"
258
-
259
- The note stays plain text even when a diagram is present.
260
- \`responses\` is [] when there are no review comments. \`status\` is one of "addressed",
261
- "declined", "deferred" or, for a question, "answered", and \`note\` says what you did or why not.
262
- An "answered" response also carries \`"at": { "commit": 3, "unit": "h_x" }\`: the commit, counted from 1,
263
- and optionally the unit that holds the explanation.`;
264
- const RULES = (checkCommand) => `- A unit is "h_…" (a text hunk), "f_…" (a whole file) or "m_…" (a mode change).
265
- - "h_x:0-3,7" takes change lines 0, 1, 2, 3 and 7 of hunk h_x. Indices are the [n] shown below.
266
- - A bare "h_x" takes every change line of that hunk that no other commit claimed.
267
- - Every change line must be used exactly once across all commits. Never repeat a line.
268
- - Use the unit ids exactly as printed. Do not invent ids.
269
- - "f_…" and "m_…" units are taken whole, never with a line list.
270
- - Every commit needs a non-empty message and at least one unit.
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.
272
- - Check your work with \`${checkCommand}\` and fix every issue it prints, syntax errors included.`;
196
+ /** Where the fixed instructions live: the skill a harness loads once, not every prompt. */
197
+ function pointer(input) {
198
+ const mode = input.mode === "split" ? "author mode" : "reviewer mode";
199
+ const where = input.skillPath === undefined ? "" : ` If it is not loaded, read ${input.skillPath}.`;
200
+ return `Write the draft as "Writing the draft" in the follow-me skill says, in ${mode}.${where}`;
201
+ }
202
+ function sessionLines(input) {
203
+ return [
204
+ `- draft: ${input.draftPath}`,
205
+ `- base: ${input.diff.base}`,
206
+ `- tip: ${input.diff.tip}`,
207
+ `- check: ${input.checkCommand}`,
208
+ ].join("\n");
209
+ }
273
210
  function renderUnit(unit) {
274
211
  return unit.kind === "hunk" ? renderHunk(unit) : renderOtherUnit(unit);
275
212
  }
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.`,
213
+ const QUOTE_LINES = 6;
214
+ const QUOTE_WIDTH = 160;
215
+ /**
216
+ * At most six rows around the picked ones (marked ">"), each cut to 160 characters: enough to
217
+ * recognise the code without a file read, short enough to send with every comment.
218
+ */
219
+ function shortQuote(rows) {
220
+ const picked = rows.flatMap((r, i) => (r.startsWith(">") ? [i] : []));
221
+ const first = picked[0] ?? 0;
222
+ const last = picked.at(-1) ?? 0;
223
+ const before = Math.max(0, Math.min(2, QUOTE_LINES - (last - first + 1)));
224
+ const start = Math.max(0, first - before);
225
+ return rows
226
+ .slice(start, start + QUOTE_LINES)
227
+ .map((r) => (r.length > QUOTE_WIDTH ? `${r.slice(0, QUOTE_WIDTH - 1)}…` : r));
228
+ }
229
+ /** One comment in a line or two, with its quote only when the agent cannot see the hunk now. */
230
+ function renderOpen(c, ctx, quote) {
231
+ const what = c.kind === "question" ? "question" : (c.severity ?? "comment");
232
+ const lines = [
233
+ `- ${c.id} ${what} on ${describeAnchor(c.anchor, ctx)}: ${JSON.stringify(c.body)}`,
234
+ ];
235
+ if (c.follows)
236
+ lines.push(` asks again after ${c.follows}`);
237
+ for (const reply of c.replies ?? []) {
238
+ lines.push(` ${reply.author} replied: ${JSON.stringify(reply.body)}`);
239
+ }
240
+ if (quote) {
241
+ const rows = anchorRows(ctx, c.anchor);
242
+ const quoted = rows.length > 0 ? rows : (c.context.lines ?? []).map((l) => `> ${l}`);
243
+ lines.push(...shortQuote(quoted).map((r) => ` ${r}`));
244
+ }
245
+ return lines.join("\n");
246
+ }
247
+ /**
248
+ * The prompt after the branch moved. The agent wrote the plan and read every comment already, so
249
+ * only new units carry bodies; the rest are one line each, and a comment is quoted only when the
250
+ * hunk it was on changed.
251
+ */
252
+ function renderMoved(input, changes, head) {
253
+ const { since, vanished, added, draft } = changes;
254
+ const { units } = input.diff;
255
+ const gone = new Map(vanished.map((v) => [v.unit.id, v.unit]));
256
+ const sections = [
257
+ head,
258
+ `## Changes since v${since}\n\nThe branch moved after plan v${since}. The draft holds that plan on the new tip.`,
285
259
  ];
286
260
  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
- }));
261
+ sections.push([
262
+ `## Units of v${since} that changed, so their ids are gone\n`,
263
+ ...vanished.map(({ unit, commits }) => {
264
+ const was = commits.map((c) => c.index + 1).join(", ");
265
+ return `- ${unit.id} ${unit.path} was in commit ${was}`;
266
+ }),
267
+ ].join("\n"));
293
268
  }
294
269
  if (added.length > 0) {
295
- parts.push("\nNew units, in no commit yet:\n", added.map(renderUnit).join("\n\n"));
270
+ sections.push(`## New units, in no commit yet\n\n${added.map(renderUnit).join("\n\n")}`);
296
271
  }
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"));
272
+ const kept = draft.commits.flatMap((commit, k) => commit.units.map((ref) => {
273
+ const path = units.get(ref.unit)?.path ?? "";
274
+ return `- ${formatSelection(ref.unit, ref.select)} ${path} commit ${k + 1}`;
275
+ }));
276
+ if (kept.length > 0) {
277
+ sections.push(`## Unchanged units, in the draft's commits\n\n${kept.join("\n")}`);
299
278
  }
300
- return parts.join("\n");
279
+ const review = input.mode === "split" ? input.review : null;
280
+ const theirs = review?.comments.filter((c) => c.author !== "agent") ?? [];
281
+ if (review !== null && theirs.length > 0) {
282
+ const ctx = { commits: [], units: new Map([...units, ...gone]) };
283
+ const changed = (c) => c.anchor.kind === "hunk" && gone.has(c.anchor.unit);
284
+ sections.push([
285
+ `## Open comments, on plan v${review.planVersion}\n`,
286
+ ...theirs.map((c) => renderOpen(c, ctx, changed(c))),
287
+ ].join("\n"));
288
+ }
289
+ return `${sections.join("\n\n")}\n`;
301
290
  }
302
291
  /** The planner prompt for a coding agent. Pure and deterministic for the same input. */
303
292
  export function renderPrompt(input) {
304
- const { diff, draftPath } = input;
305
- const { title, goal } = GOALS[input.mode];
293
+ const { diff } = input;
306
294
  const head = [
307
- `# follow-me: ${title}\n\n${goal}`,
308
- `## Output format\n\n${FORMAT(draftPath, diff.base, diff.tip)}`,
309
- `## Rules\n\n${RULES(input.checkCommand)}`,
295
+ `# follow-me: ${TITLES[input.mode]}\n\n${pointer(input)}\n\n${sessionLines(input)}`,
310
296
  ];
311
- const units = `## Units\n\nChange lines are shown as \`[n]+ text\` or \`[n]- text\`. Indented lines without a bracket are context and cannot be selected.\n\n${renderUnits(diff)}`;
312
- if (input.mode === "walk") {
313
- const sections = [
314
- ...head,
315
- ...(input.changes === undefined
316
- ? []
317
- : [renderChanges(input.changes, input, { commits: [], units: diff.units })]),
318
- `## Concerns\n\n${CONCERNS}`,
319
- `## Questions from the reviewer\n\n${QUESTIONS}\n\n${CONTEXT_RULE}`,
320
- units,
321
- ];
322
- return `${sections.join("\n\n")}\n`;
323
- }
297
+ if (input.changes !== undefined)
298
+ return renderMoved(input, input.changes, head.join(""));
299
+ const units = `## Units\n\n${renderUnits(diff)}`;
300
+ if (input.mode === "walk")
301
+ return `${[...head, units].join("\n\n")}\n`;
324
302
  const ctx = {
325
303
  commits: input.previousCommits ?? [],
326
304
  units: diff.units,
327
305
  };
328
- const sections = [
329
- ...head,
330
- ...(input.changes === undefined ? [] : [renderChanges(input.changes, input, ctx)]),
331
- units,
332
- ];
306
+ const sections = [...head, units];
333
307
  const { previousPlan, review } = input;
334
- if (previousPlan) {
335
- sections.push(`## Previous plan\n\nCommits are numbered from 1.\n\n${renderPreviousPlan(previousPlan)}`);
336
- }
308
+ if (previousPlan)
309
+ sections.push(`## Previous plan\n\n${renderPreviousPlan(previousPlan)}`);
337
310
  const theirs = review?.comments.filter((c) => c.author !== "agent") ?? [];
338
311
  const asked = theirs.filter((c) => c.kind === "question");
339
312
  const changes = theirs.filter((c) => c.kind !== "question");
340
- const questions = [`## Questions from the reviewer\n`, QUESTIONS, `\n${CONTEXT_RULE}`];
341
313
  if (review && asked.length > 0) {
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"));
314
+ sections.push([
315
+ `## Questions from the reviewer, on plan v${review.planVersion}\n`,
316
+ asked.map((c) => renderComment(c, ctx)).join("\n"),
317
+ ].join("\n"));
343
318
  }
344
- sections.push(questions.join("\n"));
345
319
  if (review && changes.length > 0) {
346
320
  sections.push([
347
- `## Open review comments\n`,
348
- `A reviewer commented on plan v${review.planVersion}. Commits are numbered from 1.`,
349
- "Answer every comment below with one entry in `responses`, changing the plan where you agree.\n",
321
+ `## Open review comments, on plan v${review.planVersion}\n`,
350
322
  changes.map((c) => renderComment(c, ctx)).join("\n"),
351
323
  ].join("\n"));
352
324
  }
@@ -412,10 +384,23 @@ function quoteHunk(units, commits, unit, k, picked) {
412
384
  return { line, text: lines.map((l) => l.text) };
413
385
  }
414
386
  const SEVERITY_RANK = { blocker: 0, major: 1, minor: 2 };
387
+ /** Marks the label of what was said before its code changed, and places what lost its code. */
388
+ function marked(ctx, id, where, quote) {
389
+ const mark = ctx.marks?.get(id);
390
+ if (mark === undefined)
391
+ return where;
392
+ if (mark.gone) {
393
+ return { path: mark.path, line: null, label: `the code is gone after v${mark.since}`, quote };
394
+ }
395
+ return { ...where, label: `${where.label} · the code changed after v${mark.since}` };
396
+ }
415
397
  function lineLabel(line, fallback) {
416
398
  return line === null ? fallback : `line ${line}`;
417
399
  }
418
400
  function whereComment(ctx, c) {
401
+ return marked(ctx, c.id, whereOnTip(ctx, c), c.context.lines ?? []);
402
+ }
403
+ function whereOnTip(ctx, c) {
419
404
  const { anchor } = c;
420
405
  switch (anchor.kind) {
421
406
  case "plan":
@@ -458,6 +443,9 @@ function whereComment(ctx, c) {
458
443
  }
459
444
  }
460
445
  function whereConcern(ctx, kept) {
446
+ return marked(ctx, kept.concern.id, concernOnTip(ctx, kept), []);
447
+ }
448
+ function concernOnTip(ctx, kept) {
461
449
  const { concern, commit } = kept;
462
450
  if (concern.unit === undefined) {
463
451
  return { path: null, line: null, label: `commit ${commit + 1}`, quote: [] };
@@ -756,8 +744,12 @@ function threadOf(c) {
756
744
  function withSeverity(severity, text) {
757
745
  return severity === null ? text : `**${severity}**\n\n${text}`;
758
746
  }
759
- function collectItems(review, commits, units, responses) {
760
- const ctx = { commits, units };
747
+ function changedOf(ctx, id) {
748
+ const mark = ctx.marks?.get(id);
749
+ return mark === undefined ? {} : { changed: { since: mark.since, gone: mark.gone } };
750
+ }
751
+ function collectItems(review, ctx, responses) {
752
+ const { commits, units } = ctx;
761
753
  const fromReviewer = review.comments.filter((c) => c.author !== "agent");
762
754
  const walk = review.mode === "walk";
763
755
  const rest = fromReviewer.filter((c) => c.kind !== "question");
@@ -777,6 +769,7 @@ function collectItems(review, commits, units, responses) {
777
769
  return {
778
770
  where: it.where,
779
771
  item: {
772
+ id: kept.concern.id,
780
773
  ...concernSpan(ctx, kept.concern),
781
774
  path: it.where.path,
782
775
  oldPath: it.where.path,
@@ -786,6 +779,7 @@ function collectItems(review, commits, units, responses) {
786
779
  thread,
787
780
  resolved: false,
788
781
  excluded: noConcerns.has(kept.concern.id),
782
+ ...changedOf(ctx, kept.concern.id),
789
783
  },
790
784
  };
791
785
  }
@@ -794,6 +788,7 @@ function collectItems(review, commits, units, responses) {
794
788
  return {
795
789
  where: it.where,
796
790
  item: {
791
+ id: first.id,
797
792
  ...commentSpan(ctx, first),
798
793
  path: it.where.path,
799
794
  oldPath: it.where.path,
@@ -803,6 +798,7 @@ function collectItems(review, commits, units, responses) {
803
798
  thread: threadOf(first),
804
799
  resolved: resolved.has(first.id),
805
800
  excluded: noComments.has(first.id),
801
+ ...changedOf(ctx, first.id),
806
802
  },
807
803
  };
808
804
  });
@@ -815,6 +811,7 @@ function collectItems(review, commits, units, responses) {
815
811
  out.push({
816
812
  where,
817
813
  item: {
814
+ id: q.id,
818
815
  ...commentSpan(ctx, q),
819
816
  path: where.path,
820
817
  oldPath: where.path,
@@ -824,6 +821,7 @@ function collectItems(review, commits, units, responses) {
824
821
  thread: threadOf(q),
825
822
  resolved: resolved.has(q.id),
826
823
  excluded: noComments.has(q.id),
824
+ ...changedOf(ctx, q.id),
827
825
  },
828
826
  });
829
827
  }
@@ -834,7 +832,8 @@ function collectItems(review, commits, units, responses) {
834
832
  * Excluded ones are present and marked; whoever posts them skips those.
835
833
  */
836
834
  export function reportItems(review, commits, units, opts) {
837
- return collectItems(review, commits, units, opts.responses).map((c) => c.item);
835
+ const session = folded(review, commits, units, opts.earlier);
836
+ return collectItems(session.review, session.ctx, opts.responses).map((c) => c.item);
838
837
  }
839
838
  /** A short list of what the reviewer left out, so nothing vanishes without a trace. */
840
839
  function renderExcluded(collected) {
@@ -850,6 +849,64 @@ function renderExcluded(collected) {
850
849
  out.push("");
851
850
  return out;
852
851
  }
852
+ /**
853
+ * The review of the whole session on this version's diff: the earlier versions' comments on
854
+ * their carried anchors, the concerns kept there that this plan lost added to the commit now
855
+ * taking their unit, and a mark on each whose code changed. Each id appears once; this
856
+ * version's own copy wins.
857
+ */
858
+ function folded(review, commits, units, earlier) {
859
+ if (earlier === undefined)
860
+ return { review, ctx: { commits, units } };
861
+ const marks = new Map();
862
+ const mark = (id, at) => {
863
+ if (at.since !== null)
864
+ marks.set(id, { since: at.since, gone: at.unit === null, path: at.path });
865
+ };
866
+ const owner = (unit, fallback) => {
867
+ const k = commits.findIndex((c) => c.refs.some((r) => r.unit === unit));
868
+ return k === -1 ? Math.min(fallback, Math.max(commits.length - 1, 0)) : k;
869
+ };
870
+ const own = new Set(review.comments.map((c) => c.id));
871
+ const comments = earlier.comments
872
+ .filter((c) => !own.has(c.comment.id))
873
+ .map((c) => {
874
+ mark(c.comment.id, c);
875
+ const { anchor } = c.comment;
876
+ if (anchor.kind !== "hunk")
877
+ return c.comment;
878
+ const moved = anchorOn(c, units, owner(c.unit, anchor.commit));
879
+ return moved === null ? c.comment : { ...c.comment, anchor: moved };
880
+ });
881
+ const inPlan = new Set(commits.flatMap((c) => (c.concerns ?? []).map((k) => k.id)));
882
+ const lost = earlier.concerns.filter((c) => !inPlan.has(c.concern.id));
883
+ for (const c of lost)
884
+ mark(c.concern.id, c);
885
+ const decided = new Set((review.concerns ?? []).map((d) => d.concern));
886
+ return {
887
+ review: {
888
+ ...review,
889
+ comments: [...comments, ...review.comments],
890
+ concerns: [
891
+ ...lost.filter((c) => !decided.has(c.concern.id)).map((c) => c.decision),
892
+ ...(review.concerns ?? []),
893
+ ],
894
+ },
895
+ ctx: {
896
+ units,
897
+ marks,
898
+ commits: commits.map((commit, k) => {
899
+ const mine = lost.filter((c) => owner(c.unit, c.commit) === k);
900
+ if (mine.length === 0)
901
+ return commit;
902
+ return {
903
+ ...commit,
904
+ concerns: [...(commit.concerns ?? []), ...mine.map((c) => concernOn(c, units))],
905
+ };
906
+ }),
907
+ },
908
+ };
909
+ }
853
910
  /**
854
911
  * The consolidated report of a review, in the voices of both the reviewer and the agent: the
855
912
  * reviewer's comments and the agent's concerns the reviewer kept, grouped by file and ordered by
@@ -858,22 +915,24 @@ function renderExcluded(collected) {
858
915
  * Concerns the reviewer has not decided are left out, as are those dismissed.
859
916
  */
860
917
  export function renderReport(full, commits, units, opts) {
861
- const excluded = full.excluded;
918
+ const session = folded(full, commits, units, opts.earlier);
919
+ const { ctx } = session;
920
+ const excluded = session.review.excluded;
862
921
  if (excluded === undefined)
863
- return renderIncluded(full, commits, units, opts);
922
+ return renderIncluded(session.review, ctx, opts);
864
923
  const noComments = new Set(excluded.comments);
865
924
  const noConcerns = new Set(excluded.concerns);
866
925
  const review = {
867
- ...full,
868
- comments: full.comments.filter((c) => !noComments.has(c.id)),
869
- concerns: (full.concerns ?? []).filter((d) => !noConcerns.has(d.concern)),
926
+ ...session.review,
927
+ comments: session.review.comments.filter((c) => !noComments.has(c.id)),
928
+ concerns: (session.review.concerns ?? []).filter((d) => !noConcerns.has(d.concern)),
870
929
  };
871
- const tail = renderExcluded(collectItems(full, commits, units, opts.responses));
872
- const main = renderIncluded(review, commits, units, opts);
930
+ const tail = renderExcluded(collectItems(session.review, ctx, opts.responses));
931
+ const main = renderIncluded(review, ctx, opts);
873
932
  return tail.length === 0 ? main : `${main}${main.endsWith("\n") ? "" : "\n"}${tail.join("\n")}`;
874
933
  }
875
- function renderIncluded(review, commits, units, opts) {
876
- const ctx = { commits, units };
934
+ function renderIncluded(review, ctx, opts) {
935
+ const { commits, units } = ctx;
877
936
  const walk = review.mode === "walk";
878
937
  const fromReviewer = review.comments.filter((c) => c.author !== "agent");
879
938
  // A thread the agent started matters to it only once the reviewer has answered.
@@ -914,47 +973,49 @@ function renderIncluded(review, commits, units, opts) {
914
973
  if (!walk && (feedback.length > 0 || questions.length > 0)) {
915
974
  out.push("Answer every comment id above in `responses` of the next draft, then run `follow-me check` and `follow-me apply`.", "");
916
975
  }
917
- const onContext = review.comments.some((c) => pickRows(ctx, c.anchor)?.touchesContext);
918
- if (onContext)
919
- out.push(CONTEXT_RULE, "");
920
976
  if (!walk && items.length > 0) {
921
- out.push("Fix the code issues on the original branch, then run `follow-me extract` again.", "");
977
+ out.push("Fix the code issues on the original branch, then run `follow-me` again.", "");
922
978
  }
923
979
  return out.join("\n");
924
980
  }
981
+ function indented(body) {
982
+ return ` ${body.split("\n").join("\n ")}`;
983
+ }
984
+ /** "- head: body" for a one-line body, else the body indented under the head. */
985
+ function said(head, body) {
986
+ return body.includes("\n") ? [`${head}:`, indented(body)] : [`${head}: ${body}`];
987
+ }
988
+ /**
989
+ * A new comment carries its short quote, since the agent has not seen it; everything else is one
990
+ * line naming its id, because the agent already has the comment it refers to.
991
+ */
925
992
  function renderEvent(event, ctx) {
926
993
  switch (event.type) {
927
- case "comment":
928
- case "comment-updated": {
994
+ case "comment": {
929
995
  const { comment: c } = event;
930
- const what = c.kind === "question" ? "question" : "comment";
931
- const verb = event.type === "comment" ? "new" : "edited";
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}`));
936
- if (c.context.path)
937
- lines.push(` - file: ${c.context.path}`);
938
- if (c.severity)
939
- lines.push(` - severity: ${c.severity}`);
996
+ const what = c.kind === "question" ? "question" : (c.severity ?? "comment");
997
+ const lines = [`- ${c.id} ${what} on ${describeAnchor(c.anchor, ctx)}`];
940
998
  if (c.follows)
941
- lines.push(` - asks again after ${c.follows}`);
942
- lines.push(` - ${c.body.split("\n").join("\n ")}`);
999
+ lines.push(` asks again after ${c.follows}`);
1000
+ lines.push(...shortQuote(anchorRows(ctx, c.anchor)).map((r) => ` ${r}`));
1001
+ lines.push(indented(c.body));
943
1002
  return lines;
944
1003
  }
1004
+ case "comment-updated":
1005
+ return said(`- ${event.comment.id} edited`, event.comment.body);
945
1006
  case "comment-deleted":
946
- return [`- comment ${event.id} was deleted`];
1007
+ return [`- ${event.id} deleted`];
947
1008
  case "reply":
948
- return [`- ${event.reply.author} replied under ${event.commentId}: ${event.reply.body}`];
1009
+ return said(`- ${event.commentId} ${event.reply.author} replied`, event.reply.body);
949
1010
  case "concern": {
950
1011
  const severity = event.severity === undefined ? "" : ` as ${event.severity}`;
951
1012
  const note = event.note === undefined ? "" : `: ${event.note}`;
952
- return [`- concern ${event.concernId} ${event.decision}${severity}${note}`];
1013
+ return [`- ${event.concernId} ${event.decision}${severity}${note}`];
953
1014
  }
954
1015
  case "resolved":
955
- return [`- question ${event.commentId} marked ${event.resolved ? "resolved" : "open again"}`];
1016
+ return [`- ${event.commentId} ${event.resolved ? "resolved" : "reopened"}`];
956
1017
  case "version":
957
- return [`- plan v${event.version} is now open`];
1018
+ return [`- plan v${event.version} is open`];
958
1019
  case "submitted":
959
1020
  return ["- the reviewer submitted the review"];
960
1021
  case "ended":
@@ -965,11 +1026,42 @@ function renderEvent(event, ctx) {
965
1026
  }
966
1027
  }
967
1028
  }
968
- /** A markdown digest of what happened on the review page, for `follow-me watch`. */
1029
+ /** What happened on the review page, for the entry and `follow-me watch`. */
969
1030
  export function renderEvents(events, ctx = null) {
970
- const first = events[0]?.seq;
971
- const last = events.at(-1)?.seq;
972
- const range = first === last ? `event ${first}` : `events ${first}-${last}`;
973
- const body = events.flatMap((event) => renderEvent(event, ctx));
974
- return [`# Review activity (${range})`, "", ...body, ""].join("\n");
1031
+ return `${events.flatMap((event) => renderEvent(event, ctx)).join("\n")}\n`;
1032
+ }
1033
+ /** A unit as prompt.md prints it, and which commits of the plan take its lines. */
1034
+ export function renderShownUnit(unit, commits) {
1035
+ const taken = commits.flatMap((commit, k) => commit.refs
1036
+ .filter((ref) => ref.unit === unit.id)
1037
+ .map((ref) => `commit ${k + 1} takes ${formatSelection(ref.unit, ref.select)}`));
1038
+ const where = taken.length === 0 ? "in no commit of the plan" : taken.join("; ");
1039
+ return `${renderUnit(unit)}\n${where}\n`;
1040
+ }
1041
+ /** A comment with every row it was made on, its words and the whole thread under it. */
1042
+ export function renderShownComment(c, ctx, version) {
1043
+ const what = c.kind === "question" ? "question" : (c.severity ?? "comment");
1044
+ const lines = [`${c.id} ${what} on plan v${version}, on ${describeAnchor(c.anchor, ctx)}`];
1045
+ if (c.follows)
1046
+ lines.push(`asks again after ${c.follows}`);
1047
+ const rows = anchorRows(ctx, c.anchor);
1048
+ const quoted = rows.length > 0 ? rows : (c.context.lines ?? []).map((l) => `> ${l}`);
1049
+ lines.push(...quoted.map((r) => ` ${r}`));
1050
+ lines.push(...said(c.author === "agent" ? "agent" : "reviewer", c.body));
1051
+ for (const reply of c.replies ?? []) {
1052
+ const diagram = reply.diagram === undefined ? "" : " (with a diagram)";
1053
+ lines.push(...said(`${reply.author} replied${diagram}`, reply.body));
1054
+ }
1055
+ return `${lines.join("\n")}\n`;
1056
+ }
1057
+ /** A concern of the plan with the lines it is pinned to, as its commit writes them. */
1058
+ export function renderShownConcern(concern, commit, ctx) {
1059
+ const where = whereConcern(ctx, { concern, commit, severity: concern.severity, note: null });
1060
+ const place = where.path === null ? where.label : `${where.path} ${where.label}`;
1061
+ return [
1062
+ `${concern.id} ${concern.severity} ${concern.kind} in commit ${commit + 1}, ${place}`,
1063
+ ...where.quote.map((r) => ` ${r}`),
1064
+ concern.text,
1065
+ "",
1066
+ ].join("\n");
975
1067
  }