@shiyuphan/follow-me 0.5.1 → 0.7.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 (102) hide show
  1. package/README.md +9 -3
  2. package/dist/src/cli.js +112 -19
  3. package/dist/src/comments.js +6 -1
  4. package/dist/src/coverage.js +397 -0
  5. package/dist/src/definition.js +226 -0
  6. package/dist/src/git.js +87 -13
  7. package/dist/src/host.js +4 -0
  8. package/dist/src/plan.js +12 -1
  9. package/dist/src/prompt.js +80 -23
  10. package/dist/src/server.js +65 -8
  11. package/dist/src/session.js +33 -0
  12. package/dist/src/types.js +4 -0
  13. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-8UsLPznO.js → abnfDiagram-WEYOIA3C-cHEJvcBn.js} +1 -1
  14. package/dist/ui/dist/assets/{arc-vovjGalz.js → arc-xolFyc0v.js} +1 -1
  15. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-BBMK-PUu.js → architecture-WOLXFQ4H-B3hvvNV2.js} +1 -1
  16. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-Bw_ehaQk.js → architectureDiagram-ZHVYKT53-BNTMbPJW.js} +1 -1
  17. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-CAmD0wKf.js → blockDiagram-RJAE7QA3-NYY6qeuY.js} +1 -1
  18. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-DCP-3zgt.js → c4Diagram-XDPPPE54-B_bNpXHQ.js} +1 -1
  19. package/dist/ui/dist/assets/channel-DPDY0XJP.js +1 -0
  20. package/dist/ui/dist/assets/{chunk-2E7WHVMD-BiHSeJMu.js → chunk-2E7WHVMD-BTet9STn.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-4EMNKR2T-CR3lHKf_.js → chunk-4EMNKR2T-qoQt4Fpx.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-55XCU76K-CtaoloTT.js → chunk-55XCU76K-C3I6EB-H.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-7S6XSTFO-eABdc7fu.js → chunk-7S6XSTFO-CL-9s_8p.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-AQDTSKZB-CPlbJWi-.js → chunk-AQDTSKZB-DHoGnGyP.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-YiMuxsb-.js → chunk-DSNLSHJZ-Bk8WO5JF.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-HAGFOTED-1VcdL78a.js → chunk-HAGFOTED-CwAC85lg.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-HNE4Q36P-u-sr_0Qu.js → chunk-HNE4Q36P-CIllywWr.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-NQOKRVIF-C_QlWSze.js → chunk-NQOKRVIF-DDlpyYiH.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-UHW7PFBP-CNuZhsbU.js → chunk-UHW7PFBP-Cxdndcon.js} +1 -1
  30. package/dist/ui/dist/assets/{chunk-VQ5W62QI-DAHWbLqU.js → chunk-VQ5W62QI-CL9d9_iJ.js} +1 -1
  31. package/dist/ui/dist/assets/{chunk-WQQL4CDK-DWQnxFCd.js → chunk-WQQL4CDK-C4kzN8QU.js} +1 -1
  32. package/dist/ui/dist/assets/{chunk-XXDRQBXY-B0lGgSOR.js → chunk-XXDRQBXY-Cv_2FSiY.js} +1 -1
  33. package/dist/ui/dist/assets/{chunk-YZO67HFG-BDw7efkN.js → chunk-YZO67HFG-oHiuYaCJ.js} +1 -1
  34. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-B5wfHsZv.js → chunk-ZAUX2LTX-Cy_8d0XT.js} +1 -1
  35. package/dist/ui/dist/assets/{chunk-ZCHMQSOR--rWtYtAG.js → chunk-ZCHMQSOR-piIpIcQi.js} +1 -1
  36. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-BKuQ_xoS.js → classDiagram-v2-4MYWR3UA-DNKRIujO.js} +1 -1
  37. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-Dk5fIpU0.js → cose-bilkent-JH36ORCC-BWKTGCHY.js} +1 -1
  38. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-DIykJb2g.js → cynefin-EF2NZ3EQ-yx7KKkAk.js} +1 -1
  39. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-Cg8tBqUb.js → cynefinDiagram-WF24UDRP-BTnZ7QVL.js} +1 -1
  40. package/dist/ui/dist/assets/{dagre-NK4GIZVX-BC_tnnjz.js → dagre-NK4GIZVX-CPh5Zn5I.js} +1 -1
  41. package/dist/ui/dist/assets/{diagram-CBK3MWTC-B_MQGixU.js → diagram-CBK3MWTC-pI6NFdrD.js} +1 -1
  42. package/dist/ui/dist/assets/{diagram-I5XIIV2D-DO681pZ5.js → diagram-I5XIIV2D-DUIfARoS.js} +1 -1
  43. package/dist/ui/dist/assets/{diagram-ODIL2UCW-D14jLzU6.js → diagram-ODIL2UCW-lafSPvAw.js} +1 -1
  44. package/dist/ui/dist/assets/{diagram-PFBEJUXE-cML48gUS.js → diagram-PFBEJUXE-u1lZas3a.js} +1 -1
  45. package/dist/ui/dist/assets/{diagram-RJA3I4VV-DSoq8jFk.js → diagram-RJA3I4VV-mc0GLMHo.js} +1 -1
  46. package/dist/ui/dist/assets/{diagram-TYUNFSNR-CaJ4pvnl.js → diagram-TYUNFSNR-1NRbBrZW.js} +1 -1
  47. package/dist/ui/dist/assets/{dist-D3j-DODP.js → dist-CHZ9mrl_.js} +1 -1
  48. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-CyRpjSix.js → ebnfDiagram-V4P3TTPT-DdStjTDc.js} +1 -1
  49. package/dist/ui/dist/assets/{elk-QONTBCTQ-ICJdzVFL.js → elk-QONTBCTQ-BzRoFO45.js} +1 -1
  50. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-B57hNTfw.js → erDiagram-KYQIGQAA-DlMkgQxj.js} +1 -1
  51. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BG8tUVIQ.js → eventmodeling-K75KTNOO-BRYIOrod.js} +1 -1
  52. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-C1IBh_GZ.js +1 -0
  53. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-Czt6Klik.js → ganttDiagram-R7GPNTJY-D8fQZ7Ah.js} +1 -1
  54. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-De_ZWJLj.js → gitGraph-VSP46ZUC-C7ZydyWX.js} +1 -1
  55. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-BMCJoOnT.js → gitGraphDiagram-PDYL7XHY-DCItam0v.js} +1 -1
  56. package/dist/ui/dist/assets/index-8BehwQ7E.js +134 -0
  57. package/dist/ui/dist/assets/index-mC2OW2Av.css +1 -0
  58. package/dist/ui/dist/assets/{info-OHQRW6UA-D75H9kUq.js → info-OHQRW6UA-C3KneBKT.js} +1 -1
  59. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-Bsm_I9dt.js → infoDiagram-MVDRKAX5-6wK09D32.js} +1 -1
  60. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-98xZAJ9y.js → ishikawaDiagram-HPEHV2GE-DTP5C8VX.js} +1 -1
  61. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-BWVGuc0P.js → journeyDiagram-BOSHW7O7-B--UIMtq.js} +1 -1
  62. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DgUE_KDd.js → kanban-definition-FNSA4HC5-Ku4N9QGt.js} +1 -1
  63. package/dist/ui/dist/assets/{line-i__8Rvas.js → line-C4-H5h1-.js} +1 -1
  64. package/dist/ui/dist/assets/{linear-BXKj0Fip.js → linear-DhAID3hm.js} +1 -1
  65. package/dist/ui/dist/assets/{mermaid-parser.core-DxvaorTa.js → mermaid-parser.core-CMPNc-u6.js} +3 -3
  66. package/dist/ui/dist/assets/{mermaid.core-ujYTsRZz.js → mermaid.core-Nd_xOg2y.js} +4 -4
  67. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-B7QD6sjk.js → mindmap-definition-6OY57UG3-BlVsym5h.js} +1 -1
  68. package/dist/ui/dist/assets/{packet-JDAUHWVQ-B6aIwfD1.js → packet-JDAUHWVQ-rsk7SM7S.js} +1 -1
  69. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-_RStHj-a.js → pegDiagram-ECIUJNBX-C0L20M8z.js} +1 -1
  70. package/dist/ui/dist/assets/{pie-XZMESJXO-CNLC-pTe.js → pie-XZMESJXO-B23-iZSN.js} +1 -1
  71. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-0uT7MT-u.js → pieDiagram-GNMPY74T-BqFhe4Oh.js} +1 -1
  72. package/dist/ui/dist/assets/{prism-core-DtxziK1i.js → prism-core-DYsiqJeR.js} +1 -1
  73. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-CTMKb5hl.js → quadrantDiagram-D57K4MC3-BKP75B-m.js} +1 -1
  74. package/dist/ui/dist/assets/{radar-ABXABTNO-D53h6I0N.js → radar-ABXABTNO-BrEk2mQ7.js} +1 -1
  75. package/dist/ui/dist/assets/{railroad-I3PHUGI6-CwjhW4mc.js → railroad-I3PHUGI6-wWO4Eapc.js} +1 -1
  76. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DDbQ3RmB.js → railroad-abnf-LNEFI6M7-DfY-IPvE.js} +1 -1
  77. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-Am7YJAqO.js → railroad-ebnf-DWYD2UWJ-19wedWGk.js} +1 -1
  78. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Ci32KoVW.js → railroad-peg-JR7BVNR7-9vIslnoJ.js} +1 -1
  79. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-BPU5A6XW.js → railroadDiagram-Y72NRBL7-DVnmXs8w.js} +1 -1
  80. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-_0Pq4QdV.js → requirementDiagram-EQYSW5RA-BEoMdlzR.js} +1 -1
  81. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-c9h-7uf8.js → sankeyDiagram-SY24JHO5-D7Tn2Gbs.js} +1 -1
  82. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-B9-S4Rc9.js → sequenceDiagram-BLEHNHQU-CyjUDBaQ.js} +1 -1
  83. package/dist/ui/dist/assets/{src-BZ3kG7lO.js → src-BiC9XNcI.js} +1 -1
  84. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-py1smSen.js → stateDiagram-v2-A6IESKWZ-CMp1VCO9.js} +1 -1
  85. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-C1E90aH4.js → swimlanes-B3ENRKFQ-KEciLd6r.js} +1 -1
  86. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DzestpDE.js +8 -0
  87. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-DNe1To6B.js → timeline-definition-JXYKGLGB-C8YQtv7M.js} +1 -1
  88. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-D6MG89i6.js → treeView-D4JQ5SDB-DgAzPaOO.js} +1 -1
  89. package/dist/ui/dist/assets/{treemap-SAJKECNS-CZpt7Mlh.js → treemap-SAJKECNS-eCRqZMYc.js} +1 -1
  90. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-CKsv-03M.js → usecaseDiagram-WPOLBNNX-Dirryw9G.js} +1 -1
  91. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-DGJ1qlyJ.js → vennDiagram-Q3Y3IDCW-BjxQbAUx.js} +1 -1
  92. package/dist/ui/dist/assets/{wardley-7MLQ67FV-ClOzBemC.js → wardley-7MLQ67FV-CqHX5RUz.js} +1 -1
  93. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-C68ahPyF.js → wardleyDiagram-IVAIDKWA-Ds1HoEUY.js} +1 -1
  94. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-DWDamCoH.js → xychartDiagram-4URNHDU4-kodzDb12.js} +1 -1
  95. package/dist/ui/dist/index.html +2 -2
  96. package/package.json +1 -1
  97. package/skills/follow-me/SKILL.md +5 -1
  98. package/dist/ui/dist/assets/channel-B7CT_oIp.js +0 -1
  99. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU--HPNoVQJ.js +0 -1
  100. package/dist/ui/dist/assets/index-B8TYcxF8.css +0 -1
  101. package/dist/ui/dist/assets/index-CYKbk4QS.js +0 -134
  102. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-I98Q8AqP.js +0 -8
