@shiyuphan/follow-me 0.7.1 → 0.9.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.
Files changed (100) hide show
  1. package/README.md +9 -1
  2. package/dist/src/cli.js +181 -42
  3. package/dist/src/comments.js +215 -9
  4. package/dist/src/coverage.js +163 -25
  5. package/dist/src/host.js +23 -0
  6. package/dist/src/plan.js +128 -6
  7. package/dist/src/prompt.js +55 -2
  8. package/dist/src/refs.js +239 -0
  9. package/dist/src/server.js +65 -3
  10. package/dist/src/session.js +1 -0
  11. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-cHEJvcBn.js → abnfDiagram-WEYOIA3C-DztIEwNE.js} +1 -1
  12. package/dist/ui/dist/assets/{arc-xolFyc0v.js → arc-DUYh-EqZ.js} +1 -1
  13. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-B3hvvNV2.js → architecture-WOLXFQ4H-BQYTB92_.js} +1 -1
  14. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-BNTMbPJW.js → architectureDiagram-ZHVYKT53-Dl7095A5.js} +1 -1
  15. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-NYY6qeuY.js → blockDiagram-RJAE7QA3-QDZ5kE1J.js} +1 -1
  16. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-B_bNpXHQ.js → c4Diagram-XDPPPE54-CKJZ1xOw.js} +1 -1
  17. package/dist/ui/dist/assets/channel-DF08Hd8f.js +1 -0
  18. package/dist/ui/dist/assets/{chunk-2E7WHVMD-BTet9STn.js → chunk-2E7WHVMD-BXBjaqd1.js} +1 -1
  19. package/dist/ui/dist/assets/{chunk-4EMNKR2T-qoQt4Fpx.js → chunk-4EMNKR2T-DPzfVuke.js} +1 -1
  20. package/dist/ui/dist/assets/{chunk-55XCU76K-C3I6EB-H.js → chunk-55XCU76K-B33Sj3w8.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-7S6XSTFO-CL-9s_8p.js → chunk-7S6XSTFO-B_u7dS03.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-AQDTSKZB-DHoGnGyP.js → chunk-AQDTSKZB-B4EzH5gx.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-Bk8WO5JF.js → chunk-DSNLSHJZ-CfWeHABr.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-HAGFOTED-CwAC85lg.js → chunk-HAGFOTED-B8G8PjLZ.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-HNE4Q36P-CIllywWr.js → chunk-HNE4Q36P-BA-PqoTF.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-NQOKRVIF-DDlpyYiH.js → chunk-NQOKRVIF-JvhuFcnR.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-UHW7PFBP-Cxdndcon.js → chunk-UHW7PFBP-BrfL5CGY.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-VQ5W62QI-CL9d9_iJ.js → chunk-VQ5W62QI-dBJqgdDy.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-WQQL4CDK-C4kzN8QU.js → chunk-WQQL4CDK-dfqd3k_P.js} +1 -1
  30. package/dist/ui/dist/assets/{chunk-XXDRQBXY-Cv_2FSiY.js → chunk-XXDRQBXY-kViWdB9M.js} +1 -1
  31. package/dist/ui/dist/assets/{chunk-YZO67HFG-oHiuYaCJ.js → chunk-YZO67HFG-ukfs4wT3.js} +1 -1
  32. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-Cy_8d0XT.js → chunk-ZAUX2LTX-BxZHki81.js} +1 -1
  33. package/dist/ui/dist/assets/{chunk-ZCHMQSOR-piIpIcQi.js → chunk-ZCHMQSOR-PjluhR_E.js} +1 -1
  34. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-DNKRIujO.js → classDiagram-v2-4MYWR3UA-Ct89LJec.js} +1 -1
  35. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-BWKTGCHY.js → cose-bilkent-JH36ORCC-BnQceGmB.js} +1 -1
  36. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-yx7KKkAk.js → cynefin-EF2NZ3EQ-D6WkCVE8.js} +1 -1
  37. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-BTnZ7QVL.js → cynefinDiagram-WF24UDRP-dn6DUPAb.js} +1 -1
  38. package/dist/ui/dist/assets/{dagre-NK4GIZVX-CPh5Zn5I.js → dagre-NK4GIZVX-BtZSlLk7.js} +1 -1
  39. package/dist/ui/dist/assets/{diagram-CBK3MWTC-pI6NFdrD.js → diagram-CBK3MWTC-Ds4_tv6X.js} +1 -1
  40. package/dist/ui/dist/assets/{diagram-I5XIIV2D-DUIfARoS.js → diagram-I5XIIV2D-DcoVkg45.js} +1 -1
  41. package/dist/ui/dist/assets/{diagram-ODIL2UCW-lafSPvAw.js → diagram-ODIL2UCW-T47FAA3k.js} +1 -1
  42. package/dist/ui/dist/assets/{diagram-PFBEJUXE-u1lZas3a.js → diagram-PFBEJUXE-CRZcaVKP.js} +1 -1
  43. package/dist/ui/dist/assets/{diagram-RJA3I4VV-mc0GLMHo.js → diagram-RJA3I4VV-D6KXeUPg.js} +1 -1
  44. package/dist/ui/dist/assets/{diagram-TYUNFSNR-1NRbBrZW.js → diagram-TYUNFSNR-BHEIVyNN.js} +1 -1
  45. package/dist/ui/dist/assets/{dist-CHZ9mrl_.js → dist-DLlKIWBp.js} +1 -1
  46. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DdStjTDc.js → ebnfDiagram-V4P3TTPT-nUDvYRBV.js} +1 -1
  47. package/dist/ui/dist/assets/{elk-QONTBCTQ-BzRoFO45.js → elk-QONTBCTQ-D21BcRzc.js} +1 -1
  48. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-DlMkgQxj.js → erDiagram-KYQIGQAA-CTpHuftU.js} +1 -1
  49. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-BRYIOrod.js → eventmodeling-K75KTNOO-B6JaKuDY.js} +1 -1
  50. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-BTeOUR-p.js +1 -0
  51. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-D8fQZ7Ah.js → ganttDiagram-R7GPNTJY-D2-pMcnC.js} +1 -1
  52. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-C7ZydyWX.js → gitGraph-VSP46ZUC-B2yxwhEo.js} +1 -1
  53. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-DCItam0v.js → gitGraphDiagram-PDYL7XHY-DnrhBMH1.js} +1 -1
  54. package/dist/ui/dist/assets/index-D74Skifa.css +1 -0
  55. package/dist/ui/dist/assets/index-Dnte9tPd.js +136 -0
  56. package/dist/ui/dist/assets/{info-OHQRW6UA-C3KneBKT.js → info-OHQRW6UA-D8oh6gvs.js} +1 -1
  57. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-6wK09D32.js → infoDiagram-MVDRKAX5-BLSZjjgB.js} +1 -1
  58. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DTP5C8VX.js → ishikawaDiagram-HPEHV2GE-BXiznLSn.js} +1 -1
  59. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-B--UIMtq.js → journeyDiagram-BOSHW7O7-BSYmVcO_.js} +1 -1
  60. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-Ku4N9QGt.js → kanban-definition-FNSA4HC5-COthkkzK.js} +1 -1
  61. package/dist/ui/dist/assets/{line-C4-H5h1-.js → line-6ND8OaOL.js} +1 -1
  62. package/dist/ui/dist/assets/{linear-DhAID3hm.js → linear-DbWEPFJX.js} +1 -1
  63. package/dist/ui/dist/assets/{mermaid-parser.core-CMPNc-u6.js → mermaid-parser.core-Btsk294z.js} +3 -3
  64. package/dist/ui/dist/assets/{mermaid.core-Nd_xOg2y.js → mermaid.core-B4llDSFO.js} +4 -4
  65. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-BlVsym5h.js → mindmap-definition-6OY57UG3-hv2PfX-x.js} +1 -1
  66. package/dist/ui/dist/assets/{packet-JDAUHWVQ-rsk7SM7S.js → packet-JDAUHWVQ-Dr4vBgjt.js} +1 -1
  67. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-C0L20M8z.js → pegDiagram-ECIUJNBX-DvlFov3N.js} +1 -1
  68. package/dist/ui/dist/assets/{pie-XZMESJXO-B23-iZSN.js → pie-XZMESJXO-C9QsqC4n.js} +1 -1
  69. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-BqFhe4Oh.js → pieDiagram-GNMPY74T-CrrHOj67.js} +1 -1
  70. package/dist/ui/dist/assets/{prism-core-DYsiqJeR.js → prism-core-Cc8H7LPG.js} +1 -1
  71. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-BKP75B-m.js → quadrantDiagram-D57K4MC3-DQ7J6-ti.js} +1 -1
  72. package/dist/ui/dist/assets/{radar-ABXABTNO-BrEk2mQ7.js → radar-ABXABTNO-BBBPaujh.js} +1 -1
  73. package/dist/ui/dist/assets/{railroad-I3PHUGI6-wWO4Eapc.js → railroad-I3PHUGI6-C-P6Igaz.js} +1 -1
  74. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-DfY-IPvE.js → railroad-abnf-LNEFI6M7-CAkorRLs.js} +1 -1
  75. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-19wedWGk.js → railroad-ebnf-DWYD2UWJ-D8Hz6gCf.js} +1 -1
  76. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-9vIslnoJ.js → railroad-peg-JR7BVNR7-D-XNMkOs.js} +1 -1
  77. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-DVnmXs8w.js → railroadDiagram-Y72NRBL7-DE9LwxJQ.js} +1 -1
  78. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-BEoMdlzR.js → requirementDiagram-EQYSW5RA-FrKFY6NJ.js} +1 -1
  79. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-D7Tn2Gbs.js → sankeyDiagram-SY24JHO5-BDxI6RDX.js} +1 -1
  80. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-CyjUDBaQ.js → sequenceDiagram-BLEHNHQU-pV1XcKX7.js} +1 -1
  81. package/dist/ui/dist/assets/{src-BiC9XNcI.js → src-snNJTXXq.js} +1 -1
  82. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMp1VCO9.js → stateDiagram-v2-A6IESKWZ-BSc_5aAf.js} +1 -1
  83. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-KEciLd6r.js → swimlanes-B3ENRKFQ-DC6qUXLh.js} +1 -1
  84. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-F1HykV3o.js +8 -0
  85. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-C8YQtv7M.js → timeline-definition-JXYKGLGB-C8L0Dxtu.js} +1 -1
  86. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-DgAzPaOO.js → treeView-D4JQ5SDB-DDyZIG5T.js} +1 -1
  87. package/dist/ui/dist/assets/{treemap-SAJKECNS-eCRqZMYc.js → treemap-SAJKECNS-CUaLgge0.js} +1 -1
  88. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-Dirryw9G.js → usecaseDiagram-WPOLBNNX-BHRBd5pV.js} +1 -1
  89. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-BjxQbAUx.js → vennDiagram-Q3Y3IDCW-Du4iwwkh.js} +1 -1
  90. package/dist/ui/dist/assets/{wardley-7MLQ67FV-CqHX5RUz.js → wardley-7MLQ67FV-DUgENI0s.js} +1 -1
  91. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-Ds1HoEUY.js → wardleyDiagram-IVAIDKWA-B86zOdke.js} +1 -1
  92. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-kodzDb12.js → xychartDiagram-4URNHDU4-twq9irqO.js} +1 -1
  93. package/dist/ui/dist/index.html +2 -2
  94. package/package.json +1 -1
  95. package/skills/follow-me/SKILL.md +5 -3
  96. package/dist/ui/dist/assets/channel-DPDY0XJP.js +0 -1
  97. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-C1IBh_GZ.js +0 -1
  98. package/dist/ui/dist/assets/index-8BehwQ7E.js +0 -134
  99. package/dist/ui/dist/assets/index-mC2OW2Av.css +0 -1
  100. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DzestpDE.js +0 -8
