@shiyuphan/follow-me 0.4.0 → 0.5.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 (96) hide show
  1. package/dist/src/cli.js +154 -11
  2. package/dist/src/comments.js +29 -11
  3. package/dist/src/evolve.js +6 -2
  4. package/dist/src/harness.js +4 -1
  5. package/dist/src/host.js +17 -1
  6. package/dist/src/install.js +1 -1
  7. package/dist/src/prompt.js +170 -166
  8. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-byISxePP.js → abnfDiagram-WEYOIA3C-BXkLs7mV.js} +1 -1
  9. package/dist/ui/dist/assets/{arc-CBzsAsP2.js → arc-DNzjw7Ie.js} +1 -1
  10. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-0OSkIwdB.js → architecture-WOLXFQ4H-D66kgJJt.js} +1 -1
  11. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-ixHybNqO.js → architectureDiagram-ZHVYKT53-D8UWuhQr.js} +1 -1
  12. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-1F8XL_Ou.js → blockDiagram-RJAE7QA3-BLkmr62i.js} +1 -1
  13. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-D9PFBXnc.js → c4Diagram-XDPPPE54-3rZPYwOc.js} +1 -1
  14. package/dist/ui/dist/assets/channel-B9JVdOBZ.js +1 -0
  15. package/dist/ui/dist/assets/{chunk-2E7WHVMD-zZdNfMkj.js → chunk-2E7WHVMD-MMnvAgpB.js} +1 -1
  16. package/dist/ui/dist/assets/{chunk-4EMNKR2T-CTKoxSSj.js → chunk-4EMNKR2T-D9FGWA3l.js} +1 -1
  17. package/dist/ui/dist/assets/{chunk-55XCU76K-ifFnDlcS.js → chunk-55XCU76K-DuOUaV8F.js} +1 -1
  18. package/dist/ui/dist/assets/{chunk-7S6XSTFO-BCMVdi_1.js → chunk-7S6XSTFO-p3YWPv0m.js} +1 -1
  19. package/dist/ui/dist/assets/{chunk-AQDTSKZB-DC-5Whju.js → chunk-AQDTSKZB-DWfKYjwk.js} +1 -1
  20. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-DASbe_gb.js → chunk-DSNLSHJZ-C3CIwpv0.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-HAGFOTED-CArytDjj.js → chunk-HAGFOTED-Bwfl9gme.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-HNE4Q36P-Dnl-Wtud.js → chunk-HNE4Q36P-0_oeU0le.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-NQOKRVIF-wvSLaeaK.js → chunk-NQOKRVIF-z_LPPPx5.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-UHW7PFBP-CckaOGiQ.js → chunk-UHW7PFBP-BH28V2yu.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-VQ5W62QI-kQBMDJ7V.js → chunk-VQ5W62QI-BXDajDf7.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-WQQL4CDK-CMg6bbLU.js → chunk-WQQL4CDK-BphBlfKj.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-XXDRQBXY-CYlFr19K.js → chunk-XXDRQBXY-BiCT5NwV.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-YZO67HFG-CqkG1vVk.js → chunk-YZO67HFG-Bg5Pu8BX.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-BUNEdx-e.js → chunk-ZAUX2LTX-C_Tgd8tB.js} +1 -1
  30. package/dist/ui/dist/assets/{chunk-ZCHMQSOR-CFTbvcrm.js → chunk-ZCHMQSOR-MBRcMi8-.js} +1 -1
  31. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-t2OdJ0LA.js → classDiagram-v2-4MYWR3UA-1wyeCe5y.js} +1 -1
  32. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-FGm030gc.js → cose-bilkent-JH36ORCC-BtOYclF6.js} +1 -1
  33. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-BtU33Lsu.js → cynefin-EF2NZ3EQ-dJmoOG8g.js} +1 -1
  34. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-DUr-I7hS.js → cynefinDiagram-WF24UDRP-AloyqbEN.js} +1 -1
  35. package/dist/ui/dist/assets/{dagre-NK4GIZVX-BWTHwJKt.js → dagre-NK4GIZVX-NCZhdnfb.js} +1 -1
  36. package/dist/ui/dist/assets/{diagram-CBK3MWTC-PcHrsEBn.js → diagram-CBK3MWTC-D8w7x5Gb.js} +1 -1
  37. package/dist/ui/dist/assets/{diagram-I5XIIV2D-CkGtKjYj.js → diagram-I5XIIV2D-BUFnCGlp.js} +1 -1
  38. package/dist/ui/dist/assets/{diagram-ODIL2UCW-B_HdBB1w.js → diagram-ODIL2UCW-BULmHKaW.js} +1 -1
  39. package/dist/ui/dist/assets/{diagram-PFBEJUXE-DL9zayE6.js → diagram-PFBEJUXE-B5MqFo0r.js} +1 -1
  40. package/dist/ui/dist/assets/{diagram-RJA3I4VV-By5wl8nK.js → diagram-RJA3I4VV-BIkDLL8E.js} +1 -1
  41. package/dist/ui/dist/assets/{diagram-TYUNFSNR-ziWPVayL.js → diagram-TYUNFSNR-Ca4gsQV8.js} +1 -1
  42. package/dist/ui/dist/assets/{dist-NDVqCM7k.js → dist-G85JKY5k.js} +1 -1
  43. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DH4YGkPc.js → ebnfDiagram-V4P3TTPT-rVjcFFtb.js} +1 -1
  44. package/dist/ui/dist/assets/{elk-QONTBCTQ-BL2RlCEv.js → elk-QONTBCTQ-BWPdvULh.js} +1 -1
  45. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BKbTMaRp.js → erDiagram-KYQIGQAA-BPxJJkRl.js} +1 -1
  46. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-Dk5RCWc-.js → eventmodeling-K75KTNOO-BSB9WYV3.js} +1 -1
  47. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-N-Spyk2o.js +1 -0
  48. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-3vM9gxeT.js → ganttDiagram-R7GPNTJY-DKps530K.js} +1 -1
  49. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-CXGFhz54.js → gitGraph-VSP46ZUC-D1d4UAFV.js} +1 -1
  50. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js → gitGraphDiagram-PDYL7XHY-N55yl6hc.js} +1 -1
  51. package/dist/ui/dist/assets/index-BKtWBS7Z.js +134 -0
  52. package/dist/ui/dist/assets/{info-OHQRW6UA-DfSsZ8ib.js → info-OHQRW6UA-CgxE3gf9.js} +1 -1
  53. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-B_7tyrL5.js → infoDiagram-MVDRKAX5-DOnUrnMf.js} +1 -1
  54. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DUvHljpg.js → ishikawaDiagram-HPEHV2GE-BIY0fFHB.js} +1 -1
  55. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-BCgqzrb8.js → journeyDiagram-BOSHW7O7-CqA2Ern0.js} +1 -1
  56. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DOzZ3NmE.js → kanban-definition-FNSA4HC5-vGrpQKIF.js} +1 -1
  57. package/dist/ui/dist/assets/{line-B-mPFVll.js → line-Brzv3qA6.js} +1 -1
  58. package/dist/ui/dist/assets/{linear-k7o0jout.js → linear-D9uIlqyh.js} +1 -1
  59. package/dist/ui/dist/assets/{mermaid-parser.core-CAyLSQpH.js → mermaid-parser.core-Bz9b-Thu.js} +3 -3
  60. package/dist/ui/dist/assets/{mermaid.core-vfVMjgx2.js → mermaid.core-BaZipu3b.js} +4 -4
  61. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-B6y-B_r4.js → mindmap-definition-6OY57UG3-D6aie7uH.js} +1 -1
  62. package/dist/ui/dist/assets/{packet-JDAUHWVQ-C_r4ed30.js → packet-JDAUHWVQ-28r3jVFY.js} +1 -1
  63. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-B8VUm3Bh.js → pegDiagram-ECIUJNBX-CbGvD4qP.js} +1 -1
  64. package/dist/ui/dist/assets/{pie-XZMESJXO-DcwtbH9y.js → pie-XZMESJXO-DHpcGnZd.js} +1 -1
  65. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-CvUNBi2F.js → pieDiagram-GNMPY74T-DuhauwnY.js} +1 -1
  66. package/dist/ui/dist/assets/{prism-core-_2ju85Ya.js → prism-core-CbZMOvrb.js} +1 -1
  67. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-Dg2eVS3j.js → quadrantDiagram-D57K4MC3-BfyNmVca.js} +1 -1
  68. package/dist/ui/dist/assets/{radar-ABXABTNO-xCNTcZE9.js → radar-ABXABTNO-Xp1TYwaj.js} +1 -1
  69. package/dist/ui/dist/assets/{railroad-I3PHUGI6-BI-sIL2Q.js → railroad-I3PHUGI6-c5FY4wwa.js} +1 -1
  70. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-Ddvk40Wx.js → railroad-abnf-LNEFI6M7-RDyWXqcL.js} +1 -1
  71. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-h35gE183.js → railroad-ebnf-DWYD2UWJ-CLD4jVou.js} +1 -1
  72. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Zim9iPdp.js → railroad-peg-JR7BVNR7-17EYTA-w.js} +1 -1
  73. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-Cz6fdmOQ.js → railroadDiagram-Y72NRBL7-C4p2wtKe.js} +1 -1
  74. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-DTJkCrcD.js → requirementDiagram-EQYSW5RA-DTYIFN8T.js} +1 -1
  75. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-Bg1iiVla.js → sankeyDiagram-SY24JHO5-YDYhRyJU.js} +1 -1
  76. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-9XfetsRA.js → sequenceDiagram-BLEHNHQU-Crvy6IlU.js} +1 -1
  77. package/dist/ui/dist/assets/{src-Dym5afQ5.js → src-BPod-Ype.js} +1 -1
  78. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMmcMV8z.js → stateDiagram-v2-A6IESKWZ-BWr70YOQ.js} +1 -1
  79. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-B2PcshTY.js → swimlanes-B3ENRKFQ-DvbG4I9a.js} +1 -1
  80. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-D2wU-jW6.js +8 -0
  81. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-Cr1hCNtW.js → timeline-definition-JXYKGLGB-CR950Bzb.js} +1 -1
  82. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-jH5ksKnh.js → treeView-D4JQ5SDB-DSDDXGrk.js} +1 -1
  83. package/dist/ui/dist/assets/{treemap-SAJKECNS-BV90e8QL.js → treemap-SAJKECNS-Ct5w2_RR.js} +1 -1
  84. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-BNwq5aJV.js → usecaseDiagram-WPOLBNNX-_eP0371y.js} +1 -1
  85. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-D-lLyZCL.js → vennDiagram-Q3Y3IDCW-Cu-IA57L.js} +1 -1
  86. package/dist/ui/dist/assets/{wardley-7MLQ67FV-CWfHQUGt.js → wardley-7MLQ67FV-r8yd6frN.js} +1 -1
  87. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-BL_qNKVd.js → wardleyDiagram-IVAIDKWA-CBUUiRQm.js} +1 -1
  88. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-BHPK5Snp.js → xychartDiagram-4URNHDU4-DEVQjvgG.js} +1 -1
  89. package/dist/ui/dist/index.html +1 -1
  90. package/package.json +1 -1
  91. package/skills/follow-me/SKILL.md +69 -7
  92. package/skills/follow-me/porting.md +7 -0
  93. package/dist/ui/dist/assets/channel-B-fwsHf1.js +0 -1
  94. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +0 -1
  95. package/dist/ui/dist/assets/index-CBbx_pJk.js +0 -134
  96. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +0 -8
