@shiyuphan/follow-me 0.4.1 → 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 (95) hide show
  1. package/dist/src/cli.js +154 -11
  2. package/dist/src/evolve.js +6 -2
  3. package/dist/src/harness.js +4 -1
  4. package/dist/src/host.js +17 -1
  5. package/dist/src/install.js +1 -1
  6. package/dist/src/prompt.js +170 -166
  7. package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-byISxePP.js → abnfDiagram-WEYOIA3C-BXkLs7mV.js} +1 -1
  8. package/dist/ui/dist/assets/{arc-CBzsAsP2.js → arc-DNzjw7Ie.js} +1 -1
  9. package/dist/ui/dist/assets/{architecture-WOLXFQ4H-0OSkIwdB.js → architecture-WOLXFQ4H-D66kgJJt.js} +1 -1
  10. package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-ixHybNqO.js → architectureDiagram-ZHVYKT53-D8UWuhQr.js} +1 -1
  11. package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-1F8XL_Ou.js → blockDiagram-RJAE7QA3-BLkmr62i.js} +1 -1
  12. package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-D9PFBXnc.js → c4Diagram-XDPPPE54-3rZPYwOc.js} +1 -1
  13. package/dist/ui/dist/assets/channel-B9JVdOBZ.js +1 -0
  14. package/dist/ui/dist/assets/{chunk-2E7WHVMD-zZdNfMkj.js → chunk-2E7WHVMD-MMnvAgpB.js} +1 -1
  15. package/dist/ui/dist/assets/{chunk-4EMNKR2T-CTKoxSSj.js → chunk-4EMNKR2T-D9FGWA3l.js} +1 -1
  16. package/dist/ui/dist/assets/{chunk-55XCU76K-ifFnDlcS.js → chunk-55XCU76K-DuOUaV8F.js} +1 -1
  17. package/dist/ui/dist/assets/{chunk-7S6XSTFO-BCMVdi_1.js → chunk-7S6XSTFO-p3YWPv0m.js} +1 -1
  18. package/dist/ui/dist/assets/{chunk-AQDTSKZB-DC-5Whju.js → chunk-AQDTSKZB-DWfKYjwk.js} +1 -1
  19. package/dist/ui/dist/assets/{chunk-DSNLSHJZ-DASbe_gb.js → chunk-DSNLSHJZ-C3CIwpv0.js} +1 -1
  20. package/dist/ui/dist/assets/{chunk-HAGFOTED-CArytDjj.js → chunk-HAGFOTED-Bwfl9gme.js} +1 -1
  21. package/dist/ui/dist/assets/{chunk-HNE4Q36P-Dnl-Wtud.js → chunk-HNE4Q36P-0_oeU0le.js} +1 -1
  22. package/dist/ui/dist/assets/{chunk-NQOKRVIF-wvSLaeaK.js → chunk-NQOKRVIF-z_LPPPx5.js} +1 -1
  23. package/dist/ui/dist/assets/{chunk-UHW7PFBP-CckaOGiQ.js → chunk-UHW7PFBP-BH28V2yu.js} +1 -1
  24. package/dist/ui/dist/assets/{chunk-VQ5W62QI-kQBMDJ7V.js → chunk-VQ5W62QI-BXDajDf7.js} +1 -1
  25. package/dist/ui/dist/assets/{chunk-WQQL4CDK-CMg6bbLU.js → chunk-WQQL4CDK-BphBlfKj.js} +1 -1
  26. package/dist/ui/dist/assets/{chunk-XXDRQBXY-CYlFr19K.js → chunk-XXDRQBXY-BiCT5NwV.js} +1 -1
  27. package/dist/ui/dist/assets/{chunk-YZO67HFG-CqkG1vVk.js → chunk-YZO67HFG-Bg5Pu8BX.js} +1 -1
  28. package/dist/ui/dist/assets/{chunk-ZAUX2LTX-BUNEdx-e.js → chunk-ZAUX2LTX-C_Tgd8tB.js} +1 -1
  29. package/dist/ui/dist/assets/{chunk-ZCHMQSOR-CFTbvcrm.js → chunk-ZCHMQSOR-MBRcMi8-.js} +1 -1
  30. package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-t2OdJ0LA.js → classDiagram-v2-4MYWR3UA-1wyeCe5y.js} +1 -1
  31. package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-FGm030gc.js → cose-bilkent-JH36ORCC-BtOYclF6.js} +1 -1
  32. package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-BtU33Lsu.js → cynefin-EF2NZ3EQ-dJmoOG8g.js} +1 -1
  33. package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-DUr-I7hS.js → cynefinDiagram-WF24UDRP-AloyqbEN.js} +1 -1
  34. package/dist/ui/dist/assets/{dagre-NK4GIZVX-BWTHwJKt.js → dagre-NK4GIZVX-NCZhdnfb.js} +1 -1
  35. package/dist/ui/dist/assets/{diagram-CBK3MWTC-PcHrsEBn.js → diagram-CBK3MWTC-D8w7x5Gb.js} +1 -1
  36. package/dist/ui/dist/assets/{diagram-I5XIIV2D-CkGtKjYj.js → diagram-I5XIIV2D-BUFnCGlp.js} +1 -1
  37. package/dist/ui/dist/assets/{diagram-ODIL2UCW-B_HdBB1w.js → diagram-ODIL2UCW-BULmHKaW.js} +1 -1
  38. package/dist/ui/dist/assets/{diagram-PFBEJUXE-DL9zayE6.js → diagram-PFBEJUXE-B5MqFo0r.js} +1 -1
  39. package/dist/ui/dist/assets/{diagram-RJA3I4VV-By5wl8nK.js → diagram-RJA3I4VV-BIkDLL8E.js} +1 -1
  40. package/dist/ui/dist/assets/{diagram-TYUNFSNR-ziWPVayL.js → diagram-TYUNFSNR-Ca4gsQV8.js} +1 -1
  41. package/dist/ui/dist/assets/{dist-NDVqCM7k.js → dist-G85JKY5k.js} +1 -1
  42. package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DH4YGkPc.js → ebnfDiagram-V4P3TTPT-rVjcFFtb.js} +1 -1
  43. package/dist/ui/dist/assets/{elk-QONTBCTQ-BL2RlCEv.js → elk-QONTBCTQ-BWPdvULh.js} +1 -1
  44. package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BKbTMaRp.js → erDiagram-KYQIGQAA-BPxJJkRl.js} +1 -1
  45. package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-Dk5RCWc-.js → eventmodeling-K75KTNOO-BSB9WYV3.js} +1 -1
  46. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-N-Spyk2o.js +1 -0
  47. package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-3vM9gxeT.js → ganttDiagram-R7GPNTJY-DKps530K.js} +1 -1
  48. package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-CXGFhz54.js → gitGraph-VSP46ZUC-D1d4UAFV.js} +1 -1
  49. package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js → gitGraphDiagram-PDYL7XHY-N55yl6hc.js} +1 -1
  50. package/dist/ui/dist/assets/index-BKtWBS7Z.js +134 -0
  51. package/dist/ui/dist/assets/{info-OHQRW6UA-DfSsZ8ib.js → info-OHQRW6UA-CgxE3gf9.js} +1 -1
  52. package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-B_7tyrL5.js → infoDiagram-MVDRKAX5-DOnUrnMf.js} +1 -1
  53. package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DUvHljpg.js → ishikawaDiagram-HPEHV2GE-BIY0fFHB.js} +1 -1
  54. package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-BCgqzrb8.js → journeyDiagram-BOSHW7O7-CqA2Ern0.js} +1 -1
  55. package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DOzZ3NmE.js → kanban-definition-FNSA4HC5-vGrpQKIF.js} +1 -1
  56. package/dist/ui/dist/assets/{line-B-mPFVll.js → line-Brzv3qA6.js} +1 -1
  57. package/dist/ui/dist/assets/{linear-k7o0jout.js → linear-D9uIlqyh.js} +1 -1
  58. package/dist/ui/dist/assets/{mermaid-parser.core-CAyLSQpH.js → mermaid-parser.core-Bz9b-Thu.js} +3 -3
  59. package/dist/ui/dist/assets/{mermaid.core-vfVMjgx2.js → mermaid.core-BaZipu3b.js} +4 -4
  60. package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-B6y-B_r4.js → mindmap-definition-6OY57UG3-D6aie7uH.js} +1 -1
  61. package/dist/ui/dist/assets/{packet-JDAUHWVQ-C_r4ed30.js → packet-JDAUHWVQ-28r3jVFY.js} +1 -1
  62. package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-B8VUm3Bh.js → pegDiagram-ECIUJNBX-CbGvD4qP.js} +1 -1
  63. package/dist/ui/dist/assets/{pie-XZMESJXO-DcwtbH9y.js → pie-XZMESJXO-DHpcGnZd.js} +1 -1
  64. package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-CvUNBi2F.js → pieDiagram-GNMPY74T-DuhauwnY.js} +1 -1
  65. package/dist/ui/dist/assets/{prism-core-_2ju85Ya.js → prism-core-CbZMOvrb.js} +1 -1
  66. package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-Dg2eVS3j.js → quadrantDiagram-D57K4MC3-BfyNmVca.js} +1 -1
  67. package/dist/ui/dist/assets/{radar-ABXABTNO-xCNTcZE9.js → radar-ABXABTNO-Xp1TYwaj.js} +1 -1
  68. package/dist/ui/dist/assets/{railroad-I3PHUGI6-BI-sIL2Q.js → railroad-I3PHUGI6-c5FY4wwa.js} +1 -1
  69. package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-Ddvk40Wx.js → railroad-abnf-LNEFI6M7-RDyWXqcL.js} +1 -1
  70. package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-h35gE183.js → railroad-ebnf-DWYD2UWJ-CLD4jVou.js} +1 -1
  71. package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Zim9iPdp.js → railroad-peg-JR7BVNR7-17EYTA-w.js} +1 -1
  72. package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-Cz6fdmOQ.js → railroadDiagram-Y72NRBL7-C4p2wtKe.js} +1 -1
  73. package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-DTJkCrcD.js → requirementDiagram-EQYSW5RA-DTYIFN8T.js} +1 -1
  74. package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-Bg1iiVla.js → sankeyDiagram-SY24JHO5-YDYhRyJU.js} +1 -1
  75. package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-9XfetsRA.js → sequenceDiagram-BLEHNHQU-Crvy6IlU.js} +1 -1
  76. package/dist/ui/dist/assets/{src-Dym5afQ5.js → src-BPod-Ype.js} +1 -1
  77. package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMmcMV8z.js → stateDiagram-v2-A6IESKWZ-BWr70YOQ.js} +1 -1
  78. package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-B2PcshTY.js → swimlanes-B3ENRKFQ-DvbG4I9a.js} +1 -1
  79. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-D2wU-jW6.js +8 -0
  80. package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-Cr1hCNtW.js → timeline-definition-JXYKGLGB-CR950Bzb.js} +1 -1
  81. package/dist/ui/dist/assets/{treeView-D4JQ5SDB-jH5ksKnh.js → treeView-D4JQ5SDB-DSDDXGrk.js} +1 -1
  82. package/dist/ui/dist/assets/{treemap-SAJKECNS-BV90e8QL.js → treemap-SAJKECNS-Ct5w2_RR.js} +1 -1
  83. package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-BNwq5aJV.js → usecaseDiagram-WPOLBNNX-_eP0371y.js} +1 -1
  84. package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-D-lLyZCL.js → vennDiagram-Q3Y3IDCW-Cu-IA57L.js} +1 -1
  85. package/dist/ui/dist/assets/{wardley-7MLQ67FV-CWfHQUGt.js → wardley-7MLQ67FV-r8yd6frN.js} +1 -1
  86. package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-BL_qNKVd.js → wardleyDiagram-IVAIDKWA-CBUUiRQm.js} +1 -1
  87. package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-BHPK5Snp.js → xychartDiagram-4URNHDU4-DEVQjvgG.js} +1 -1
  88. package/dist/ui/dist/index.html +1 -1
  89. package/package.json +1 -1
  90. package/skills/follow-me/SKILL.md +69 -7
  91. package/skills/follow-me/porting.md +7 -0
  92. package/dist/ui/dist/assets/channel-B-fwsHf1.js +0 -1
  93. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +0 -1
  94. package/dist/ui/dist/assets/index-CBbx_pJk.js +0 -134
  95. 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": {
@@ -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
  /**