@shiyuphan/follow-me 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli.js +171 -14
- package/dist/src/comments.js +62 -9
- package/dist/src/evolve.js +146 -26
- package/dist/src/harness.js +4 -1
- package/dist/src/host.js +29 -4
- package/dist/src/install.js +1 -1
- package/dist/src/plan.js +14 -0
- package/dist/src/prompt.js +270 -178
- package/dist/src/server.js +106 -29
- package/dist/src/session.js +23 -0
- package/dist/ui/dist/assets/{abnfDiagram-WEYOIA3C-byISxePP.js → abnfDiagram-WEYOIA3C-8UsLPznO.js} +1 -1
- package/dist/ui/dist/assets/{arc-CBzsAsP2.js → arc-vovjGalz.js} +1 -1
- package/dist/ui/dist/assets/{architecture-WOLXFQ4H-0OSkIwdB.js → architecture-WOLXFQ4H-BBMK-PUu.js} +1 -1
- package/dist/ui/dist/assets/{architectureDiagram-ZHVYKT53-ixHybNqO.js → architectureDiagram-ZHVYKT53-Bw_ehaQk.js} +1 -1
- package/dist/ui/dist/assets/{blockDiagram-RJAE7QA3-1F8XL_Ou.js → blockDiagram-RJAE7QA3-CAmD0wKf.js} +1 -1
- package/dist/ui/dist/assets/{c4Diagram-XDPPPE54-D9PFBXnc.js → c4Diagram-XDPPPE54-DCP-3zgt.js} +1 -1
- package/dist/ui/dist/assets/channel-B7CT_oIp.js +1 -0
- package/dist/ui/dist/assets/{chunk-2E7WHVMD-zZdNfMkj.js → chunk-2E7WHVMD-BiHSeJMu.js} +1 -1
- package/dist/ui/dist/assets/{chunk-4EMNKR2T-CTKoxSSj.js → chunk-4EMNKR2T-CR3lHKf_.js} +1 -1
- package/dist/ui/dist/assets/{chunk-55XCU76K-ifFnDlcS.js → chunk-55XCU76K-CtaoloTT.js} +1 -1
- package/dist/ui/dist/assets/{chunk-7S6XSTFO-BCMVdi_1.js → chunk-7S6XSTFO-eABdc7fu.js} +1 -1
- package/dist/ui/dist/assets/{chunk-AQDTSKZB-DC-5Whju.js → chunk-AQDTSKZB-CPlbJWi-.js} +1 -1
- package/dist/ui/dist/assets/{chunk-DSNLSHJZ-DASbe_gb.js → chunk-DSNLSHJZ-YiMuxsb-.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HAGFOTED-CArytDjj.js → chunk-HAGFOTED-1VcdL78a.js} +1 -1
- package/dist/ui/dist/assets/{chunk-HNE4Q36P-Dnl-Wtud.js → chunk-HNE4Q36P-u-sr_0Qu.js} +1 -1
- package/dist/ui/dist/assets/{chunk-NQOKRVIF-wvSLaeaK.js → chunk-NQOKRVIF-C_QlWSze.js} +1 -1
- package/dist/ui/dist/assets/{chunk-UHW7PFBP-CckaOGiQ.js → chunk-UHW7PFBP-CNuZhsbU.js} +1 -1
- package/dist/ui/dist/assets/{chunk-VQ5W62QI-kQBMDJ7V.js → chunk-VQ5W62QI-DAHWbLqU.js} +1 -1
- package/dist/ui/dist/assets/{chunk-WQQL4CDK-CMg6bbLU.js → chunk-WQQL4CDK-DWQnxFCd.js} +1 -1
- package/dist/ui/dist/assets/{chunk-XXDRQBXY-CYlFr19K.js → chunk-XXDRQBXY-B0lGgSOR.js} +1 -1
- package/dist/ui/dist/assets/{chunk-YZO67HFG-CqkG1vVk.js → chunk-YZO67HFG-BDw7efkN.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZAUX2LTX-BUNEdx-e.js → chunk-ZAUX2LTX-B5wfHsZv.js} +1 -1
- package/dist/ui/dist/assets/{chunk-ZCHMQSOR-CFTbvcrm.js → chunk-ZCHMQSOR--rWtYtAG.js} +1 -1
- package/dist/ui/dist/assets/{classDiagram-v2-4MYWR3UA-t2OdJ0LA.js → classDiagram-v2-4MYWR3UA-BKuQ_xoS.js} +1 -1
- package/dist/ui/dist/assets/{cose-bilkent-JH36ORCC-FGm030gc.js → cose-bilkent-JH36ORCC-Dk5fIpU0.js} +1 -1
- package/dist/ui/dist/assets/{cynefin-EF2NZ3EQ-BtU33Lsu.js → cynefin-EF2NZ3EQ-DIykJb2g.js} +1 -1
- package/dist/ui/dist/assets/{cynefinDiagram-WF24UDRP-DUr-I7hS.js → cynefinDiagram-WF24UDRP-Cg8tBqUb.js} +1 -1
- package/dist/ui/dist/assets/{dagre-NK4GIZVX-BWTHwJKt.js → dagre-NK4GIZVX-BC_tnnjz.js} +1 -1
- package/dist/ui/dist/assets/{diagram-CBK3MWTC-PcHrsEBn.js → diagram-CBK3MWTC-B_MQGixU.js} +1 -1
- package/dist/ui/dist/assets/{diagram-I5XIIV2D-CkGtKjYj.js → diagram-I5XIIV2D-DO681pZ5.js} +1 -1
- package/dist/ui/dist/assets/{diagram-ODIL2UCW-B_HdBB1w.js → diagram-ODIL2UCW-D14jLzU6.js} +1 -1
- package/dist/ui/dist/assets/{diagram-PFBEJUXE-DL9zayE6.js → diagram-PFBEJUXE-cML48gUS.js} +1 -1
- package/dist/ui/dist/assets/{diagram-RJA3I4VV-By5wl8nK.js → diagram-RJA3I4VV-DSoq8jFk.js} +1 -1
- package/dist/ui/dist/assets/{diagram-TYUNFSNR-ziWPVayL.js → diagram-TYUNFSNR-CaJ4pvnl.js} +1 -1
- package/dist/ui/dist/assets/{dist-NDVqCM7k.js → dist-D3j-DODP.js} +1 -1
- package/dist/ui/dist/assets/{ebnfDiagram-V4P3TTPT-DH4YGkPc.js → ebnfDiagram-V4P3TTPT-CyRpjSix.js} +1 -1
- package/dist/ui/dist/assets/{elk-QONTBCTQ-BL2RlCEv.js → elk-QONTBCTQ-ICJdzVFL.js} +1 -1
- package/dist/ui/dist/assets/{erDiagram-KYQIGQAA-BKbTMaRp.js → erDiagram-KYQIGQAA-B57hNTfw.js} +1 -1
- package/dist/ui/dist/assets/{eventmodeling-K75KTNOO-Dk5RCWc-.js → eventmodeling-K75KTNOO-BG8tUVIQ.js} +1 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU--HPNoVQJ.js +1 -0
- package/dist/ui/dist/assets/{ganttDiagram-R7GPNTJY-3vM9gxeT.js → ganttDiagram-R7GPNTJY-Czt6Klik.js} +1 -1
- package/dist/ui/dist/assets/{gitGraph-VSP46ZUC-CXGFhz54.js → gitGraph-VSP46ZUC-De_ZWJLj.js} +1 -1
- package/dist/ui/dist/assets/{gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js → gitGraphDiagram-PDYL7XHY-BMCJoOnT.js} +1 -1
- package/dist/ui/dist/assets/index-CYKbk4QS.js +134 -0
- package/dist/ui/dist/assets/{info-OHQRW6UA-DfSsZ8ib.js → info-OHQRW6UA-D75H9kUq.js} +1 -1
- package/dist/ui/dist/assets/{infoDiagram-MVDRKAX5-B_7tyrL5.js → infoDiagram-MVDRKAX5-Bsm_I9dt.js} +1 -1
- package/dist/ui/dist/assets/{ishikawaDiagram-HPEHV2GE-DUvHljpg.js → ishikawaDiagram-HPEHV2GE-98xZAJ9y.js} +1 -1
- package/dist/ui/dist/assets/{journeyDiagram-BOSHW7O7-BCgqzrb8.js → journeyDiagram-BOSHW7O7-BWVGuc0P.js} +1 -1
- package/dist/ui/dist/assets/{kanban-definition-FNSA4HC5-DOzZ3NmE.js → kanban-definition-FNSA4HC5-DgUE_KDd.js} +1 -1
- package/dist/ui/dist/assets/{line-B-mPFVll.js → line-i__8Rvas.js} +1 -1
- package/dist/ui/dist/assets/{linear-k7o0jout.js → linear-BXKj0Fip.js} +1 -1
- package/dist/ui/dist/assets/{mermaid-parser.core-CAyLSQpH.js → mermaid-parser.core-DxvaorTa.js} +3 -3
- package/dist/ui/dist/assets/{mermaid.core-vfVMjgx2.js → mermaid.core-ujYTsRZz.js} +4 -4
- package/dist/ui/dist/assets/{mindmap-definition-6OY57UG3-B6y-B_r4.js → mindmap-definition-6OY57UG3-B7QD6sjk.js} +1 -1
- package/dist/ui/dist/assets/{packet-JDAUHWVQ-C_r4ed30.js → packet-JDAUHWVQ-B6aIwfD1.js} +1 -1
- package/dist/ui/dist/assets/{pegDiagram-ECIUJNBX-B8VUm3Bh.js → pegDiagram-ECIUJNBX-_RStHj-a.js} +1 -1
- package/dist/ui/dist/assets/{pie-XZMESJXO-DcwtbH9y.js → pie-XZMESJXO-CNLC-pTe.js} +1 -1
- package/dist/ui/dist/assets/{pieDiagram-GNMPY74T-CvUNBi2F.js → pieDiagram-GNMPY74T-0uT7MT-u.js} +1 -1
- package/dist/ui/dist/assets/{prism-core-_2ju85Ya.js → prism-core-DtxziK1i.js} +1 -1
- package/dist/ui/dist/assets/{quadrantDiagram-D57K4MC3-Dg2eVS3j.js → quadrantDiagram-D57K4MC3-CTMKb5hl.js} +1 -1
- package/dist/ui/dist/assets/{radar-ABXABTNO-xCNTcZE9.js → radar-ABXABTNO-D53h6I0N.js} +1 -1
- package/dist/ui/dist/assets/{railroad-I3PHUGI6-BI-sIL2Q.js → railroad-I3PHUGI6-CwjhW4mc.js} +1 -1
- package/dist/ui/dist/assets/{railroad-abnf-LNEFI6M7-Ddvk40Wx.js → railroad-abnf-LNEFI6M7-DDbQ3RmB.js} +1 -1
- package/dist/ui/dist/assets/{railroad-ebnf-DWYD2UWJ-h35gE183.js → railroad-ebnf-DWYD2UWJ-Am7YJAqO.js} +1 -1
- package/dist/ui/dist/assets/{railroad-peg-JR7BVNR7-Zim9iPdp.js → railroad-peg-JR7BVNR7-Ci32KoVW.js} +1 -1
- package/dist/ui/dist/assets/{railroadDiagram-Y72NRBL7-Cz6fdmOQ.js → railroadDiagram-Y72NRBL7-BPU5A6XW.js} +1 -1
- package/dist/ui/dist/assets/{requirementDiagram-EQYSW5RA-DTJkCrcD.js → requirementDiagram-EQYSW5RA-_0Pq4QdV.js} +1 -1
- package/dist/ui/dist/assets/{sankeyDiagram-SY24JHO5-Bg1iiVla.js → sankeyDiagram-SY24JHO5-c9h-7uf8.js} +1 -1
- package/dist/ui/dist/assets/{sequenceDiagram-BLEHNHQU-9XfetsRA.js → sequenceDiagram-BLEHNHQU-B9-S4Rc9.js} +1 -1
- package/dist/ui/dist/assets/{src-Dym5afQ5.js → src-BZ3kG7lO.js} +1 -1
- package/dist/ui/dist/assets/{stateDiagram-v2-A6IESKWZ-CMmcMV8z.js → stateDiagram-v2-A6IESKWZ-py1smSen.js} +1 -1
- package/dist/ui/dist/assets/{swimlanes-B3ENRKFQ-B2PcshTY.js → swimlanes-B3ENRKFQ-C1E90aH4.js} +1 -1
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-I98Q8AqP.js +8 -0
- package/dist/ui/dist/assets/{timeline-definition-JXYKGLGB-Cr1hCNtW.js → timeline-definition-JXYKGLGB-DNe1To6B.js} +1 -1
- package/dist/ui/dist/assets/{treeView-D4JQ5SDB-jH5ksKnh.js → treeView-D4JQ5SDB-D6MG89i6.js} +1 -1
- package/dist/ui/dist/assets/{treemap-SAJKECNS-BV90e8QL.js → treemap-SAJKECNS-CZpt7Mlh.js} +1 -1
- package/dist/ui/dist/assets/{usecaseDiagram-WPOLBNNX-BNwq5aJV.js → usecaseDiagram-WPOLBNNX-CKsv-03M.js} +1 -1
- package/dist/ui/dist/assets/{vennDiagram-Q3Y3IDCW-D-lLyZCL.js → vennDiagram-Q3Y3IDCW-DGJ1qlyJ.js} +1 -1
- package/dist/ui/dist/assets/{wardley-7MLQ67FV-CWfHQUGt.js → wardley-7MLQ67FV-ClOzBemC.js} +1 -1
- package/dist/ui/dist/assets/{wardleyDiagram-IVAIDKWA-BL_qNKVd.js → wardleyDiagram-IVAIDKWA-C68ahPyF.js} +1 -1
- package/dist/ui/dist/assets/{xychartDiagram-4URNHDU4-BHPK5Snp.js → xychartDiagram-4URNHDU4-DWDamCoH.js} +1 -1
- package/dist/ui/dist/index.html +1 -1
- package/package.json +1 -1
- package/skills/follow-me/SKILL.md +69 -7
- package/skills/follow-me/porting.md +7 -0
- package/dist/ui/dist/assets/channel-B-fwsHf1.js +0 -1
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +0 -1
- package/dist/ui/dist/assets/index-CBbx_pJk.js +0 -134
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +0 -8
package/dist/src/cli.js
CHANGED
|
@@ -5,13 +5,13 @@ 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
|
-
import { BranchMovedError, cleanSessions, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sinceLastPlan, } from "./session.js";
|
|
14
|
+
import { BranchMovedError, cleanSessions, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, sessionComments, sinceLastPlan, } from "./session.js";
|
|
15
15
|
import { checkSyntax } from "./syntax.js";
|
|
16
16
|
class UsageError extends Error {
|
|
17
17
|
name = "UsageError";
|
|
@@ -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(
|
|
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);
|
|
@@ -889,7 +909,9 @@ function finished(session, stored) {
|
|
|
889
909
|
const text = renderStored(session, stored);
|
|
890
910
|
if (text === null)
|
|
891
911
|
return 1;
|
|
892
|
-
|
|
912
|
+
// A review that swapped versions holds what its earlier versions said too.
|
|
913
|
+
const said = stored.review.comments.length + (stored.review.carried?.length ?? 0);
|
|
914
|
+
say(`review v${stored.version} ended with ${plural(said, "comment")}`);
|
|
893
915
|
process.stdout.write(text);
|
|
894
916
|
say();
|
|
895
917
|
next("done; the user can port it with follow-me report --json");
|
|
@@ -917,8 +939,7 @@ function evolveHandoff(session, cwd, mode, args, moved, hosted) {
|
|
|
917
939
|
session.writeDraft(serializePlanFile(moved.draft));
|
|
918
940
|
if (draftState(session).kind === "valid")
|
|
919
941
|
return null;
|
|
920
|
-
|
|
921
|
-
session.writePrompt(renderPrompt(promptInput(session, mode, args, { since, vanished, added })));
|
|
942
|
+
session.writePrompt(renderPrompt(promptInput(session, mode, args, moved)));
|
|
922
943
|
say(`wrote ${relative(cwd, session.promptPath)}`);
|
|
923
944
|
const step = `assign the new units in ${relative(cwd, session.draftPath)}, then run follow-me again`;
|
|
924
945
|
next(hosted ? `${step}; ${STILL_WORKING}` : step);
|
|
@@ -1058,9 +1079,12 @@ async function listen(session, open, args) {
|
|
|
1058
1079
|
const news = fresh.filter(isNews);
|
|
1059
1080
|
if (news.length > 0) {
|
|
1060
1081
|
const ctx = { commits: projectedCommits(session, version), units: session.diff.units };
|
|
1061
|
-
process.stdout.write(renderEvents(news, ctx));
|
|
1062
1082
|
const ended = news.some((e) => e.type === "ended");
|
|
1063
|
-
|
|
1083
|
+
const done = ended || news.some((e) => e.type === "submitted");
|
|
1084
|
+
// The report that follows quotes every comment, so the digest only says what else happened.
|
|
1085
|
+
const shown = done ? news.filter((e) => !isCommentEvent(e)) : news;
|
|
1086
|
+
process.stdout.write(renderEvents(shown, ctx));
|
|
1087
|
+
if (done) {
|
|
1064
1088
|
say();
|
|
1065
1089
|
if (ended) {
|
|
1066
1090
|
const stored = reviewsOf(session).find((r) => r.version === version);
|
|
@@ -1218,7 +1242,19 @@ function renderStored(session, stored, format = "markdown") {
|
|
|
1218
1242
|
.plans()
|
|
1219
1243
|
.filter((p) => p.version > stored.version)
|
|
1220
1244
|
.flatMap((p) => p.file.responses);
|
|
1221
|
-
const
|
|
1245
|
+
const { carried } = stored.review;
|
|
1246
|
+
const said = reviewsOf(session)
|
|
1247
|
+
.filter((r) => r.version <= stored.version)
|
|
1248
|
+
.map((r) => r.review);
|
|
1249
|
+
const earlier = carried === undefined
|
|
1250
|
+
? {}
|
|
1251
|
+
: {
|
|
1252
|
+
earlier: {
|
|
1253
|
+
comments: sessionComments(carried, said),
|
|
1254
|
+
concerns: stored.review.carriedConcerns ?? [],
|
|
1255
|
+
},
|
|
1256
|
+
};
|
|
1257
|
+
const opts = { branch: session.anchor.branch, responses, ...earlier };
|
|
1222
1258
|
if (format === "json") {
|
|
1223
1259
|
const items = reportItems(stored.review, verdict.plan.commits, session.diff.units, opts);
|
|
1224
1260
|
return `${JSON.stringify(items, null, 2)}\n`;
|
|
@@ -1259,6 +1295,9 @@ async function feedback(cwd, args) {
|
|
|
1259
1295
|
console.error("follow-me: feedback is now report; printing the same");
|
|
1260
1296
|
return report(cwd, args, args.flags.has("walk"));
|
|
1261
1297
|
}
|
|
1298
|
+
function isCommentEvent(event) {
|
|
1299
|
+
return event.type === "comment" || event.type === "comment-updated";
|
|
1300
|
+
}
|
|
1262
1301
|
/** Events the reviewer caused; the agent's own replies, explanations and version swaps are not news. */
|
|
1263
1302
|
function isNews(event) {
|
|
1264
1303
|
if (event.type === "version")
|
|
@@ -1357,7 +1396,7 @@ async function reply(cwd, args) {
|
|
|
1357
1396
|
...(diagram === undefined ? {} : { diagram }),
|
|
1358
1397
|
});
|
|
1359
1398
|
say(`replied ${id} to ${commentId}`);
|
|
1360
|
-
next(
|
|
1399
|
+
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1361
1400
|
return 0;
|
|
1362
1401
|
}
|
|
1363
1402
|
/** The Mermaid source in --diagram's file, or undefined without the flag. */
|
|
@@ -1474,7 +1513,123 @@ async function explain(cwd, args) {
|
|
|
1474
1513
|
...(diagram === undefined ? {} : { diagram }),
|
|
1475
1514
|
});
|
|
1476
1515
|
say(`explained ${id}`);
|
|
1477
|
-
next(
|
|
1516
|
+
next(`run follow-me again; ${STILL_WORKING}`);
|
|
1517
|
+
return 0;
|
|
1518
|
+
}
|
|
1519
|
+
const SHOW_USAGE = "usage: follow-me show <h_…|c_…|k_…|path:line[@<commit>|@base]>";
|
|
1520
|
+
/** Every comment the session has, with the plan version it was made on: live ones first. */
|
|
1521
|
+
async function knownComments(session) {
|
|
1522
|
+
const open = await openOnServer(session, null);
|
|
1523
|
+
const live = open === null ? [] : await fetchComments(open.port, open.summary);
|
|
1524
|
+
const saved = [
|
|
1525
|
+
session.priorReview("split", null),
|
|
1526
|
+
session.priorReview("walk", null),
|
|
1527
|
+
...reviewsOf(session).map((r) => r.review),
|
|
1528
|
+
].flatMap((r) => (r === null ? [] : [{ version: r.planVersion, comments: r.comments }]));
|
|
1529
|
+
return [...live, ...saved];
|
|
1530
|
+
}
|
|
1531
|
+
const SHOW_MARGIN = 3;
|
|
1532
|
+
/**
|
|
1533
|
+
* The lines around path:line as commit `at` of the latest plan writes the file (1-based), the
|
|
1534
|
+
* base, or the tip when `at` is undefined. Read from the plan, so it works before any apply.
|
|
1535
|
+
*/
|
|
1536
|
+
function showLines(session, raw) {
|
|
1537
|
+
const m = /^(.+):(\d+)(?:@(\d+|base))?$/.exec(raw);
|
|
1538
|
+
const [, typed, lineRaw, at] = m ?? [];
|
|
1539
|
+
if (typed === undefined || lineRaw === undefined)
|
|
1540
|
+
throw new UsageError(SHOW_USAGE);
|
|
1541
|
+
const path = Buffer.from(typed, "utf8").toString("latin1");
|
|
1542
|
+
const file = [...session.diff.textFiles.values()].find((f) => f.path === path);
|
|
1543
|
+
if (file === undefined) {
|
|
1544
|
+
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`;
|
|
1545
|
+
}
|
|
1546
|
+
let text;
|
|
1547
|
+
let label;
|
|
1548
|
+
if (at === undefined) {
|
|
1549
|
+
text = file.tip?.bytes ?? null;
|
|
1550
|
+
label = "the tip";
|
|
1551
|
+
}
|
|
1552
|
+
else if (at === "base") {
|
|
1553
|
+
text = file.base === null ? null : file.base.lines.join("");
|
|
1554
|
+
label = "the base";
|
|
1555
|
+
}
|
|
1556
|
+
else {
|
|
1557
|
+
const latest = session.plans().at(-1);
|
|
1558
|
+
const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
|
|
1559
|
+
const n = Number(at);
|
|
1560
|
+
if (n < 1 || n > commits.length) {
|
|
1561
|
+
throw new UsageError(`@${at} is not one of the ${commits.length} commits of the plan`);
|
|
1562
|
+
}
|
|
1563
|
+
text = file.base === null ? null : file.base.lines.join("");
|
|
1564
|
+
for (const commit of commits.slice(0, n)) {
|
|
1565
|
+
for (const change of commit.changes) {
|
|
1566
|
+
if (change.path !== path)
|
|
1567
|
+
continue;
|
|
1568
|
+
text = change.op === "write" ? change.bytes : null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
label = `commit ${n}`;
|
|
1572
|
+
}
|
|
1573
|
+
if (text === null)
|
|
1574
|
+
return `${typed} does not exist in ${label}\n`;
|
|
1575
|
+
const lines = Buffer.from(text, "latin1").toString("utf8").split("\n");
|
|
1576
|
+
if (lines.at(-1) === "")
|
|
1577
|
+
lines.pop();
|
|
1578
|
+
const line = Number(lineRaw);
|
|
1579
|
+
if (line < 1 || line > lines.length) {
|
|
1580
|
+
return `${typed} has ${plural(lines.length, "line")} in ${label}\n`;
|
|
1581
|
+
}
|
|
1582
|
+
const from = Math.max(1, line - SHOW_MARGIN);
|
|
1583
|
+
const to = Math.min(lines.length, line + SHOW_MARGIN);
|
|
1584
|
+
const width = String(to).length;
|
|
1585
|
+
const rows = lines.slice(from - 1, to).map((l, i) => {
|
|
1586
|
+
const n = from + i;
|
|
1587
|
+
return `${n === line ? ">" : " "} ${String(n).padStart(width)} ${l}`;
|
|
1588
|
+
});
|
|
1589
|
+
return `${typed}:${line} in ${label}\n${rows.join("\n")}\n`;
|
|
1590
|
+
}
|
|
1591
|
+
/** Expands a ref the agent was given to the lines and thread it names, so no file read is needed. */
|
|
1592
|
+
async function showRef(cwd, args) {
|
|
1593
|
+
const [ref, extra] = args.positionals;
|
|
1594
|
+
if (ref === undefined || extra !== undefined)
|
|
1595
|
+
throw new UsageError(SHOW_USAGE);
|
|
1596
|
+
const session = await openSession(cwd, sessionOpts(args));
|
|
1597
|
+
const { units } = session.diff;
|
|
1598
|
+
const latest = session.plans().at(-1);
|
|
1599
|
+
if (isUnitId(ref)) {
|
|
1600
|
+
const unit = units.get(ref);
|
|
1601
|
+
if (unit === undefined) {
|
|
1602
|
+
throw new UsageError(`${ref} is not a unit of the branch now; its hunk changed`);
|
|
1603
|
+
}
|
|
1604
|
+
const commits = latest === undefined ? [] : projectedCommits(session, latest.version);
|
|
1605
|
+
process.stdout.write(renderShownUnit(unit, commits));
|
|
1606
|
+
return 0;
|
|
1607
|
+
}
|
|
1608
|
+
if (/^c_[a-z2-7]{6}$/.test(ref) && isCommentId(ref)) {
|
|
1609
|
+
for (const { version, comments } of await knownComments(session)) {
|
|
1610
|
+
const comment = comments.find((c) => c.id === ref);
|
|
1611
|
+
if (comment === undefined)
|
|
1612
|
+
continue;
|
|
1613
|
+
const ctx = { commits: projectedCommits(session, version), units };
|
|
1614
|
+
process.stdout.write(renderShownComment(comment, ctx, version));
|
|
1615
|
+
return 0;
|
|
1616
|
+
}
|
|
1617
|
+
throw new UsageError(`no comment ${ref} in this session`);
|
|
1618
|
+
}
|
|
1619
|
+
if (isConcernId(ref)) {
|
|
1620
|
+
for (const plan of session.plans().toReversed()) {
|
|
1621
|
+
const commits = projectedCommits(session, plan.version);
|
|
1622
|
+
for (const commit of commits) {
|
|
1623
|
+
const concern = commit.concerns?.find((k) => k.id === ref);
|
|
1624
|
+
if (concern === undefined)
|
|
1625
|
+
continue;
|
|
1626
|
+
process.stdout.write(renderShownConcern(concern, commit.index, { commits, units }));
|
|
1627
|
+
return 0;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
throw new UsageError(`no concern ${ref} in a plan that fits the branch`);
|
|
1631
|
+
}
|
|
1632
|
+
process.stdout.write(showLines(session, ref));
|
|
1478
1633
|
return 0;
|
|
1479
1634
|
}
|
|
1480
1635
|
function describeLock(lock) {
|
|
@@ -1711,6 +1866,8 @@ export async function main(argv, cwd) {
|
|
|
1711
1866
|
const spec = { values: ["commit", "unit", "lines", "body", "diagram", "branch"] };
|
|
1712
1867
|
return await explain(cwd, parseArgs(rest, spec));
|
|
1713
1868
|
}
|
|
1869
|
+
case "show":
|
|
1870
|
+
return await showRef(cwd, parseArgs(rest, { values: ["branch"] }));
|
|
1714
1871
|
case "report":
|
|
1715
1872
|
return await report(cwd, parseArgs(rest, { values: ["branch"], flags: ["json", "markdown"] }));
|
|
1716
1873
|
case "clean": {
|
package/dist/src/comments.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isRepoPath, isUnitId } from "./diff.js";
|
|
2
|
-
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isPlanVersion, isSeverity, } from "./plan.js";
|
|
2
|
+
import { diagramProblem, isArray, isCommentId, isConcernId, isFields, isPlanVersion, isSeverity, parsePlacedConcern, } from "./plan.js";
|
|
3
3
|
/**
|
|
4
4
|
* The wire formats of review state, parsed once at the boundary: comments, replies, concern
|
|
5
5
|
* decisions, the review file, its autosaved draft and the events a hosted session logs. Every
|
|
@@ -253,13 +253,7 @@ function parseEarlierReplies(v) {
|
|
|
253
253
|
function isLine(v) {
|
|
254
254
|
return typeof v === "number" && Number.isInteger(v) && v >= 1;
|
|
255
255
|
}
|
|
256
|
-
|
|
257
|
-
if (!isFields(v)) {
|
|
258
|
-
return "not an object";
|
|
259
|
-
}
|
|
260
|
-
const comment = parseComment(v.comment);
|
|
261
|
-
if (typeof comment === "string")
|
|
262
|
-
return comment;
|
|
256
|
+
function parsePlacement(v) {
|
|
263
257
|
const { unit, offsets, changed, path, newLine } = v;
|
|
264
258
|
if (unit !== null && !(typeof unit === "string" && isUnitId(unit)))
|
|
265
259
|
return "unit is not a unit id";
|
|
@@ -273,7 +267,6 @@ export function parseCarried(v) {
|
|
|
273
267
|
if (newLine !== null && !isLine(newLine))
|
|
274
268
|
return "newLine is not null or a line";
|
|
275
269
|
return {
|
|
276
|
-
comment,
|
|
277
270
|
unit,
|
|
278
271
|
offsets: offsets === null ? null : offsets.filter(isIndex),
|
|
279
272
|
changed,
|
|
@@ -281,6 +274,56 @@ export function parseCarried(v) {
|
|
|
281
274
|
newLine,
|
|
282
275
|
};
|
|
283
276
|
}
|
|
277
|
+
export function parseCarried(v) {
|
|
278
|
+
if (!isFields(v))
|
|
279
|
+
return "not an object";
|
|
280
|
+
const comment = parseComment(v.comment);
|
|
281
|
+
if (typeof comment === "string")
|
|
282
|
+
return comment;
|
|
283
|
+
const placement = parsePlacement(v);
|
|
284
|
+
return typeof placement === "string" ? placement : { comment, ...placement };
|
|
285
|
+
}
|
|
286
|
+
function parseSince(v) {
|
|
287
|
+
if (v === null)
|
|
288
|
+
return null;
|
|
289
|
+
return isPlanVersion(v) ? v : "since is not null or a plan version";
|
|
290
|
+
}
|
|
291
|
+
function parseCarriedRef(v) {
|
|
292
|
+
if (!isFields(v))
|
|
293
|
+
return "not an object";
|
|
294
|
+
const comment = asCommentId(v.comment);
|
|
295
|
+
if (comment === null)
|
|
296
|
+
return "comment is not a comment id";
|
|
297
|
+
if (!isPlanVersion(v.version))
|
|
298
|
+
return "version is not a plan version";
|
|
299
|
+
const since = parseSince(v.since);
|
|
300
|
+
if (typeof since === "string")
|
|
301
|
+
return since;
|
|
302
|
+
const placement = parsePlacement(v);
|
|
303
|
+
if (typeof placement === "string")
|
|
304
|
+
return placement;
|
|
305
|
+
return { ...placement, comment, version: v.version, since };
|
|
306
|
+
}
|
|
307
|
+
function parseCarriedConcern(v) {
|
|
308
|
+
if (!isFields(v)) {
|
|
309
|
+
return "not an object";
|
|
310
|
+
}
|
|
311
|
+
const concern = parsePlacedConcern(v.concern);
|
|
312
|
+
if (typeof concern === "string")
|
|
313
|
+
return `concern: ${concern}`;
|
|
314
|
+
if (!isIndex(v.commit))
|
|
315
|
+
return "commit is not a commit index";
|
|
316
|
+
const decision = parseDecision(v.decision);
|
|
317
|
+
if (typeof decision === "string")
|
|
318
|
+
return `decision: ${decision}`;
|
|
319
|
+
const since = parseSince(v.since);
|
|
320
|
+
if (typeof since === "string")
|
|
321
|
+
return since;
|
|
322
|
+
const placement = parsePlacement(v);
|
|
323
|
+
if (typeof placement === "string")
|
|
324
|
+
return placement;
|
|
325
|
+
return { ...placement, concern, commit: v.commit, decision, since };
|
|
326
|
+
}
|
|
284
327
|
/** Boundary parse of reviews/vN.json. Files written before walk mode existed have no mode. */
|
|
285
328
|
export function parseReview(v) {
|
|
286
329
|
if (!isFields(v)) {
|
|
@@ -314,6 +357,12 @@ export function parseReview(v) {
|
|
|
314
357
|
const earlier = parseList(v.earlierReplies, parseEarlierReplies, "earlierReplies");
|
|
315
358
|
if (typeof earlier === "string")
|
|
316
359
|
return earlier;
|
|
360
|
+
const carried = v.carried === undefined ? null : parseList(v.carried, parseCarriedRef, "carried");
|
|
361
|
+
if (typeof carried === "string")
|
|
362
|
+
return carried;
|
|
363
|
+
const concernsCarried = parseList(v.carriedConcerns, parseCarriedConcern, "carriedConcerns");
|
|
364
|
+
if (typeof concernsCarried === "string")
|
|
365
|
+
return concernsCarried;
|
|
317
366
|
return {
|
|
318
367
|
schema: v.schema,
|
|
319
368
|
mode,
|
|
@@ -325,6 +374,8 @@ export function parseReview(v) {
|
|
|
325
374
|
...(resolved.length === 0 ? {} : { resolved }),
|
|
326
375
|
...(anyExcluded ? { excluded } : {}),
|
|
327
376
|
...(earlier.length === 0 ? {} : { earlierReplies: earlier }),
|
|
377
|
+
...(carried === null ? {} : { carried }),
|
|
378
|
+
...(concernsCarried.length === 0 ? {} : { carriedConcerns: concernsCarried }),
|
|
328
379
|
};
|
|
329
380
|
}
|
|
330
381
|
function lenient(items, parse) {
|
|
@@ -350,6 +401,8 @@ export function parseDraft(v) {
|
|
|
350
401
|
resolved: lenientIds(v.resolved, isCommentIdValue),
|
|
351
402
|
excluded: typeof excluded === "string" ? { comments: [], concerns: [] } : excluded,
|
|
352
403
|
earlierReplies: lenient(v.earlierReplies, parseEarlierReplies),
|
|
404
|
+
carried: v.carried === undefined ? null : lenient(v.carried, parseCarriedRef),
|
|
405
|
+
carriedConcerns: lenient(v.carriedConcerns, parseCarriedConcern),
|
|
353
406
|
};
|
|
354
407
|
}
|
|
355
408
|
export function parseEvent(v) {
|
package/dist/src/evolve.js
CHANGED
|
@@ -78,46 +78,166 @@ function distance(hunk, units, line) {
|
|
|
78
78
|
const [from, to] = span(hunk, units);
|
|
79
79
|
return line < from ? from - line : line > to ? line - to : 0;
|
|
80
80
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
/** The body row whose tip line is `line`; a kept row before a removal at the same line. */
|
|
82
|
+
function rowAt(hunk, units, line) {
|
|
83
|
+
if (line === null)
|
|
84
|
+
return 0;
|
|
85
|
+
const lines = tipLines(hunk, units);
|
|
86
|
+
const row = hunk.body.findIndex((r, i) => r.op !== "-" && lines[i] === line);
|
|
87
|
+
return row === -1 ? 0 : row;
|
|
87
88
|
}
|
|
88
|
-
function
|
|
89
|
+
function pinnedComment(comment) {
|
|
89
90
|
const { anchor } = comment;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
// A comment on a whole hunk has no line; inventing one moves its card off the hunk's end.
|
|
92
|
+
const whole = anchor.kind === "hunk" && anchor.changes === null && anchor.offsets === undefined;
|
|
93
|
+
return { whole, row: (hunk) => firstRow(comment, hunk) };
|
|
94
|
+
}
|
|
95
|
+
function pinnedConcern(concern) {
|
|
96
|
+
const { lines } = concern;
|
|
97
|
+
if (lines === undefined)
|
|
98
|
+
return { whole: true, row: () => 0 };
|
|
99
|
+
return {
|
|
100
|
+
whole: false,
|
|
101
|
+
row: (hunk) => {
|
|
102
|
+
const row = hunk.body.findIndex((r) => r.op !== " " && lines.includes(r.change));
|
|
103
|
+
return row === -1 ? 0 : row;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Where a comment points on the version it was made on. */
|
|
108
|
+
function placedOwn(comment) {
|
|
109
|
+
const { anchor } = comment;
|
|
110
|
+
return {
|
|
111
|
+
unit: anchor.kind === "hunk" ? anchor.unit : null,
|
|
112
|
+
offsets: anchor.kind === "hunk" ? (anchor.offsets ?? null) : null,
|
|
113
|
+
changed: false,
|
|
114
|
+
path: comment.context.path,
|
|
115
|
+
newLine: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pure. Moves a placement on the old diff to the new one: a unit the new diff still has keeps its
|
|
120
|
+
* place, else the hunk of the same path nearest its old tip line takes it, marked changed. A
|
|
121
|
+
* placement on no unit stays as it is.
|
|
122
|
+
*/
|
|
123
|
+
function move(at, pinned, oldUnits, newUnits) {
|
|
124
|
+
if (at.unit === null)
|
|
125
|
+
return at;
|
|
126
|
+
const kept = newUnits.get(at.unit);
|
|
127
|
+
const old = oldUnits.get(at.unit);
|
|
94
128
|
if (kept !== undefined) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
129
|
+
let newLine = null;
|
|
130
|
+
if (kept.kind === "hunk" && !pinned.whole) {
|
|
131
|
+
if (!at.changed)
|
|
132
|
+
newLine = tipLines(kept, newUnits)[pinned.row(kept)] ?? null;
|
|
133
|
+
else if (at.newLine !== null && old?.kind === "hunk") {
|
|
134
|
+
newLine = at.newLine - tipStartOf(old, oldUnits) + tipStartOf(kept, newUnits);
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
newLine = at.newLine;
|
|
138
|
+
}
|
|
139
|
+
return { ...at, path: kept.path, newLine };
|
|
104
140
|
}
|
|
105
|
-
const
|
|
106
|
-
const path = old?.path ?? comment.context.path;
|
|
141
|
+
const path = old?.path ?? at.path;
|
|
107
142
|
const onPath = [...newUnits.values()].filter((u) => u.path === path);
|
|
108
143
|
const first = onPath[0];
|
|
144
|
+
const moved = { offsets: null, changed: true, path };
|
|
109
145
|
if (first === undefined)
|
|
110
|
-
return { ...
|
|
111
|
-
const moved = { comment, offsets: null, changed: true, path };
|
|
146
|
+
return { ...moved, unit: null, newLine: null };
|
|
112
147
|
const hunks = onPath.filter((u) => u.kind === "hunk");
|
|
113
148
|
if (old?.kind !== "hunk" || hunks.length === 0)
|
|
114
149
|
return { ...moved, unit: first.id, newLine: null };
|
|
115
|
-
const row =
|
|
150
|
+
const row = at.changed ? rowAt(old, oldUnits, at.newLine) : pinned.row(old);
|
|
116
151
|
const oldLine = tipLines(old, oldUnits)[row] ?? tipStartOf(old, oldUnits);
|
|
117
152
|
const nearest = hunks.reduce((a, b) => distance(b, newUnits, oldLine) < distance(a, newUnits, oldLine) ? b : a);
|
|
118
153
|
return {
|
|
119
154
|
...moved,
|
|
120
155
|
unit: nearest.id,
|
|
121
|
-
newLine: landing(nearest, newUnits, oldLine, old.body[row]?.text),
|
|
156
|
+
newLine: pinned.whole ? null : landing(nearest, newUnits, oldLine, old.body[row]?.text),
|
|
122
157
|
};
|
|
123
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Pure. Places each comment of the previous version on the new diff: on its own unit when that
|
|
161
|
+
* hunk is unchanged, else on the hunk of the same path nearest its old tip line.
|
|
162
|
+
*/
|
|
163
|
+
export function carryComments(comments, oldUnits, newUnits) {
|
|
164
|
+
return comments.map((comment) => ({
|
|
165
|
+
comment,
|
|
166
|
+
...move(placedOwn(comment), pinnedComment(comment), oldUnits, newUnits),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
/** since stays once set; a first change records the version the code still matched. */
|
|
170
|
+
function sinceOf(before, after, since, at) {
|
|
171
|
+
return since ?? (after.changed && !before.changed ? at : null);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Pure. What an open review carries from version `at` into the next: what it carried already,
|
|
175
|
+
* moved again, then the comments said on `at`. Nothing is dropped, so a comment of v1 is still
|
|
176
|
+
* there after the second swap.
|
|
177
|
+
*/
|
|
178
|
+
export function carrySession(carried, said, at, oldUnits, newUnits) {
|
|
179
|
+
const again = carried.map((c) => {
|
|
180
|
+
const moved = move(c, pinnedComment(c.comment), oldUnits, newUnits);
|
|
181
|
+
return { ...c, ...moved, since: sinceOf(c, moved, c.since, at) };
|
|
182
|
+
});
|
|
183
|
+
const fresh = said.map((comment) => {
|
|
184
|
+
const own = placedOwn(comment);
|
|
185
|
+
const moved = move(own, pinnedComment(comment), oldUnits, newUnits);
|
|
186
|
+
return { comment, version: at, ...moved, since: sinceOf(own, moved, null, at) };
|
|
187
|
+
});
|
|
188
|
+
return [...again, ...fresh];
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Pure. Kept concerns of version `at` that the next plan no longer has, plus those carried before,
|
|
192
|
+
* placed on the new diff. A concern the next plan has again keeps its decision there instead, so
|
|
193
|
+
* it is left out here.
|
|
194
|
+
*/
|
|
195
|
+
export function carryConcerns(carried, kept, inNext, at, oldUnits, newUnits) {
|
|
196
|
+
const fresh = kept.map((k) => {
|
|
197
|
+
const unit = k.concern.unit ?? null;
|
|
198
|
+
const path = unit === null ? null : (oldUnits.get(unit)?.path ?? null);
|
|
199
|
+
return { ...k, unit, offsets: null, changed: false, path, newLine: null, since: null };
|
|
200
|
+
});
|
|
201
|
+
return [...carried, ...fresh]
|
|
202
|
+
.filter((c) => !inNext.has(c.concern.id))
|
|
203
|
+
.map((c) => {
|
|
204
|
+
const moved = move(c, pinnedConcern(c.concern), oldUnits, newUnits);
|
|
205
|
+
return { ...c, ...moved, since: sinceOf(c, moved, c.since, at) };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Pure. Where a carried hunk comment points on the diff it was carried to, as an anchor of that
|
|
210
|
+
* diff: its own rows when the hunk is unchanged, the row at its new line when it changed, the
|
|
211
|
+
* whole hunk when it has no line. Null when the code is gone or the comment is not on a hunk.
|
|
212
|
+
*/
|
|
213
|
+
export function anchorOn(carried, units, commit) {
|
|
214
|
+
const { anchor } = carried.comment;
|
|
215
|
+
if (anchor.kind !== "hunk" || carried.unit === null)
|
|
216
|
+
return null;
|
|
217
|
+
if (!carried.changed)
|
|
218
|
+
return { ...anchor, commit };
|
|
219
|
+
const hunk = units.get(carried.unit);
|
|
220
|
+
if (hunk?.kind !== "hunk" || carried.newLine === null) {
|
|
221
|
+
return { kind: "hunk", unit: carried.unit, commit, changes: null };
|
|
222
|
+
}
|
|
223
|
+
const row = rowAt(hunk, units, carried.newLine);
|
|
224
|
+
const at = hunk.body[row];
|
|
225
|
+
const changes = at === undefined || at.op === " " ? [] : [at.change];
|
|
226
|
+
return { kind: "hunk", unit: carried.unit, commit, changes, offsets: [row] };
|
|
227
|
+
}
|
|
228
|
+
/** Pure. A carried concern as a concern of the diff it was carried to; see anchorOn. */
|
|
229
|
+
export function concernOn(carried, units) {
|
|
230
|
+
const { concern } = carried;
|
|
231
|
+
if (carried.unit === null)
|
|
232
|
+
return concern;
|
|
233
|
+
if (!carried.changed)
|
|
234
|
+
return { ...concern, unit: carried.unit };
|
|
235
|
+
const { lines: _old, ...rest } = concern;
|
|
236
|
+
const hunk = units.get(carried.unit);
|
|
237
|
+
if (hunk?.kind !== "hunk" || carried.newLine === null)
|
|
238
|
+
return { ...rest, unit: carried.unit };
|
|
239
|
+
const at = hunk.body[rowAt(hunk, units, carried.newLine)];
|
|
240
|
+
return at === undefined || at.op === " "
|
|
241
|
+
? { ...rest, unit: carried.unit }
|
|
242
|
+
: { ...rest, unit: carried.unit, lines: [at.change] };
|
|
243
|
+
}
|
package/dist/src/harness.js
CHANGED
|
@@ -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
|
-
|
|
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,11 +3,11 @@ 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
|
-
import { HEALTH_APP, isSessionId,
|
|
10
|
-
import { BranchMovedError, diffOfPlan, isAlive, openSessionAt, SessionFileError, SessionLockedError, } from "./session.js";
|
|
9
|
+
import { HEALTH_APP, isSessionId, loadDraft, PortInUseError, RegistrationError, startServer, } from "./server.js";
|
|
10
|
+
import { BranchMovedError, diffOfPlan, isAlive, openSessionAt, SessionFileError, SessionLockedError, sessionComments, } from "./session.js";
|
|
11
11
|
const RECORD = "server.json";
|
|
12
12
|
const DEFAULT_IDLE_MS = 30_000;
|
|
13
13
|
const START_TIMEOUT_MS = 10_000;
|
|
@@ -127,7 +127,15 @@ async function loadHosted(registration, held) {
|
|
|
127
127
|
throw new RegistrationError(`plan v${version} no longer fits ${branch}; check it again`);
|
|
128
128
|
}
|
|
129
129
|
const reviewsDir = session.reviewsDir();
|
|
130
|
-
const
|
|
130
|
+
const draft = await loadDraft(reviewsDir, version);
|
|
131
|
+
const { comments } = draft;
|
|
132
|
+
// A restart in the middle of a review that swapped: the draft names what was said before.
|
|
133
|
+
const earlier = draft.carried === null
|
|
134
|
+
? null
|
|
135
|
+
: {
|
|
136
|
+
comments: sessionComments(draft.carried, reviewed.filter((r) => r.version < version).map((r) => r.review)),
|
|
137
|
+
concerns: draft.carriedConcerns,
|
|
138
|
+
};
|
|
131
139
|
const prior = session.priorReview(mode, version);
|
|
132
140
|
const previous = prior === null || prior.comments.length === 0
|
|
133
141
|
? null
|
|
@@ -151,6 +159,7 @@ async function loadHosted(registration, held) {
|
|
|
151
159
|
release,
|
|
152
160
|
previous,
|
|
153
161
|
carried,
|
|
162
|
+
earlier,
|
|
154
163
|
state: {
|
|
155
164
|
mode,
|
|
156
165
|
anchor: session.anchor,
|
|
@@ -295,6 +304,22 @@ export async function fetchCarried(port, summary) {
|
|
|
295
304
|
return [carried];
|
|
296
305
|
});
|
|
297
306
|
}
|
|
307
|
+
/** The comments of the hosted review, and of the version before a swap, with their versions. */
|
|
308
|
+
export async function fetchComments(port, summary) {
|
|
309
|
+
const state = await call(port, "GET", `${summary.url}api/state`);
|
|
310
|
+
const parse = (raw) => (Array.isArray(raw) ? raw : []).flatMap((item) => {
|
|
311
|
+
const comment = parseComment(item);
|
|
312
|
+
return typeof comment === "string" ? [] : [comment];
|
|
313
|
+
});
|
|
314
|
+
if (!isFields(state))
|
|
315
|
+
throw new RegistrationError("the server sent no state");
|
|
316
|
+
const previous = state["previous"];
|
|
317
|
+
const out = [{ version: summary.version, comments: parse(state["comments"]) }];
|
|
318
|
+
if (isFields(previous) && isPlanVersion(previous["version"])) {
|
|
319
|
+
out.push({ version: previous["version"], comments: parse(previous["comments"]) });
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
298
323
|
/** One long poll of the session's event log: what happened after `since`, or nothing at timeout. */
|
|
299
324
|
export async function pollEvents(port, summary, since, timeoutMs) {
|
|
300
325
|
const path = `${summary.url}api/events?since=${since}&timeout=${timeoutMs}`;
|