package/dist/src/cli.js CHANGED
@@ -5,11 +5,11 @@ import { dirname, join, relative } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { DiffParseError, formatUnitRef, isUnitId } from "./diff.js";
7
7
  import { BranchRefusedError, branchHead, GitError, isOwnWork, signing, treeOf, writeBranch, } from "./git.js";
8
- import { fetchCarried, 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 { runInstall, runUninstall, syncSkills } from "./install.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 { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } from "./install.js";
10
10
  import { naiveDraft } from "./naive.js";
11
- import { isFields, serializePlanFile, validatePlan } from "./plan.js";
12
- import { renderEvents, renderPrompt, renderReport, reportItems, } from "./prompt.js";
11
+ import { isCommentId, isConcernId, isFields, serializePlanFile, validatePlan, } from "./plan.js";
12
+ import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownUnit, reportItems, } from "./prompt.js";
13
13
  import { RegistrationError, sessionIdOf } from "./server.js";
14
14
  import { BranchMovedError, cleanSessions, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sinceLastPlan, } from "./session.js";
15
15
  import { checkSyntax } from "./syntax.js";
@@ -30,6 +30,9 @@ Other commands:
30
30
  Mermaid source
31
31
  explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
32
32
  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
33
36
  report [--json] [--markdown] print the report of the latest review; --json lists