package/README.md CHANGED
@@ -69,8 +69,14 @@ If the hashes differ, `apply` exits with code 1. The commits are signed when you
69
69
 
70
70
  The red blob in the party hat is your branch tip. Nothing follow-me does moves it.
71
71
 
72
- ![The review page: one scrolling document of commits with the stack dropdown in the sticky header, a picked line with its Ask box, and an inline thread where the agent answers in Markdown with a diagram](assets/screenshots/review.png)
73
- *The review page. Pick any line, changed or not, or a range; Ask or Flag opens right under it. The agent answers inline in Markdown.*
72
+ ![The review page: the index on the left groups commits by section in top-down order, and commit 2 shows a question on a line with the agent's answer inline](assets/screenshots/review.png)
73
+ *The review page. The index groups the commits the way they read: the story first, then the steps it calls, then the details. Pick any line, changed or not, and Ask or Flag right under it; the agent answers inline.*
74
+
75
+ ![Cmd-clicking format in pow opens a peek of its definition, which lives outside this branch](assets/screenshots/definition.png)
76
+ *Cmd-click a name to jump to where it is defined, even in a later commit, and Alt+Left to come back. A definition the branch does not change opens as a peek.*
77
+
78
+ ![Coverage while reading: gutter bars mark covered, partly covered and uncovered lines, with counts per commit, per file and for the branch, and a popover listing the test that ran a line](assets/screenshots/coverage.png)
79
+ *Coverage while reading. Your agent runs the tests with coverage and passes the lcov to `follow-me coverage`; follow-me never runs anything. Bars show covered, partly covered and uncovered lines, `x` jumps to the next uncovered line, and clicking a bar lists the tests that ran it.*
74
80
 
75
81
  ![A diagram opened in the lightbox at full size](assets/screenshots/diagram.png)
76
82
  *Diagrams sit full width under the commit or reply they explain, and open full size on click.*
@@ -78,7 +84,7 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
78
84
  ![The hand-off page: every kept item with its original-branch path and line, an include checkbox, and copy buttons](assets/screenshots/handoff.png)
79
85
  *Submit review. Each item carries the original branch's path and line, so your agent can post it to the merge request as is.*
80
86
 
81
- Keys: `j` and `k` move between commits, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
87
+ Keys: `[` shows or hides the index, `j` and `k` move between commits, Cmd-click (Ctrl-click) goes to a definition and Alt+Left comes back, arrows and Shift+arrows pick rows inside a hunk, `c` comments on the pick, `v` marks a commit viewed, `g` shows or hides coverage and `x` jumps to the next uncovered line, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
82
88
 
83
89
  ### Reviewing someone else's branch
84
90
 
package/dist/src/cli.js CHANGED
@@ -1,21 +1,27 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { readFileSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join, relative } from "node:path";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { changedLines, LcovError, parseLcov, parseLocArg, recordRun, runOf, tipPlacer, totalOf, } from "./coverage.js";
7
+ import { definitionIndex, isIdentifier } from "./definition.js";
6
8
  import { DiffParseError, formatUnitRef, isUnitId } from "./diff.js";
7
- import { BranchRefusedError, branchHead, GitError, isOwnWork, signing, treeOf, writeBranch, } from "./git.js";
8
- import { fetchCarried, fetchComments, fetchReport, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postReply, probeHealth, readServerRecord, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
9
+ import { BranchRefusedError, branchHead, GitError, isOwnWork, pathsAt, pathsChanged, signing, treeOf, writeBranch, } from "./git.js";
10
+ import { fetchCarried, fetchComments, fetchReport, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postCoverage, postReply, probeHealth, readServerRecord, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
9
11
  import { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } from "./install.js";
10
12
  import { naiveDraft } from "./naive.js";
11
13
  import { isCommentId, isConcernId, isFields, serializePlanFile, validatePlan, } from "./plan.js";
12
- import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownUnit, reportItems, } from "./prompt.js";
14
+ import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownDefinitions, renderShownUnit, reportItems, } from "./prompt.js";
13
15
  import { RegistrationError, sessionIdOf } from "./server.js";
14
- import { BranchMovedError, cleanSessions, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
16
+ import { BranchMovedError, cleanSessions, coverageAt, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
15
17
  import { checkSyntax } from "./syntax.js";
16
18
  class UsageError extends Error {
17
19
  name = "UsageError";
18
20
  }
21
+ /** A test run follow-me will not record, with the reason the agent can act on. */
22
+ class CoverageError extends Error {
23
+ name = "CoverageError";
24
+ }
19
25
  const SHORT_USAGE = `usage: follow-me [--wait <seconds>] [--branch <name>]
20
26
 
21
27
  Run follow-me with no command. Each call does the next thing that is missing and returns: it
@@ -30,11 +36,14 @@ Other commands:
30
36
  Mermaid source
31
37
  explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
32
38
  start a thread on the page without being asked
33
- show <h_…|c_…|k_…|path:line[@n|@base]> print a unit, a comment's thread, a concern, or the
34
- lines around path:line in commit n, instead of
35
- reading files
39
+ show <h_…|c_…|k_…|path:line[@n|@base]|name>
40
+ print a unit, a comment's thread, a concern, the
41
+ lines around path:line in commit n, or where a name
42
+ is defined, instead of reading files
36
43
  report [--json] [--markdown] print the report of the latest review; --json lists
37
44
  the items to post to a merge request
45
+ coverage <lcov> [--test <id> [--at <path:line>]]
46
+ record a test run taken at the tip for the page
38
47
 
39
48
  help --all lists every command.`;
40
49
  const USAGE = `usage: follow-me <command> [options]
@@ -85,17 +94,24 @@ Interactive loop (while the review page is open):
85
94
  --lines are change lines of the unit, commits count
86
95
  from 1; with no target it is about the whole plan.
87
96
  watch leaves these out like your replies
88
- show <h_…|c_…|k_…|path:line[@n|@base]> [--branch <name>]
97
+ show <h_…|c_…|k_…|path:line[@n|@base]|name> [--branch <name>]
89
98
  print a unit with the commits that take it, a
90
99
  comment with its rows and thread, a concern with its
91
- lines, or the lines around path:line at the tip, in
92
- commit n of the plan, or in the base
100
+ lines, the lines around path:line at the tip, in
101
+ commit n of the plan, or in the base, or each place
102
+ a name is defined at the tip with the commit it
103
+ lands in
93
104
  report [--json] [--markdown] [--branch <name>]
94
105
  print the report of the open review, or of the last
95
106
  one that ended. Markdown lists issues by file and
96
107
  line in both voices, then questions with answers;
97
108
  --json lists the items to post to a merge request
98
109
  (path, lines, severity, body, thread, excluded)
110
+ coverage <lcov> [--test <id> [--at <path:line>]] [--branch <name>]
111
+ record the lcov of a test run at the branch tip;
112
+ refuses when a covered file in the working tree
113
+ differs from the tip. A plain run replaces the last;
114
+ --test runs add one test each, --at is where it is
99
115
  apply, while the page is open, swaps it to the new plan version in place, same URL.
100
116
 
101
117
  Old names, still working:
@@ -905,8 +921,8 @@ function reviewedPlan(session) {
905
921
  return null;
906
922
  }
907
923
  /** Prints the finished review's report, which is what the agent works from from here on. */
908
- function finished(session, stored) {
909
- const text = renderStored(session, stored);
924
+ async function finished(session, stored) {
925
+ const text = await renderStored(session, stored);
910
926
  if (text === null)
911
927
  return 1;
912
928
  // A review that swapped versions holds what its earlier versions said too.
@@ -1169,6 +1185,9 @@ async function entry(cwd, args) {
1169
1185
  const result = await hostReview(session, mode, detached, async () => prep, reportSaved);
1170
1186
  if (result.kind !== "elsewhere")
1171
1187
  return exitCode(result);
1188
+ if (session.coverage() === null) {
1189
+ next("if the project has tests, run them with coverage at the tip and pass the lcov to follow-me coverage");
1190
+ }
1172
1191
  say("follow-me is interactive: keep running it until it prints the report");
1173
1192
  return listen(session, result, args);
1174
1193
  }
@@ -1226,7 +1245,7 @@ async function walkPlan(session, args) {
1226
1245
  return alreadyReviewed(session, latest.version, args) ? null : latest;
1227
1246
  }
1228
1247
  /** The consolidated report of a saved review, or null after saying why it cannot be rendered. */
1229
- function renderStored(session, stored, format = "markdown") {
1248
+ async function renderStored(session, stored, format = "markdown") {
1230
1249
  const plan = session.plans().find((p) => p.version === stored.version);
1231
1250
  if (plan === undefined) {
1232
1251
  console.error(`follow-me: review v${stored.version} refers to a plan that is missing`);
@@ -1254,7 +1273,13 @@ function renderStored(session, stored, format = "markdown") {
1254
1273
  concerns: stored.review.carriedConcerns ?? [],
1255
1274
  },
1256
1275
  };
1257
- const opts = { branch: session.anchor.branch, responses, ...earlier };
1276
+ const coverage = await coverageAt(session.root, session.dir, session.anchor.tip);
1277
+ const opts = {
1278
+ branch: session.anchor.branch,
1279
+ responses,
1280
+ ...earlier,
1281
+ ...(coverage === null ? {} : { coverage }),
1282
+ };
1258
1283
  if (format === "json") {
1259
1284
  const items = reportItems(stored.review, verdict.plan.commits, session.diff.units, opts);
1260
1285
  return `${JSON.stringify(items, null, 2)}\n`;
@@ -1284,12 +1309,67 @@ async function report(cwd, args, walkOnly = false) {
1284
1309
  console.error("follow-me: no review is open and none has ended; run `follow-me review` first");
1285
1310
  return 1;
1286
1311
  }
1287
- const text = renderStored(session, stored, format);
1312
+ const text = await renderStored(session, stored, format);
1288
1313
  if (text === null)
1289
1314
  return 1;
1290
1315
  process.stdout.write(text);
1291
1316
  return 0;
1292
1317
  }
1318
+ const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]]";
1319
+ /**
1320
+ * Records the lcov of a test run the agent made at the tip in the session's coverage.json and
1321
+ * prints how much of the branch's changed code it ran. follow-me never runs the tests itself.
1322
+ */
1323
+ async function coverage(cwd, args) {
1324
+ const [file, extra] = args.positionals;
1325
+ const id = args.values.get("test");
1326
+ const atArg = args.values.get("at");
1327
+ if (file === undefined || extra !== undefined)
1328
+ throw new UsageError(COVERAGE_USAGE);
1329
+ if (atArg !== undefined && id === undefined) {
1330
+ throw new UsageError(`--at needs --test\n${COVERAGE_USAGE}`);
1331
+ }
1332
+ let text;
1333
+ try {
1334
+ text = readFileSync(resolve(cwd, file), "utf8");
1335
+ }
1336
+ catch {
1337
+ throw new UsageError(`cannot read ${file}`);
1338
+ }
1339
+ const records = parseLcov(text);
1340
+ const session = await openFresh(cwd, args);
1341
+ const { tip } = session.anchor;
1342
+ const place = tipPlacer(session.root, cwd, await pathsAt(session.root, tip));
1343
+ const run = runOf(records, place);
1344
+ if (run.files.size === 0) {
1345
+ throw new CoverageError(`none of the ${plural(new Set(records.map((r) => r.path)).size, "file")} in ${file} is in ${session.anchor.branch} at the tip; run follow-me coverage from the directory the tests ran in`);
1346
+ }
1347
+ const changed = await pathsChanged(session.root, tip, null);
1348
+ const differ = [...run.files.keys()].filter((p) => changed.has(p));
1349
+ if (differ.length > 0) {
1350
+ throw new CoverageError(`coverage must be taken on the tip; these files differ: ${differ.map(show).join(", ")}`);
1351
+ }
1352
+ let test;
1353
+ if (id !== undefined) {
1354
+ const at = atArg === undefined ? undefined : parseLocArg(atArg, place);
1355
+ if (typeof at === "string")
1356
+ throw new UsageError(at);
1357
+ test = { id, ...(at === undefined ? {} : { at }) };
1358
+ }
1359
+ const takenAt = new Date().toISOString();
1360
+ const recorded = recordRun(session.coverage(), run, {
1361
+ tip,
1362
+ takenAt,
1363
+ ...(test === undefined ? {} : { test }),
1364
+ });
1365
+ session.writeCoverage(recorded);
1366
+ const open = await openOnServer(session, null);
1367
+ if (open !== null)
1368
+ await postCoverage(open.port, open.summary);
1369
+ const { covered, executable } = totalOf(changedLines(session.diff.units, recorded.files).values());
1370
+ say(`coverage for ${plural(recorded.files.size, "file")} at ${short(tip)}; ${covered} of ${executable} changed lines covered`);
1371
+ return 0;
1372
+ }
1293
1373
  /** The old name of report. */
1294
1374
  async function feedback(cwd, args) {
1295
1375
  console.error("follow-me: feedback is now report; printing the same");
@@ -1298,9 +1378,12 @@ async function feedback(cwd, args) {
1298
1378
  function isCommentEvent(event) {
1299
1379
  return event.type === "comment" || event.type === "comment-updated";
1300
1380
  }
1301
- /** Events the reviewer caused; the agent's own replies, explanations and version swaps are not news. */
1381
+ /**
1382
+ * Events the reviewer caused; the agent's own replies, explanations, version swaps and coverage
1383
+ * are not news.
1384
+ */
1302
1385
  function isNews(event) {
1303
- if (event.type === "version")
1386
+ if (event.type === "version" || event.type === "coverage")
1304
1387
  return false;
1305
1388
  if (event.type === "comment" || event.type === "comment-updated") {
1306
1389
  return event.comment.author !== "agent";
@@ -1516,7 +1599,7 @@ async function explain(cwd, args) {
1516
1599
  next(`run follow-me again; ${STILL_WORKING}`);
1517
1600
  return 0;
1518
1601
  }
1519
- const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:line[@<commit>|@base]>";
1602
+ const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:line[@<commit>|@base]|name>";
1520
1603
  /** Every comment the session has, with the plan version it was made on: live ones first. */
1521
1604
  async function knownComments(session) {
1522
1605
  const open = await openOnServer(session, null);
@@ -1629,6 +1712,12 @@ async function showRef(cwd, args) {
1629
1712
  }
1630
1713
  throw new UsageError(`no concern ${ref} in a plan that fits the branch`);
1631
1714
  }
1715
+ if (isIdentifier(ref)) {
1716
+ const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
1717
+ const index = definitionIndex(session.root, session.diff.tip, units, commits);
1718
+ process.stdout.write(renderShownDefinitions(ref, await index.lookup(ref, null)));
1719
+ return 0;
1720
+ }
1632
1721
  process.stdout.write(showLines(session, ref));
1633
1722
  return 0;
1634
1723
  }
@@ -1868,6 +1957,8 @@ export async function main(argv, cwd) {
1868
1957
  }
1869
1958
  case "show":
1870
1959
  return await showRef(cwd, parseArgs(rest, { values: ["branch"] }));
1960
+ case "coverage":
1961
+ return await coverage(cwd, parseArgs(rest, { values: ["test", "at", "branch"] }));
1871
1962
  case "report":
1872
1963
  return await report(cwd, parseArgs(rest, { values: ["branch"], flags: ["json", "markdown"] }));
1873
1964
  case "clean": {
@@ -1908,6 +1999,8 @@ export async function main(argv, cwd) {
1908
1999
  catch (error) {
1909
2000
  const known = [
1910
2001
  UsageError,
2002
+ CoverageError,
2003
+ LcovError,
1911
2004
  BranchMovedError,
1912
2005
  BranchRefusedError,
1913
2006
  SessionFileError,
@@ -1,4 +1,4 @@
1
- import { isRepoPath, isUnitId } from "./diff.js";
1
+ import { isRepoPath, isSha, isUnitId } from "./diff.js";
2
2
  import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
3
3
  /**
4
4
  * The wire formats of review state, parsed once at the boundary: comments, replies, concern
@@ -452,6 +452,11 @@ export function parseEvent(v) {
452
452
  return carried;
453
453
  return { seq, type: v.type, version: v.version, hash: v.hash, carried };
454
454
  }
455
+ case "coverage":
456
+ if (typeof v.tip !== "string" || !isSha(v.tip) || typeof v.takenAt !== "string") {
457
+ return "malformed coverage event";
458
+ }
459
+ return { seq, type: v.type, tip: v.tip, takenAt: v.takenAt };
455
460
  case "submitted":
456
461
  return { seq, type: v.type };
457
462
  case "ended":