package/README.md CHANGED
@@ -75,16 +75,24 @@ The red blob in the party hat is your branch tip. Nothing follow-me does moves i
75
75
  ![Cmd-clicking format in pow opens a peek of its definition, which lives outside this branch](assets/screenshots/definition.png)
76
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
77
 
78
+ When your agent's answer names code, like `src/lib.js:4-5`, each place becomes a numbered chip. Hover the answer to tint those lines, click a chip to jump there and Alt+Left to come back. An answer that names several places is a walkthrough: press Enter on it, then `.` and `,` to step.
79
+
80
+ ![An agent reply naming three places as numbered chips, one in a later commit, with those lines tinted and numbered in the code and the walkthrough bar at step 1 of 3](assets/screenshots/refs.png)
81
+ *A walkthrough. Each place the agent names is tinted and numbered on the code, the current step strongest, and the bar at the bottom says where you are.*
82
+
78
83
  ![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
84
  *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.*
80
85
 
86
+ ![A checkout state machine reply: each state marked by whether a test reached it, tested transitions green with a count of their tests, one untested transition red and dashed, the sequences no one test drives, and a table naming the tests of each state and move](assets/screenshots/state-machine.png)
87
+ *Your agent offers what it can draw, like "a state machine for this guard", as chips under the commit. Tap one and it answers with the diagram, each state marked by whether a test reached it. A transition is green with the number of tests that take it, or red and dashed when none does. When the tests run one at a time, the table names them, and a list names the sequences each step of which has a test but no one test drives. Click a state or a transition to jump to its code.*
88
+
81
89
  ![A diagram opened in the lightbox at full size](assets/screenshots/diagram.png)
82
90
  *Diagrams sit full width under the commit or reply they explain, and open full size on click.*
83
91
 
84
92
  ![The hand-off page: every kept item with its original-branch path and line, an include checkbox, and copy buttons](assets/screenshots/handoff.png)
85
93
  *Submit review. Each item carries the original branch's path and line, so your agent can post it to the merge request as is.*
86
94
 
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.
95
+ 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, `.` and `,` step through the agent's walkthrough, `?` lists the rest. An unfinished review autosaves and comes back when the page is opened again.
88
96
 
89
97
  ### Reviewing someone else's branch
90
98
 
package/dist/src/cli.js CHANGED
@@ -3,15 +3,17 @@ import { readFileSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
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";
6
+ import { mapOffTip, parseStateMap } from "./comments.js";
7
+ import { changedLines, LcovError, PREVIEW_LINES, parseLcov, parseLocArg, recordRun, runOf, tipPlacer, totalOf, } from "./coverage.js";
7
8
  import { definitionIndex, isIdentifier } from "./definition.js";
8
- import { DiffParseError, formatUnitRef, isUnitId } from "./diff.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
+ import { DiffParseError, formatUnitRef, isRepoPath, isUnitId, splitLines } from "./diff.js";
10
+ import { BranchRefusedError, blobsAt, branchHead, GitError, isOwnWork, pathsAt, pathsChanged, signing, treeOf, writeBranch, } from "./git.js";
11
+ import { fetchCarried, fetchComments, fetchReport, fetchWaitingOffers, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postCoverage, postReply, probeHealth, readServerRecord, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
11
12
  import { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } from "./install.js";
12
13
  import { naiveDraft } from "./naive.js";
13
- import { isCommentId, isConcernId, isFields, serializePlanFile, validatePlan, } from "./plan.js";
14
+ import { isCommentId, isConcernId, isFields, OFFERS_PER_COMMIT, OFFERS_PER_PLAN, serializePlanFile, validatePlan, } from "./plan.js";
14
15
  import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownDefinitions, renderShownUnit, reportItems, } from "./prompt.js";
16
+ import { parseRefText, refsIn, repoPathOfTyped, resolveRefs } from "./refs.js";
15
17
  import { RegistrationError, sessionIdOf } from "./server.js";
16
18
  import { BranchMovedError, cleanSessions, coverageAt, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
17
19
  import { checkSyntax } from "./syntax.js";
@@ -31,19 +33,21 @@ prints what they flagged or asked. It is interactive: do what its "next:" line s
31
33
  again until it prints the report.
32
34
 
33
35
  Other commands:
34
- reply <commentId> --body <text> [--diagram <file>]
36
+ reply <commentId> --body <text> [--diagram <file> [--map <file>]]
35
37
  answer under a comment on the page; the diagram is
36
- Mermaid source
38
+ Mermaid source, the map JSON tying its states to
39
+ tip lines
37
40
  explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
38
41
  start a thread on the page without being asked
39
- show <h_…|c_…|k_…|path:line[@n|@base]|name>
42
+ show <h_…|c_…|k_…|path:a[-b][@n|@base]|name>
40
43
  print a unit, a comment's thread, a concern, the
41
- lines around path:line in commit n, or where a name
44
+ lines around path:a-b in commit n, or where a name
42
45
  is defined, instead of reading files
43
46
  report [--json] [--markdown] print the report of the latest review; --json lists
44
47
  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
48
+ coverage <lcov> [--test <id> [--at <path:line>]] [--reset]
49
+ record a test run taken at the tip for the page;
50
+ --reset alone clears it before per-test runs
47
51
 
48
52
  help --all lists every command.`;
49
53
  const USAGE = `usage: follow-me <command> [options]
@@ -86,18 +90,20 @@ Interactive loop (while the review page is open):
86
90
  reports the end of the review. Your own replies are
87
91
  left out. --follow streams every event until the
88
92
  review ends instead of stopping at the first
89
- reply <commentId> --body <text> [--diagram <file>]
93
+ reply <commentId> --body <text> [--diagram <file> [--map <file>]]
90
94
  answer under a comment on the page; the diagram is
91
- Mermaid source
95
+ Mermaid source, the map JSON tying its states to
96
+ tip lines. \`path:a-b[@n]\` in backticks in the body
97
+ points at code, and is refused if it does not exist
92
98
  explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
93
99
  start a thread on the page without being asked;
94
100
  --lines are change lines of the unit, commits count
95
101
  from 1; with no target it is about the whole plan.
96
102
  watch leaves these out like your replies
97
- show <h_…|c_…|k_…|path:line[@n|@base]|name> [--branch <name>]
103
+ show <h_…|c_…|k_…|path:a[-b][@n|@base]|name> [--branch <name>]
98
104
  print a unit with the commits that take it, a
99
105
  comment with its rows and thread, a concern with its
100
- lines, the lines around path:line at the tip, in
106
+ lines, the lines around path:a-b at the tip, in
101
107
  commit n of the plan, or in the base, or each place
102
108
  a name is defined at the tip with the commit it
103
109
  lands in
@@ -107,11 +113,12 @@ Interactive loop (while the review page is open):
107
113
  line in both voices, then questions with answers;
108
114
  --json lists the items to post to a merge request
109
115
  (path, lines, severity, body, thread, excluded)
110
- coverage <lcov> [--test <id> [--at <path:line>]] [--branch <name>]
116
+ coverage <lcov> [--test <id> [--at <path:line>]] [--reset] [--branch <name>]
111
117
  record the lcov of a test run at the branch tip;
112
118
  refuses when a covered file in the working tree
113
119
  differs from the tip. A plain run replaces the last;
114
- --test runs add one test each, --at is where it is
120
+ --test runs add one test each, --at is where it is.
121
+ --reset starts fresh; alone it clears the last run
115
122
  apply, while the page is open, swaps it to the new plan version in place, same URL.
116
123
 
117
124
  Old names, still working:
@@ -291,6 +298,12 @@ function formatIssue(issue) {
291
298
  return `$.commits[${issue.commit}]: concern: ${issue.reason}`;
292
299
  case "too-many-concerns":
293
300
  return `${issue.unit} has more than one concern, in commits ${issue.commits.join(" and ")}; keep the one that matters`;
301
+ case "bad-offer":
302
+ return `$.commits[${issue.commit}]: offer: ${issue.reason}`;
303
+ case "too-many-offers":
304
+ return issue.commit === null
305
+ ? `the plan has ${issue.count} offers; at most ${OFFERS_PER_PLAN}, and most plans need none`
306
+ : `$.commits[${issue.commit}] has ${issue.count} offers; at most ${OFFERS_PER_COMMIT} per commit`;
294
307
  case "final-mismatch": {
295
308
  const tip = issue.expected ? `${issue.expected.mode} ${issue.expected.blob}` : "absent";
296
309
  return `${show(issue.path)}: the last commit has ${issue.got}, the tip has ${tip}`;
@@ -305,6 +318,9 @@ function formatWarning(w) {
305
318
  if (w.kind === "long-title") {
306
319
  return `warning: $.commits[${w.commit}]: title is ${w.length} characters; keep it to 50`;
307
320
  }
321
+ if (w.kind === "section-order") {
322
+ return `warning: $.commits[${w.commit}]: section ${w.section} comes after ${w.after}; sections read story, steps, details, wiring`;
323
+ }
308
324
  if (w.kind === "question-addressed") {
309
325
  return `warning: question ${w.comment} is marked addressed; explain it with "answered" and "at" unless answering it changed the plan`;
310
326
  }
@@ -1053,6 +1069,13 @@ function waitOf(args) {
1053
1069
  /** One call fits inside a harness's tool timeout, so the agent's loop is one command per turn. */
1054
1070
  const ENTRY_WAIT_SECONDS = 90;
1055
1071
  const MAX_ENTRY_WAIT_SECONDS = 300;
1072
+ /** The step that names the oldest accepted offer the agent has not answered, or null. */
1073
+ async function offerStep(port, summary) {
1074
+ const [first] = await fetchWaitingOffers(port, summary);
1075
+ if (first === undefined)
1076
+ return null;
1077
+ return `${STILL_WORKING}; offer ${first.offer} waits for your diagram: follow-me reply ${first.comment} --diagram <file> [--map <file>], then run follow-me again`;
1078
+ }
1056
1079
  /**
1057
1080
  * Listens to the hosted review for up to --wait seconds and prints what the reviewer did. It
1058
1081
  * returns at once with news, and with the report when the reviewer submitted or ended the review.
@@ -1112,11 +1135,13 @@ async function listen(session, open, args) {
1112
1135
  next("done; the user can port it with follow-me report --json");
1113
1136
  return 0;
1114
1137
  }
1115
- next(`${STILL_WORKING}; answer with follow-me reply <id> --body "<markdown>" [--diagram file], then run follow-me again`);
1138
+ next((await offerStep(port, summary)) ??
1139
+ `${STILL_WORKING}; answer with follow-me reply <id> --body "<markdown>" [--diagram file], then run follow-me again`);
1116
1140
  return 0;
1117
1141
  }
1118
1142
  if (left <= 0) {
1119
- next(`${STILL_WORKING}; run follow-me again to keep listening`);
1143
+ next((await offerStep(port, summary)) ??
1144
+ `${STILL_WORKING}; run follow-me again to keep listening`);
1120
1145
  return 0;
1121
1146
  }
1122
1147
  }
@@ -1315,7 +1340,7 @@ async function report(cwd, args, walkOnly = false) {
1315
1340
  process.stdout.write(text);
1316
1341
  return 0;
1317
1342
  }
1318
- const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]]";
1343
+ const COVERAGE_USAGE = "usage: follow-me coverage <lcov> [--test <id> [--at <path:line>]] [--reset]\n follow-me coverage --reset";
1319
1344
  /**
1320
1345
  * Records the lcov of a test run the agent made at the tip in the session's coverage.json and
1321
1346
  * prints how much of the branch's changed code it ran. follow-me never runs the tests itself.
@@ -1324,6 +1349,13 @@ async function coverage(cwd, args) {
1324
1349
  const [file, extra] = args.positionals;
1325
1350
  const id = args.values.get("test");
1326
1351
  const atArg = args.values.get("at");
1352
+ const reset = args.flags.has("reset");
1353
+ if (file === undefined && reset && id === undefined && atArg === undefined) {
1354
+ const session = await openFresh(cwd, args);
1355
+ session.clearCoverage();
1356
+ say("coverage cleared; the next run starts fresh");
1357
+ return 0;
1358
+ }
1327
1359
  if (file === undefined || extra !== undefined)
1328
1360
  throw new UsageError(COVERAGE_USAGE);
1329
1361
  if (atArg !== undefined && id === undefined) {
@@ -1354,10 +1386,15 @@ async function coverage(cwd, args) {
1354
1386
  const at = atArg === undefined ? undefined : parseLocArg(atArg, place);
1355
1387
  if (typeof at === "string")
1356
1388
  throw new UsageError(at);
1357
- test = { id, ...(at === undefined ? {} : { at }) };
1389
+ const preview = at === undefined ? undefined : await testPreview(session, at);
1390
+ test = {
1391
+ id,
1392
+ ...(at === undefined ? {} : { at }),
1393
+ ...(preview === undefined ? {} : { preview }),
1394
+ };
1358
1395
  }
1359
1396
  const takenAt = new Date().toISOString();
1360
- const recorded = recordRun(session.coverage(), run, {
1397
+ const recorded = recordRun(reset ? null : session.coverage(), run, {
1361
1398
  tip,
1362
1399
  takenAt,
1363
1400
  ...(test === undefined ? {} : { test }),
@@ -1370,6 +1407,19 @@ async function coverage(cwd, args) {
1370
1407
  say(`coverage for ${plural(recorded.files.size, "file")} at ${short(tip)}; ${covered} of ${executable} changed lines covered`);
1371
1408
  return 0;
1372
1409
  }
1410
+ /**
1411
+ * Up to PREVIEW_LINES tip lines from where a test starts, when the branch leaves its file alone:
1412
+ * the page shows the branch's own files from the plan, and the others need this to peek.
1413
+ */
1414
+ async function testPreview(session, at) {
1415
+ const changed = new Set([...session.diff.units.values()].map((u) => u.path));
1416
+ if (changed.has(at.path))
1417
+ return undefined;
1418
+ const bytes = (await blobsAt(session.root, session.anchor.tip, [at.path])).get(at.path) ?? "";
1419
+ return splitLines(bytes)
1420
+ .slice(at.start - 1, at.start - 1 + PREVIEW_LINES)
1421
+ .map((line) => Buffer.from(line.replace(/\r?\n$/, ""), "latin1").toString("utf8"));
1422
+ }
1373
1423
  /** The old name of report. */
1374
1424
  async function feedback(cwd, args) {
1375
1425
  console.error("follow-me: feedback is now report; printing the same");
@@ -1380,11 +1430,13 @@ function isCommentEvent(event) {
1380
1430
  }
1381
1431
  /**
1382
1432
  * Events the reviewer caused; the agent's own replies, explanations, version swaps and coverage
1383
- * are not news.
1433
+ * are not news, and neither is a dismissed offer, which asks nothing of the agent.
1384
1434
  */
1385
1435
  function isNews(event) {
1386
1436
  if (event.type === "version" || event.type === "coverage")
1387
1437
  return false;
1438
+ if (event.type === "offer-dismissed")
1439
+ return false;
1388
1440
  if (event.type === "comment" || event.type === "comment-updated") {
1389
1441
  return event.comment.author !== "agent";
1390
1442
  }
@@ -1464,24 +1516,107 @@ async function watch(cwd, args) {
1464
1516
  /** Posts the agent's answer under a comment on the hosted page. */
1465
1517
  async function reply(cwd, args) {
1466
1518
  const [commentId, extra] = args.positionals;
1467
- const body = args.values.get("body");
1519
+ const diagram = diagramOf(args);
1520
+ // A diagram can be the whole answer, as when it answers an accepted offer.
1521
+ const body = args.values.get("body") ?? (diagram === undefined ? undefined : "");
1468
1522
  if (commentId === undefined || extra !== undefined || body === undefined) {
1469
- throw new UsageError("usage: follow-me reply <commentId> --body <text> [--diagram <file>]");
1523
+ throw new UsageError(REPLY_USAGE);
1524
+ }
1525
+ const mapFile = args.values.get("map");
1526
+ if (mapFile !== undefined && diagram === undefined) {
1527
+ throw new UsageError(`--map needs --diagram, the state diagram it ties to code\n${REPLY_USAGE}`);
1470
1528
  }
1471
- const diagram = diagramOf(args);
1472
1529
  const session = await openSession(cwd, sessionOpts(args));
1530
+ const map = mapFile === undefined ? undefined : await readMap(session, mapFile, diagram);
1473
1531
  const open = await openOnServer(session, null);
1474
1532
  if (open === null)
1475
1533
  return notOpen(session);
1534
+ const refs = await bodyRefs(session, open.summary.version, body);
1476
1535
  const id = await postReply(open.port, open.summary, commentId, {
1477
1536
  author: "agent",
1478
1537
  body,
1479
1538
  ...(diagram === undefined ? {} : { diagram }),
1539
+ ...(map === undefined ? {} : { map }),
1540
+ ...(refs.length === 0 ? {} : { refs }),
1480
1541
  });
1481
1542
  say(`replied ${id} to ${commentId}`);
1482
1543
  next(`run follow-me again; ${STILL_WORKING}`);
1483
1544
  return 0;
1484
1545
  }
1546
+ const REPLY_USAGE = `usage: follow-me reply <commentId> --body <text> [--diagram <file> [--map <file>]]
1547
+ follow-me reply <commentId> --diagram <file> [--map <file>] [--body <text>]`;
1548
+ /** A map path as git names it: the JSON's UTF-8 text as the bytes git prints, read as latin1. */
1549
+ function repoPathOf(typed) {
1550
+ const path = Buffer.from(typed, "utf8").toString("latin1");
1551
+ if (!isRepoPath(path))
1552
+ throw new UsageError(`${typed} is not a repo path`);
1553
+ return path;
1554
+ }
1555
+ /**
1556
+ * --map's JSON, checked against the tip: every place is a file there and within its lines. Every
1557
+ * bad entry is named at once, so one fix round is enough.
1558
+ */
1559
+ async function readMap(session, file, diagram) {
1560
+ let raw;
1561
+ try {
1562
+ raw = JSON.parse(readFileSync(file, "utf8"));
1563
+ }
1564
+ catch (error) {
1565
+ throw new UsageError(`cannot read ${file} as JSON: ${error instanceof Error ? error.message : error}`);
1566
+ }
1567
+ const typed = parseStateMap(raw, diagram);
1568
+ if (typeof typed === "string")
1569
+ throw new UsageError(`${file}: ${typed}`);
1570
+ const at = (loc) => ({ ...loc, path: repoPathOf(loc.path) });
1571
+ const map = {
1572
+ states: Object.fromEntries(Object.entries(typed.states).map(([name, locs]) => [name, locs.map(at)])),
1573
+ ...(typed.transitions === undefined
1574
+ ? {}
1575
+ : { transitions: typed.transitions.map((t) => ({ ...t, at: t.at.map(at) })) }),
1576
+ };
1577
+ const { root, anchor } = session;
1578
+ const named = [
1579
+ ...Object.values(map.states).flat(),
1580
+ ...(map.transitions ?? []).flatMap((t) => t.at),
1581
+ ].map((loc) => loc.path);
1582
+ const tip = await pathsAt(root, anchor.tip);
1583
+ const present = [...new Set(named)].filter((p) => tip.has(p));
1584
+ const lines = new Map([...(await blobsAt(root, anchor.tip, present))].map(([p, bytes]) => [
1585
+ p,
1586
+ splitLines(bytes).length,
1587
+ ]));
1588
+ const problems = mapOffTip(map, lines, (p) => Buffer.from(p, "latin1").toString("utf8"));
1589
+ if (problems.length > 0)
1590
+ throw new UsageError(`${file}:\n ${problems.join("\n ")}`);
1591
+ return map;
1592
+ }
1593
+ /**
1594
+ * The refs --body writes, resolved against plan `version` and the tip. Every one that does not
1595
+ * resolve is named at once, as --map's are.
1596
+ */
1597
+ async function bodyRefs(session, version, body) {
1598
+ const specs = refsIn(body);
1599
+ if (specs.length === 0)
1600
+ return [];
1601
+ const { root, diff } = session;
1602
+ const otherChanged = new Set([...diff.units.values()].flatMap((u) => (diff.textFiles.has(u.path) ? [] : [u.path])));
1603
+ const asked = [...new Set(specs.map((s) => repoPathOfTyped(s.path)))].filter((p) => isRepoPath(p) && !diff.textFiles.has(p) && !otherChanged.has(p));
1604
+ const tip = asked.length === 0 ? new Set() : await pathsAt(root, diff.tip);
1605
+ const present = asked.filter((p) => tip.has(p));
1606
+ const unchanged = new Map([...(await blobsAt(root, diff.tip, present))].map(([p, bytes]) => [p, splitLines(bytes)]));
1607
+ const { refs, problems } = resolveRefs(specs, {
1608
+ tip: diff.tip,
1609
+ textFiles: diff.textFiles,
1610
+ otherChanged,
1611
+ unchanged,
1612
+ commits: projectedCommits(session, version),
1613
+ version,
1614
+ });
1615
+ if (problems.length > 0) {
1616
+ throw new UsageError(`--body points at code that does not resolve:\n ${problems.join("\n ")}`);
1617
+ }
1618
+ return refs;
1619
+ }
1485
1620
  /** The Mermaid source in --diagram's file, or undefined without the flag. */
1486
1621
  function diagramOf(args) {
1487
1622
  const file = args.values.get("diagram");
@@ -1589,17 +1724,19 @@ async function explain(cwd, args) {
1589
1724
  if (open === null)
1590
1725
  return notOpen(session);
1591
1726
  const target = explainTarget(session, open.summary.version, args);
1727
+ const refs = await bodyRefs(session, open.summary.version, body);
1592
1728
  const id = await postComment(open.port, open.summary, {
1593
1729
  ...target,
1594
1730
  body,
1595
1731
  author: "agent",
1596
1732
  ...(diagram === undefined ? {} : { diagram }),
1733
+ ...(refs.length === 0 ? {} : { refs }),
1597
1734
  });
1598
1735
  say(`explained ${id}`);
1599
1736
  next(`run follow-me again; ${STILL_WORKING}`);
1600
1737
  return 0;
1601
1738
  }
1602
- const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:line[@<commit>|@base]|name>";
1739
+ const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:a[-b][@<commit>|@base]|name>";
1603
1740
  /** Every comment the session has, with the plan version it was made on: live ones first. */
1604
1741
  async function knownComments(session) {
1605
1742
  const open = await openOnServer(session, null);
@@ -1613,14 +1750,16 @@ async function knownComments(session) {
1613
1750
  }
1614
1751
  const SHOW_MARGIN = 3;
1615
1752
  /**
1616
- * The lines around path:line as commit `at` of the latest plan writes the file (1-based), the
1617
- * base, or the tip when `at` is undefined. Read from the plan, so it works before any apply.
1753
+ * The lines around path:a-b as commit `at` of the latest plan writes the file (1-based), the
1754
+ * base, or the tip when `at` is null. Read from the plan, so it works before any apply.
1618
1755
  */
1619
1756
  function showLines(session, raw) {
1620
- const m = /^(.+):(\d+)(?:@(\d+|base))?$/.exec(raw);
1621
- const [, typed, lineRaw, at] = m ?? [];
1622
- if (typed === undefined || lineRaw === undefined)
1757
+ const spec = parseRefText(raw);
1758
+ if (spec === null)
1623
1759
  throw new UsageError(SHOW_USAGE);
1760
+ const { path: typed, start, end, at } = spec;
1761
+ if (end < start)
1762
+ throw new UsageError(`${raw} ends before it starts`);
1624
1763
  const path = Buffer.from(typed, "utf8").toString("latin1");
1625
1764
  const file = [...session.diff.textFiles.values()].find((f) => f.path === path);
1626
1765
  if (file === undefined) {
@@ -1628,7 +1767,7 @@ function showLines(session, raw) {
1628
1767
  }
1629
1768
  let text;
1630
1769
  let label;
1631
- if (at === undefined) {
1770
+ if (at === null) {
1632
1771
  text = file.tip?.bytes ?? null;
1633
1772
  label = "the tip";
1634
1773
  }
@@ -1639,7 +1778,7 @@ function showLines(session, raw) {
1639
1778
  else {
1640
1779
  const latest = session.plans().at(-1);
1641
1780
  const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
1642
- const n = Number(at);
1781
+ const n = at;
1643
1782
  if (n < 1 || n > commits.length) {
1644
1783
  throw new UsageError(`@${at} is not one of the ${commits.length} commits of the plan`);
1645
1784
  }
@@ -1658,18 +1797,18 @@ function showLines(session, raw) {
1658
1797
  const lines = Buffer.from(text, "latin1").toString("utf8").split("\n");
1659
1798
  if (lines.at(-1) === "")
1660
1799
  lines.pop();
1661
- const line = Number(lineRaw);
1662
- if (line < 1 || line > lines.length) {
1800
+ if (start < 1 || end > lines.length) {
1663
1801
  return `${typed} has ${plural(lines.length, "line")} in ${label}\n`;
1664
1802
  }
1665
- const from = Math.max(1, line - SHOW_MARGIN);
1666
- const to = Math.min(lines.length, line + SHOW_MARGIN);
1803
+ const from = Math.max(1, start - SHOW_MARGIN);
1804
+ const to = Math.min(lines.length, end + SHOW_MARGIN);
1667
1805
  const width = String(to).length;
1668
1806
  const rows = lines.slice(from - 1, to).map((l, i) => {
1669
1807
  const n = from + i;
1670
- return `${n === line ? ">" : " "} ${String(n).padStart(width)} ${l}`;
1808
+ return `${n >= start && n <= end ? ">" : " "} ${String(n).padStart(width)} ${l}`;
1671
1809
  });
1672
- return `${typed}:${line} in ${label}\n${rows.join("\n")}\n`;
1810
+ const span = end > start ? `${start}-${end}` : `${start}`;
1811
+ return `${typed}:${span} in ${label}\n${rows.join("\n")}\n`;
1673
1812
  }
1674
1813
  /** Expands a ref the agent was given to the lines and thread it names, so no file read is needed. */
1675
1814
  async function showRef(cwd, args) {
@@ -1950,7 +2089,7 @@ export async function main(argv, cwd) {
1950
2089
  return await watch(cwd, parseArgs(rest, spec));
1951
2090
  }
1952
2091
  case "reply":
1953
- return await reply(cwd, parseArgs(rest, { values: ["body", "diagram", "branch"] }));
2092
+ return await reply(cwd, parseArgs(rest, { values: ["body", "diagram", "map", "branch"] }));
1954
2093
  case "explain": {
1955
2094
  const spec = { values: ["commit", "unit", "lines", "body", "diagram", "branch"] };
1956
2095
  return await explain(cwd, parseArgs(rest, spec));
@@ -1958,7 +2097,7 @@ export async function main(argv, cwd) {
1958
2097
  case "show":
1959
2098
  return await showRef(cwd, parseArgs(rest, { values: ["branch"] }));
1960
2099
  case "coverage":
1961
- return await coverage(cwd, parseArgs(rest, { values: ["test", "at", "branch"] }));
2100
+ return await coverage(cwd, parseArgs(rest, { values: ["test", "at", "branch"], flags: ["reset"] }));
1962
2101
  case "report":
1963
2102
  return await report(cwd, parseArgs(rest, { values: ["branch"], flags: ["json", "markdown"] }));
1964
2103
  case "clean": {