34
37
  the items to post to a merge request
35
38
 
@@ -82,6 +85,11 @@ Interactive loop (while the review page is open):
82
85
  --lines are change lines of the unit, commits count
83
86
  from 1; with no target it is about the whole plan.
84
87
  watch leaves these out like your replies
88
+ show <h_…|c_…|k_…|path:line[@n|@base]> [--branch <name>]
89
+ print a unit with the commits that take it, a
90
+ 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
85
93
  report [--json] [--markdown] [--branch <name>]
86
94
  print the report of the open review, or of the last
87
95
  one that ended. Markdown lists issues by file and
@@ -425,7 +433,7 @@ async function applyHosted(session, args, open) {
425
433
  say(`follow-me review: ${sessionUrl(open.port, swapped)}`);
426
434
  say(`the page now shows plan v${latest.version}; the comments on v${open.summary.version} are saved`);
427
435
  }
428
- next("follow-me watch");
436
+ next(`run follow-me again; ${STILL_WORKING}`);
429
437
  return 0;
430
438
  }
431
439
  function signMode(args) {
@@ -489,6 +497,7 @@ function promptInput(session, mode, args, changes) {
489
497
  diff: session.diff,
490
498
  draftPath: session.draftPath,
491
499
  checkCommand: `follow-me check${branchFlag(args)}`,
500
+ ...skillFile(),
492
501
  ...(changes === undefined ? {} : { changes }),
493
502
  };
494
503
  if (mode === "walk")
@@ -502,6 +511,17 @@ function promptInput(session, mode, args, changes) {
502
511
  previousCommits: previous === undefined ? [] : projectedCommits(session, previous.version),
503
512
  };
504
513
  }
514
+ /** The skill this package ships, which holds the rules prompt.md points at; none if it is missing. */
515
+ function skillFile() {
516
+ try {
517
+ return { skillPath: join(findSkillDir(), "SKILL.md") };
518
+ }
519
+ catch (error) {
520
+ if (error instanceof InstallError)
521
+ return {};
522
+ throw error;
523
+ }
524
+ }
505
525
  /** The commits of a frozen plan as projected now, or none when it no longer fits the branch. */
506
526
  function projectedCommits(session, version) {
507
527
  const plan = session.plans().find((p) => p.version === version);
@@ -917,8 +937,7 @@ function evolveHandoff(session, cwd, mode, args, moved, hosted) {
917
937
  session.writeDraft(serializePlanFile(moved.draft));
918
938
  if (draftState(session).kind === "valid")
919
939
  return null;
920
- const { since, vanished, added } = moved;
921
- session.writePrompt(renderPrompt(promptInput(session, mode, args, { since, vanished, added })));
940
+ session.writePrompt(renderPrompt(promptInput(session, mode, args, moved)));
922
941
  say(`wrote ${relative(cwd, session.promptPath)}`);
923
942
  const step = `assign the new units in ${relative(cwd, session.draftPath)}, then run follow-me again`;
924
943
  next(hosted ? `${step}; ${STILL_WORKING}` : step);
@@ -1058,9 +1077,12 @@ async function listen(session, open, args) {
1058
1077
  const news = fresh.filter(isNews);
1059
1078
  if (news.length > 0) {
1060
1079
  const ctx = { commits: projectedCommits(session, version), units: session.diff.units };
1061
- process.stdout.write(renderEvents(news, ctx));
1062
1080
  const ended = news.some((e) => e.type === "ended");
1063
- if (ended || news.some((e) => e.type === "submitted")) {
1081
+ const done = ended || news.some((e) => e.type === "submitted");
1082
+ // The report that follows quotes every comment, so the digest only says what else happened.
1083
+ const shown = done ? news.filter((e) => !isCommentEvent(e)) : news;
1084
+ process.stdout.write(renderEvents(shown, ctx));
1085
+ if (done) {
1064
1086
  say();
1065
1087
  if (ended) {
1066
1088
  const stored = reviewsOf(session).find((r) => r.version === version);
@@ -1259,6 +1281,9 @@ async function feedback(cwd, args) {
1259
1281
  console.error("follow-me: feedback is now report; printing the same");
1260
1282
  return report(cwd, args, args.flags.has("walk"));
1261
1283
  }
1284
+ function isCommentEvent(event) {
1285
+ return event.type === "comment" || event.type === "comment-updated";
1286
+ }
1262
1287
  /** Events the reviewer caused; the agent's own replies, explanations and version swaps are not news. */
1263
1288
  function isNews(event) {
1264
1289
  if (event.type === "version")
@@ -1357,7 +1382,7 @@ async function reply(cwd, args) {
1357
1382
  ...(diagram === undefined ? {} : { diagram }),
1358
1383
  });
1359
1384
  say(`replied ${id} to ${commentId}`);
1360
- next("follow-me watch");
1385
+ next(`run follow-me again; ${STILL_WORKING}`);
1361
1386
  return 0;
1362
1387
  }
1363
1388
  /** The Mermaid source in --diagram's file, or undefined without the flag. */
@@ -1474,7 +1499,123 @@ async function explain(cwd, args) {
1474
1499
  ...(diagram === undefined ? {} : { diagram }),
1475
1500
  });
1476
1501
  say(`explained ${id}`);
1477
- next("follow-me watch");
1502
+ next(`run follow-me again; ${STILL_WORKING}`);
1503
+ return 0;
1504
+ }
1505
+ const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:line[@<commit>|@base]>";
1506
+ /** Every comment the session has, with the plan version it was made on: live ones first. */
1507
+ async function knownComments(session) {
1508
+ const open = await openOnServer(session, null);
1509
+ const live = open === null ? [] : await fetchComments(open.port, open.summary);
1510
+ const saved = [
1511
+ session.priorReview("split", null),
1512
+ session.priorReview("walk", null),
1513
+ ...reviewsOf(session).map((r) => r.review),
1514
+ ].flatMap((r) => (r === null ? [] : [{ version: r.planVersion, comments: r.comments }]));
1515
+ return [...live, ...saved];
1516
+ }
1517
+ const SHOW_MARGIN = 3;
1518
+ /**
1519
+ * The lines around path:line as commit `at` of the latest plan writes the file (1-based), the
1520
+ * base, or the tip when `at` is undefined. Read from the plan, so it works before any apply.
1521
+ */
1522
+ function showLines(session, raw) {
1523
+ const m = /^(.+):(\d+)(?:@(\d+|base))?$/.exec(raw);
1524
+ const [, typed, lineRaw, at] = m ?? [];
1525
+ if (typed === undefined || lineRaw === undefined)
1526
+ throw new UsageError(SHOW_USAGE);
1527
+ const path = Buffer.from(typed, "utf8").toString("latin1");
1528
+ const file = [...session.diff.textFiles.values()].find((f) => f.path === path);
1529
+ if (file === undefined) {
1530
+ return `${typed} is not a text file this branch changes; it reads the same in every commit, so read it from the working tree\n`;
1531
+ }
1532
+ let text;
1533
+ let label;
1534
+ if (at === undefined) {
1535
+ text = file.tip?.bytes ?? null;
1536
+ label = "the tip";
1537
+ }
1538
+ else if (at === "base") {
1539
+ text = file.base === null ? null : file.base.lines.join("");
1540
+ label = "the base";
1541
+ }
1542
+ else {
1543
+ const latest = session.plans().at(-1);
1544
+ const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
1545
+ const n = Number(at);
1546
+ if (n < 1 || n > commits.length) {
1547
+ throw new UsageError(`@${at} is not one of the ${commits.length} commits of the plan`);
1548
+ }
1549
+ text = file.base === null ? null : file.base.lines.join("");
1550
+ for (const commit of commits.slice(0, n)) {
1551
+ for (const change of commit.changes) {
1552
+ if (change.path !== path)
1553
+ continue;
1554
+ text = change.op === "write" ? change.bytes : null;
1555
+ }
1556
+ }
1557
+ label = `commit ${n}`;
1558
+ }
1559
+ if (text === null)
1560
+ return `${typed} does not exist in ${label}\n`;
1561
+ const lines = Buffer.from(text, "latin1").toString("utf8").split("\n");
1562
+ if (lines.at(-1) === "")
1563
+ lines.pop();
1564
+ const line = Number(lineRaw);
1565
+ if (line < 1 || line > lines.length) {
1566
+ return `${typed} has ${plural(lines.length, "line")} in ${label}\n`;
1567
+ }
1568
+ const from = Math.max(1, line - SHOW_MARGIN);
1569
+ const to = Math.min(lines.length, line + SHOW_MARGIN);
1570
+ const width = String(to).length;
1571
+ const rows = lines.slice(from - 1, to).map((l, i) => {
1572
+ const n = from + i;
1573
+ return `${n === line ? ">" : " "} ${String(n).padStart(width)} ${l}`;
1574
+ });
1575
+ return `${typed}:${line} in ${label}\n${rows.join("\n")}\n`;
1576
+ }
1577
+ /** Expands a ref the agent was given to the lines and thread it names, so no file read is needed. */
1578
+ async function showRef(cwd, args) {
1579
+ const [ref, extra] = args.positionals;
1580
+ if (ref === undefined || extra !== undefined)
1581
+ throw new UsageError(SHOW_USAGE);
1582
+ const session = await openSession(cwd, sessionOpts(args));
1583
+ const { units } = session.diff;
1584
+ const latest = session.plans().at(-1);
1585
+ if (isUnitId(ref)) {
1586
+ const unit = units.get(ref);
1587
+ if (unit === undefined) {
1588
+ throw new UsageError(`${ref} is not a unit of the branch now; its hunk changed`);
1589
+ }
1590
+ const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
1591
+ process.stdout.write(renderShownUnit(unit, commits));
1592
+ return 0;
1593
+ }
1594
+ if (/^c_[a-z2-7]{6}$/.test(ref) && isCommentId(ref)) {
1595
+ for (const { version, comments } of await knownComments(session)) {
1596
+ const comment = comments.find((c) => c.id === ref);
1597
+ if (comment === undefined)
1598
+ continue;
1599
+ const ctx = { commits: projectedCommits(session, version), units };
1600
+ process.stdout.write(renderShownComment(comment, ctx, version));
1601
+ return 0;
1602
+ }
1603
+ throw new UsageError(`no comment ${ref} in this session`);
1604
+ }
1605
+ if (isConcernId(ref)) {
1606
+ for (const plan of session.plans().toReversed()) {
1607
+ const commits = projectedCommits(session, plan.version);
1608
+ for (const commit of commits) {
1609
+ const concern = commit.concerns?.find((k) => k.id === ref);
1610
+ if (concern === undefined)
1611
+ continue;
1612
+ process.stdout.write(renderShownConcern(concern, commit.index, { commits, units }));
1613
+ return 0;
1614
+ }
1615
+ }
1616
+ throw new UsageError(`no concern ${ref} in a plan that fits the branch`);
1617
+ }
1618
+ process.stdout.write(showLines(session, ref));
1478
1619
  return 0;
1479
1620
  }
1480
1621
  function describeLock(lock) {
@@ -1711,6 +1852,8 @@ export async function main(argv, cwd) {
1711
1852
  const spec = { values: ["commit", "unit", "lines", "body", "diagram", "branch"] };
1712
1853
  return await explain(cwd, parseArgs(rest, spec));
1713
1854
  }
1855
+ case "show":
1856
+ return await showRef(cwd, parseArgs(rest, { values: ["branch"] }));
1714
1857
  case "report":
1715
1858
  return await report(cwd, parseArgs(rest, { values: ["branch"], flags: ["json", "markdown"] }));
1716
1859
  case "clean": {
@@ -187,7 +187,10 @@ export function parseNewDecision(concern, v) {
187
187
  return "the decision is not an object";
188
188
  return withDecision(concern, { ...v, decidedAt: undefined });
189
189
  }
190
- /** Absent reads as none. The first item that does not parse names its index and reason. */
190
+ /**
191
+ * Absent reads as none. The first item that does not parse names its index and reason. Items
192
+ * are objects so a string result can only mean a failure; string values go through parseIds.
193
+ */
191
194
  function parseList(raw, parse, what) {
192
195
  if (raw === undefined)
193
196
  return [];
@@ -202,8 +205,26 @@ function parseList(raw, parse, what) {
202
205
  }
203
206
  return out;
204
207
  }
205
- function parseConcernId(item) {
206
- return isConcernId(item) ? item : "not a concern id";
208
+ /** Absent reads as none. The first item that is not an id of the kind names its index. */
209
+ function parseIds(raw, is, what, kind) {
210
+ if (raw === undefined)
211
+ return [];
212
+ if (!isArray(raw))
213
+ return `${what} is not an array`;
214
+ const out = [];
215
+ for (const [i, item] of raw.entries()) {
216
+ if (!is(item))
217
+ return `${what}[${i}]: not a ${kind} id`;
218
+ out.push(item);
219
+ }
220
+ return out;
221
+ }
222
+ /** Keeps only the items that are ids of the kind. */
223
+ function lenientIds(raw, is) {
224
+ return isArray(raw) ? raw.filter(is) : [];
225
+ }
226
+ function isCommentIdValue(item) {
227
+ return typeof item === "string" && isCommentId(item);
207
228
  }
208
229
  /** Absent reads as nothing excluded. */
209
230
  export function parseExcluded(v) {
@@ -211,17 +232,14 @@ export function parseExcluded(v) {
211
232
  return { comments: [], concerns: [] };
212
233
  if (!isFields(v))
213
234
  return "excluded is not an object";
214
- const comments = parseList(v.comments, parseCommentId, "excluded.comments");
235
+ const comments = parseIds(v.comments, isCommentIdValue, "excluded.comments", "comment");
215
236
  if (typeof comments === "string")
216
237
  return comments;
217
- const concerns = parseList(v.concerns, parseConcernId, "excluded.concerns");
238
+ const concerns = parseIds(v.concerns, isConcernId, "excluded.concerns", "concern");
218
239
  return typeof concerns === "string" ? concerns : { comments, concerns };
219
240
  }
220
241
  function asCommentId(item) {
221
- return typeof item === "string" && isCommentId(item) ? item : null;
222
- }
223
- function parseCommentId(item) {
224
- return asCommentId(item) ?? "not a comment id";
242
+ return isCommentIdValue(item) ? item : null;
225
243
  }
226
244
  function parseEarlierReplies(v) {
227
245
  if (!isFields(v))
@@ -286,7 +304,7 @@ export function parseReview(v) {
286
304
  const concerns = parseList(v.concerns, parseDecision, "concerns");
287
305
  if (typeof concerns === "string")
288
306
  return concerns;
289
- const resolved = parseList(v.resolved, parseCommentId, "resolved");
307
+ const resolved = parseIds(v.resolved, isCommentIdValue, "resolved", "comment");
290
308
  if (typeof resolved === "string")
291
309
  return resolved;
292
310
  const excluded = parseExcluded(v.excluded);
@@ -329,7 +347,7 @@ export function parseDraft(v) {
329
347
  return {
330
348
  comments: lenient(v.comments, parseComment),
331
349
  concerns: lenient(v.concerns, parseDecision),
332
- resolved: lenient(v.resolved, parseCommentId),
350
+ resolved: lenientIds(v.resolved, isCommentIdValue),
333
351
  excluded: typeof excluded === "string" ? { comments: [], concerns: [] } : excluded,
334
352
  earlierReplies: lenient(v.earlierReplies, parseEarlierReplies),
335
353
  };
@@ -90,9 +90,13 @@ function carry(comment, oldUnits, newUnits) {
90
90
  const nowhere = { comment, unit: null, offsets: null, newLine: null };
91
91
  if (anchor.kind !== "hunk")
92
92
  return { ...nowhere, changed: false, path: comment.context.path };
93
+ // A comment on a whole hunk has no line; inventing one moves its card off the hunk's end.
94
+ const whole = anchor.changes === null && anchor.offsets === undefined;
93
95
  const kept = newUnits.get(anchor.unit);
94
96
  if (kept !== undefined) {
95
- const newLine = kept.kind === "hunk" ? (tipLines(kept, newUnits)[firstRow(comment, kept)] ?? null) : null;
97
+ const newLine = kept.kind === "hunk" && !whole
98
+ ? (tipLines(kept, newUnits)[firstRow(comment, kept)] ?? null)
99
+ : null;
96
100
  return {
97
101
  comment,
98
102
  unit: kept.id,
@@ -118,6 +122,6 @@ function carry(comment, oldUnits, newUnits) {
118
122
  return {
119
123
  ...moved,
120
124
  unit: nearest.id,
121
- newLine: landing(nearest, newUnits, oldLine, old.body[row]?.text),
125
+ newLine: whole ? null : landing(nearest, newUnits, oldLine, old.body[row]?.text),
122
126
  };
123
127
  }
@@ -68,7 +68,10 @@ export const HARNESSES = [
68
68
  export function findHarness(id) {
69
69
  return HARNESSES.find((h) => h.id === id);
70
70
  }
71
+ /** The files of the skill folder, SKILL.md first. */
72
+ export const SKILL_FILES = ["SKILL.md", "porting.md"];
71
73
  export function plannedInstall(harness, scope, home, repoRoot) {
72
74
  const root = scope === "user" ? join(home, harness.userDir) : join(repoRoot, harness.projectDir);
73
- return [{ path: join(root, SKILL_NAME, "SKILL.md"), source: "SKILL.md" }];
75
+ // porting.md is read only when the user asks to post a review, so the skill stays short.
76
+ return SKILL_FILES.map((source) => ({ path: join(root, SKILL_NAME, source), source }));
74
77
  }
package/dist/src/host.js CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { closeSync, linkSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { parseCarried, parseEvent, parseEventBatch } from "./comments.js";
6
+ import { parseCarried, parseComment, parseEvent, parseEventBatch, } from "./comments.js";
7
7
  import { carryComments } from "./evolve.js";
8
8
  import { isPlanVersion, serializePlanFile, validatePlan } from "./plan.js";
9
9
  import { HEALTH_APP, isSessionId, loadDraftComments, PortInUseError, RegistrationError, startServer, } from "./server.js";
@@ -295,6 +295,22 @@ export async function fetchCarried(port, summary) {
295
295
  return [carried];
296
296
  });
297
297
  }
298
+ /** The comments of the hosted review, and of the version before a swap, with their versions. */
299
+ export async function fetchComments(port, summary) {
300
+ const state = await call(port, "GET", `${summary.url}api/state`);
301
+ const parse = (raw) => (Array.isArray(raw) ? raw : []).flatMap((item) => {
302
+ const comment = parseComment(item);
303
+ return typeof comment === "string" ? [] : [comment];
304
+ });
305
+ if (!isFields(state))
306
+ throw new RegistrationError("the server sent no state");
307
+ const previous = state["previous"];
308
+ const out = [{ version: summary.version, comments: parse(state["comments"]) }];
309
+ if (isFields(previous) && isPlanVersion(previous["version"])) {
310
+ out.push({ version: previous["version"], comments: parse(previous["comments"]) });
311
+ }
312
+ return out;
313
+ }
298
314
  /** One long poll of the session's event log: what happened after `since`, or nothing at timeout. */
299
315
  export async function pollEvents(port, summary, since, timeoutMs) {
300
316
  const path = `${summary.url}api/events?since=${since}&timeout=${timeoutMs}`;
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, rmdirSync, rmSync, writeFileSync }
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { findHarness, HARNESSES, plannedInstall, SKILL_NAME, } from "./harness.js";
5
- class InstallError extends Error {
5
+ export class InstallError extends Error {
6
6
  name = "InstallError";
7
7
  }
8
8
  /**