@shiyuphan/follow-me 0.2.0 → 0.3.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.
- package/README.md +44 -55
- package/dist/src/cli.js +566 -188
- package/dist/src/comments.js +66 -5
- package/dist/src/git.js +12 -0
- package/dist/src/host.js +19 -8
- package/dist/src/install.js +21 -0
- package/dist/src/plan.js +1 -1
- package/dist/src/prompt.js +343 -39
- package/dist/src/server.js +176 -67
- package/dist/src/session.js +13 -11
- package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-DcmzRkVZ.js +1 -0
- package/dist/ui/dist/assets/arc-T5QB0T1C.js +1 -0
- package/dist/ui/dist/assets/architecture-WOLXFQ4H-DUIkQTeE.js +1 -0
- package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-BUYi4-bA.js +36 -0
- package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-dyJxtT9e.js +140 -0
- package/dist/ui/dist/assets/c4Diagram-XDPPPE54-CrAgiaZR.js +38 -0
- package/dist/ui/dist/assets/channel-wW7ebm0i.js +1 -0
- package/dist/ui/dist/assets/chunk-2E7WHVMD-C6fAf4Vj.js +88 -0
- package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/dist/assets/chunk-4EMNKR2T-BLZjwZ4Q.js +1 -0
- package/dist/ui/dist/assets/chunk-55XCU76K-DGLK2Q-L.js +1 -0
- package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/dist/assets/chunk-7S6XSTFO-BablszTX.js +2 -0
- package/dist/ui/dist/assets/chunk-AQDTSKZB-Cuk3B_vb.js +72 -0
- package/dist/ui/dist/assets/chunk-DSNLSHJZ-DiC78RqE.js +127 -0
- package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/ui/dist/assets/chunk-HAGFOTED-X_RbvixX.js +62 -0
- package/dist/ui/dist/assets/chunk-HNE4Q36P-C14_x5ck.js +1 -0
- package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/dist/ui/dist/assets/chunk-NQOKRVIF-Dxd0dtYR.js +213 -0
- package/dist/ui/dist/assets/chunk-UHW7PFBP-BIxePNUS.js +1 -0
- package/dist/ui/dist/assets/chunk-VQ5W62QI-CKSKLbj4.js +1 -0
- package/dist/ui/dist/assets/chunk-WQQL4CDK-CWNmZGlR.js +10 -0
- package/dist/ui/dist/assets/chunk-XXDRQBXY-D-I5QHgG.js +1 -0
- package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/dist/assets/chunk-YZO67HFG-DFP68CFd.js +1 -0
- package/dist/ui/dist/assets/chunk-ZAUX2LTX-DY90d4QG.js +1 -0
- package/dist/ui/dist/assets/chunk-ZCHMQSOR-Dlpr-5UC.js +2 -0
- package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-CH9uhoLJ.js +217 -0
- package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-DtYHrp8D.js +1 -0
- package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtslbtrY.js +1 -0
- package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUU1Jcc7.js +62 -0
- package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/ui/dist/assets/dagre-NK4GIZVX-CDlP-dRW.js +4 -0
- package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/dist/assets/diagram-CBK3MWTC-DiV2fn-r.js +24 -0
- package/dist/ui/dist/assets/diagram-I5XIIV2D-Bg5RehGi.js +24 -0
- package/dist/ui/dist/assets/diagram-ODIL2UCW-CFmazz3f.js +30 -0
- package/dist/ui/dist/assets/diagram-PFBEJUXE-uw3ZBqDp.js +3 -0
- package/dist/ui/dist/assets/diagram-RJA3I4VV-uEOYYRYa.js +41 -0
- package/dist/ui/dist/assets/diagram-TYUNFSNR-BN58LF_3.js +200 -0
- package/dist/ui/dist/assets/dist-tWS_lXDS.js +1 -0
- package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-B68Tz-w8.js +1 -0
- package/dist/ui/dist/assets/elk-QONTBCTQ-DZZohNea.js +27 -0
- package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BH_m74pv.js +99 -0
- package/dist/ui/dist/assets/eventmodeling-K75KTNOO-CTp5FHos.js +1 -0
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-CsCvexry.js +1 -0
- package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-BDau7_0f.js +292 -0
- package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/dist/assets/gitGraph-VSP46ZUC-Bv84IoCm.js +1 -0
- package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-DzXcMlm8.js +106 -0
- package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/dist/assets/index-BTJoqVVN.js +133 -0
- package/dist/ui/dist/assets/index-D60U3smc.css +1 -0
- package/dist/ui/dist/assets/info-OHQRW6UA-222J0O_j.js +1 -0
- package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-DE2sa3Vz.js +2 -0
- package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-BJDk0Zcj.js +70 -0
- package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-JSUXX5Hb.js +139 -0
- package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-Bg79b49M.js +89 -0
- package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/ui/dist/assets/line-Bk5qQSIa.js +1 -0
- package/dist/ui/dist/assets/linear-PFnVMru5.js +1 -0
- package/dist/ui/dist/assets/mermaid-parser.core-Dcxlzc-S.js +7 -0
- package/dist/ui/dist/assets/mermaid.core-TeOHQJ00.js +44 -0
- package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-IaNk8NnW.js +98 -0
- package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/dist/assets/packet-JDAUHWVQ-DMff5cVK.js +1 -0
- package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-66Xu4YRc.js +1 -0
- package/dist/ui/dist/assets/pie-XZMESJXO-GEtzLCIM.js +1 -0
- package/dist/ui/dist/assets/pieDiagram-GNMPY74T-7jMBi-kU.js +39 -0
- package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
- package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
- package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
- package/dist/ui/dist/assets/prism-core-FDHlWQ43.js +1 -0
- package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
- package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
- package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
- package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
- package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
- package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
- package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
- package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
- package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
- package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
- package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
- package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
- package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
- package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
- package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
- package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
- package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
- package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
- package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
- package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
- package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
- package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-8hIWayC2.js +7 -0
- package/dist/ui/dist/assets/radar-ABXABTNO-BTdwUOOF.js +1 -0
- package/dist/ui/dist/assets/railroad-I3PHUGI6-DcMbDqBL.js +1 -0
- package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-gTzUTWeI.js +1 -0
- package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-CNtdUPtk.js +1 -0
- package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-DOiExAtE.js +1 -0
- package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-2kE_ygvM.js +1 -0
- package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-CoIAHRq2.js +84 -0
- package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-7p8D3wim.js +40 -0
- package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-C6QFnMvn.js +169 -0
- package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/ui/dist/assets/src-FgijoBVs.js +1 -0
- package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-n9eAb_bT.js +291 -0
- package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-U18mtUzX.js +1 -0
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DX07QVy9.js +8 -0
- package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-CD2Z8QGP.js +120 -0
- package/dist/ui/dist/assets/treeView-D4JQ5SDB-sqUjH5gB.js +1 -0
- package/dist/ui/dist/assets/treemap-SAJKECNS-DvZ38FP7.js +1 -0
- package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-CqOGidaZ.js +328 -0
- package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D1SXSBQ5.js +34 -0
- package/dist/ui/dist/assets/wardley-7MLQ67FV-Bpwio-Dx.js +1 -0
- package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-JSUXHMnk.js +78 -0
- package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-B4IFqQTN.js +7 -0
- package/dist/ui/dist/index.html +15 -0
- package/dist/ui/dist/style.css +121 -0
- package/package.json +31 -10
- package/skills/follow-me/SKILL.md +15 -89
- package/dist/src/anthropic.js +0 -133
- package/dist/ui/app.js +0 -2201
- package/dist/ui/concerns.js +0 -165
- package/dist/ui/highlight.js +0 -221
- package/dist/ui/index.html +0 -94
- package/dist/ui/live.js +0 -184
- package/dist/ui/playback.js +0 -579
- package/dist/ui/questions.js +0 -189
- package/dist/ui/style.css +0 -1331
- package/dist/ui/summary.js +0 -45
package/dist/src/cli.js
CHANGED
|
@@ -1,35 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join, relative } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { runInstall, runUninstall } from "./install.js";
|
|
6
|
+
import { DiffParseError, formatUnitRef, isUnitId } from "./diff.js";
|
|
7
|
+
import { BranchRefusedError, branchHead, GitError, isOwnWork, signing, treeOf, writeBranch, } from "./git.js";
|
|
8
|
+
import { fetchReport, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postReply, probeHealth, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
|
|
9
|
+
import { runInstall, runUninstall, syncSkills } from "./install.js";
|
|
10
10
|
import { naiveDraft } from "./naive.js";
|
|
11
11
|
import { serializePlanFile, validatePlan } from "./plan.js";
|
|
12
|
-
import { renderEvents, renderPrompt, renderReport } from "./prompt.js";
|
|
12
|
+
import { renderEvents, renderPrompt, renderReport, reportItems, } from "./prompt.js";
|
|
13
13
|
import { RegistrationError, sessionIdOf } from "./server.js";
|
|
14
14
|
import { BranchMovedError, cleanSessions, extractSession, MissingDraftError, NotARepositoryError, openSession, SessionFileError, SessionLockedError, } from "./session.js";
|
|
15
15
|
import { checkSyntax } from "./syntax.js";
|
|
16
16
|
class UsageError extends Error {
|
|
17
17
|
name = "UsageError";
|
|
18
18
|
}
|
|
19
|
+
const SHORT_USAGE = `usage: follow-me [--wait <seconds>] [--branch <name>]
|
|
20
|
+
|
|
21
|
+
Run follow-me with no command. Each call does the next thing that is missing and returns: it
|
|
22
|
+
writes the prompt for you to answer, reports what is wrong with your draft, opens the review for
|
|
23
|
+
the user, then listens to them for up to --wait seconds (default 60, 0 for none) and prints what
|
|
24
|
+
they flagged or asked. Do what its "next:" line says. Run it again to keep listening.
|
|
25
|
+
|
|
26
|
+
Other commands:
|
|
27
|
+
reply <commentId> --body <text> [--diagram <file>]
|
|
28
|
+
answer under a comment on the page; the diagram is
|
|
29
|
+
Mermaid source
|
|
30
|
+
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
31
|
+
start a thread on the page without being asked
|
|
32
|
+
report [--json] [--markdown] print the report of the latest review; --json lists
|
|
33
|
+
the items to post to a merge request
|
|
34
|
+
|
|
35
|
+
help --all lists every command.`;
|
|
19
36
|
const USAGE = `usage: follow-me <command> [options]
|
|
20
37
|
|
|
38
|
+
follow-me with no command is the entry point; see \`follow-me help\`. The commands below are what it
|
|
39
|
+
runs, for when you want one step by hand.
|
|
40
|
+
|
|
21
41
|
Split your own branch:
|
|
22
|
-
split [--base <ref>] [--branch <name>] [--
|
|
23
|
-
extract,
|
|
24
|
-
|
|
42
|
+
split [--base <ref>] [--branch <name>] [--no-open]
|
|
43
|
+
extract, apply your agent's draft and open the
|
|
44
|
+
review in one go; without a valid draft it writes
|
|
45
|
+
the prompt for your agent and stops
|
|
25
46
|
extract [--base <ref>] [--branch <name>] anchor a session and list the units
|
|
26
47
|
prompt [--walk] [--branch <name>] print the planner prompt; your coding agent
|
|
27
48
|
writes draft.json from it
|
|
28
|
-
plan
|
|
29
|
-
|
|
30
|
-
Anthropic API
|
|
31
|
-
plan --naive [--branch <name>] no API, no agent: one "wip: <path>" commit per
|
|
32
|
-
touched file, in diff order
|
|
49
|
+
plan --naive [--branch <name>] no agent: one "wip: <path>" commit per touched
|
|
50
|
+
file, in diff order
|
|
33
51
|
check [path] [--branch <name>] [--syntax "<cmd {file}>"]
|
|
34
52
|
validate draft.json or the given plan file, and
|
|
35
53
|
check that every file each commit writes parses;
|
|
@@ -40,11 +58,11 @@ Split your own branch:
|
|
|
40
58
|
--name; never on <branch> or a checked-out branch;
|
|
41
59
|
refuses a commit with a file that does not parse
|
|
42
60
|
unless --allow-syntax-errors
|
|
43
|
-
review [--
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
61
|
+
review [--base <ref>] [--branch <name>] [--port <n>] [--detach] [--no-open]
|
|
62
|
+
[--wait [--timeout <seconds>]] open the review, commit by commit; --wait returns
|
|
63
|
+
only once the review ends. After apply it reviews
|
|
64
|
+
your split; otherwise it plans for a reader and
|
|
65
|
+
reviews the code, applying nothing
|
|
48
66
|
status show the session's plans, reviews, lock and the
|
|
49
67
|
branch apply writes
|
|
50
68
|
|
|
@@ -58,15 +76,22 @@ Interactive loop (while the review page is open):
|
|
|
58
76
|
reply <commentId> --body <text> [--diagram <file>]
|
|
59
77
|
answer under a comment on the page; the diagram is
|
|
60
78
|
Mermaid source
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]
|
|
80
|
+
start a thread on the page without being asked;
|
|
81
|
+
--lines are change lines of the unit, commits count
|
|
82
|
+
from 1; with no target it is about the whole plan.
|
|
83
|
+
watch leaves these out like your replies
|
|
84
|
+
report [--json] [--markdown] [--branch <name>]
|
|
85
|
+
print the report of the open review, or of the last
|
|
86
|
+
one that ended. Markdown lists issues by file and
|
|
87
|
+
line in both voices, then questions with answers;
|
|
88
|
+
--json lists the items to post to a merge request
|
|
89
|
+
(path, lines, severity, body, thread, excluded)
|
|
64
90
|
apply, while the page is open, swaps it to the new plan version in place, same URL.
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
walk
|
|
68
|
-
|
|
69
|
-
review its code; nothing is applied
|
|
92
|
+
Old names, still working:
|
|
93
|
+
walk is review; prints one line saying so
|
|
94
|
+
feedback [--walk] is report; prints one line saying so
|
|
70
95
|
|
|
71
96
|
Review server:
|
|
72
97
|
server status show the review server and the sessions it hosts
|
|
@@ -78,14 +103,17 @@ Setup:
|
|
|
78
103
|
install --list show each harness, its skill dirs and docs
|
|
79
104
|
uninstall [--harness <ids>|--all] [--project] [--dry-run]
|
|
80
105
|
remove the skill files install writes
|
|
106
|
+
upgrade [--harness <ids>|--all] [--project] [--dry-run]
|
|
107
|
+
update the package with npm, then refresh the
|
|
108
|
+
installed skill copies
|
|
81
109
|
clean [--all] [--dry-run] remove sessions of deleted branches, finished
|
|
82
110
|
review drafts and leftovers of dead processes;
|
|
83
111
|
--all removes .follow-me/ entirely
|
|
84
112
|
--version print the version
|
|
85
113
|
help print this
|
|
86
114
|
|
|
87
|
-
--walk writes the prompt for a reviewer: a story with
|
|
88
|
-
review
|
|
115
|
+
--walk writes the prompt for a reviewer: a story with notes instead of a split.
|
|
116
|
+
review shares one server per user: a running one hosts the session and the command
|
|
89
117
|
prints its URL; otherwise the command serves until its review ends, or with --detach starts the
|
|
90
118
|
server in the background. --port taken by another program falls back to a free port.
|
|
91
119
|
--force lets apply move a branch follow-me did not create; it never allows <branch> itself.
|
|
@@ -462,36 +490,27 @@ function promptInput(session, mode, args) {
|
|
|
462
490
|
};
|
|
463
491
|
if (mode === "walk")
|
|
464
492
|
return { ...common, mode };
|
|
493
|
+
const previous = session.plans().at(-1);
|
|
465
494
|
return {
|
|
466
495
|
...common,
|
|
467
496
|
mode,
|
|
468
|
-
previousPlan:
|
|
497
|
+
previousPlan: previous?.file ?? null,
|
|
469
498
|
review: session.lastReview(),
|
|
499
|
+
previousCommits: previous === undefined ? [] : projectedCommits(session, previous.version),
|
|
470
500
|
};
|
|
471
501
|
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
session.writeDraft(draft);
|
|
480
|
-
const verdict = validate(session, draft);
|
|
481
|
-
if (verdict.ok) {
|
|
482
|
-
say(`wrote ${relative(cwd, session.draftPath)} (${plural(verdict.plan.commits.length, "commit")})`);
|
|
483
|
-
return verdict.plan;
|
|
484
|
-
}
|
|
485
|
-
const issues = verdict.issues.map((issue) => `- ${formatIssue(issue)}`).join("\n");
|
|
486
|
-
prompt = `${prompt}\n\n## Your previous draft had these issues, write a corrected plan\n\n${issues}\n`;
|
|
487
|
-
}
|
|
488
|
-
say(`wrote ${relative(cwd, session.draftPath)} but it still has issues, repair it and run \`follow-me check${branchFlag(args)}\``);
|
|
489
|
-
return null;
|
|
502
|
+
/** The commits of a frozen plan as projected now, or none when it no longer fits the branch. */
|
|
503
|
+
function projectedCommits(session, version) {
|
|
504
|
+
const plan = session.plans().find((p) => p.version === version);
|
|
505
|
+
if (plan === undefined)
|
|
506
|
+
return [];
|
|
507
|
+
const verdict = validatePlan(session.diff, serializePlanFile(plan.file));
|
|
508
|
+
return verdict.ok ? verdict.plan.commits : [];
|
|
490
509
|
}
|
|
491
|
-
/** Writes the one-commit-per-file draft with no
|
|
510
|
+
/** Writes the one-commit-per-file draft with no agent. */
|
|
492
511
|
function naive(session, cwd, args) {
|
|
493
|
-
if (args.flags.has("walk")
|
|
494
|
-
throw new UsageError("--naive writes no story, so --walk
|
|
512
|
+
if (args.flags.has("walk")) {
|
|
513
|
+
throw new UsageError("--naive writes no story, so --walk does not apply");
|
|
495
514
|
}
|
|
496
515
|
const draft = naiveDraft(session.diff);
|
|
497
516
|
session.writeDraft(draft);
|
|
@@ -503,14 +522,11 @@ function naive(session, cwd, args) {
|
|
|
503
522
|
return 0;
|
|
504
523
|
}
|
|
505
524
|
async function plan(cwd, args) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (args.flags.has("naive"))
|
|
509
|
-
return naive(session, cwd, args);
|
|
510
|
-
if ((await draftWithApi(session, cwd, mode, args)) === null)
|
|
525
|
+
if (!args.flags.has("naive")) {
|
|
526
|
+
console.error("follow-me plan: there is no built-in model planner; your coding agent writes the draft from `follow-me prompt`, or use --naive for one commit per file");
|
|
511
527
|
return 1;
|
|
512
|
-
|
|
513
|
-
return
|
|
528
|
+
}
|
|
529
|
+
return naive(await openSession(cwd, sessionOpts(args)), cwd, args);
|
|
514
530
|
}
|
|
515
531
|
/** The latest frozen plan, or the draft frozen now when it validates. */
|
|
516
532
|
function latestPlan(session) {
|
|
@@ -524,8 +540,8 @@ function latestPlan(session) {
|
|
|
524
540
|
return null;
|
|
525
541
|
return { version: session.freeze(verdict.plan), plan: verdict.plan };
|
|
526
542
|
}
|
|
527
|
-
function announce(
|
|
528
|
-
say(`follow-me
|
|
543
|
+
function announce(url, args) {
|
|
544
|
+
say(`follow-me review: ${url}`);
|
|
529
545
|
if (!args.flags.has("no-open"))
|
|
530
546
|
openBrowser(url);
|
|
531
547
|
}
|
|
@@ -548,10 +564,10 @@ async function hostReview(session, mode, args, prepare, report) {
|
|
|
548
564
|
const home = stateDir();
|
|
549
565
|
const open = await openOnServer(session, mode);
|
|
550
566
|
if (open !== null) {
|
|
551
|
-
announce(
|
|
567
|
+
announce(sessionUrl(open.port, open.summary), args);
|
|
552
568
|
return { kind: "elsewhere", ...open };
|
|
553
569
|
}
|
|
554
|
-
return locked(session,
|
|
570
|
+
return locked(session, "review", async (held) => {
|
|
555
571
|
const prepared = await prepare();
|
|
556
572
|
if (prepared === null)
|
|
557
573
|
return { kind: "refused" };
|
|
@@ -577,12 +593,12 @@ async function hostReview(session, mode, args, prepare, report) {
|
|
|
577
593
|
started = await startOrAttach(home, port, say);
|
|
578
594
|
if (started.kind === "attached") {
|
|
579
595
|
const summary = await registerWith(started.port, registration);
|
|
580
|
-
announce(
|
|
596
|
+
announce(sessionUrl(started.port, summary), args);
|
|
581
597
|
return { kind: "elsewhere", port: started.port, summary };
|
|
582
598
|
}
|
|
583
599
|
const { host } = started;
|
|
584
600
|
const { summary, done } = await host.register(registration);
|
|
585
|
-
announce(
|
|
601
|
+
announce(sessionUrl(host.port, summary), args);
|
|
586
602
|
const review = await done;
|
|
587
603
|
if (review === null)
|
|
588
604
|
return { kind: "stopped" };
|
|
@@ -601,13 +617,10 @@ function exitCode(hosted) {
|
|
|
601
617
|
}
|
|
602
618
|
return hosted.kind === "refused" || hosted.kind === "stopped" ? 1 : 0;
|
|
603
619
|
}
|
|
604
|
-
function alreadyReviewed(session,
|
|
605
|
-
if (!reviewsOf(session
|
|
620
|
+
function alreadyReviewed(session, version, args) {
|
|
621
|
+
if (!reviewsOf(session).some((r) => r.version === version))
|
|
606
622
|
return false;
|
|
607
|
-
|
|
608
|
-
console.error(mode === "walk"
|
|
609
|
-
? `follow-me: plan v${version} already has a code review; read it with \`follow-me feedback --walk${branch}\``
|
|
610
|
-
: `follow-me: plan v${version} already has a review; read it with \`follow-me feedback${branch}\`, then run \`follow-me split${branch}\` for the next plan`);
|
|
623
|
+
console.error(`follow-me: plan v${version} already has a review; read it with \`follow-me report${branchFlag(args)}\``);
|
|
611
624
|
return true;
|
|
612
625
|
}
|
|
613
626
|
/** This session's review, of this mode or any, on the user's server when that holds its lock. */
|
|
@@ -630,13 +643,15 @@ function timeoutOf(args) {
|
|
|
630
643
|
}
|
|
631
644
|
return seconds;
|
|
632
645
|
}
|
|
633
|
-
/**
|
|
634
|
-
function reviewsOf(session
|
|
635
|
-
|
|
646
|
+
/** Every finished review, oldest first, including reviewer reviews older builds stored apart. */
|
|
647
|
+
function reviewsOf(session) {
|
|
648
|
+
const seen = new Set();
|
|
649
|
+
return [...session.reviews(), ...session.walkReviews()]
|
|
650
|
+
.filter((r) => !seen.has(r.version) && seen.add(r.version))
|
|
651
|
+
.sort((a, b) => a.version - b.version);
|
|
636
652
|
}
|
|
637
|
-
function savedReview(session,
|
|
638
|
-
|
|
639
|
-
return stored?.review ?? null;
|
|
653
|
+
function savedReview(session, version) {
|
|
654
|
+
return reviewsOf(session).find((r) => r.version === version)?.review ?? null;
|
|
640
655
|
}
|
|
641
656
|
/**
|
|
642
657
|
* Polls the server until it no longer hosts the session, then reads the review it saved. Null
|
|
@@ -649,31 +664,47 @@ async function waitForEnd(session, hosted, timeout) {
|
|
|
649
664
|
return "timeout";
|
|
650
665
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
651
666
|
}
|
|
652
|
-
return savedReview(session, hosted.summary.
|
|
667
|
+
return savedReview(session, hosted.summary.version);
|
|
653
668
|
}
|
|
654
|
-
function
|
|
669
|
+
function reportSaved(review) {
|
|
655
670
|
say(`review v${review.planVersion} saved with ${plural(review.comments.length, "comment")}`);
|
|
656
|
-
next("follow-me
|
|
671
|
+
next("follow-me report");
|
|
657
672
|
}
|
|
658
673
|
function prepareSplit(session, args) {
|
|
659
674
|
return async () => {
|
|
660
675
|
const latest = latestPlan(session);
|
|
661
|
-
if (latest === null || alreadyReviewed(session,
|
|
676
|
+
if (latest === null || alreadyReviewed(session, latest.version, args))
|
|
662
677
|
return null;
|
|
663
678
|
return latest;
|
|
664
679
|
};
|
|
665
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Author mode when a split branch was applied for the latest plan: the plan is the author's own
|
|
683
|
+
* and the review is of it. Otherwise the reader is a reviewer meeting the branch, so the plan only
|
|
684
|
+
* orders the reading and nothing is applied. The wire keeps calling that mode "walk".
|
|
685
|
+
*/
|
|
686
|
+
function reviewMode(session) {
|
|
687
|
+
const applied = session.applied();
|
|
688
|
+
const latest = session.plans().at(-1);
|
|
689
|
+
return applied !== null && latest !== undefined && applied.version === latest.version
|
|
690
|
+
? "split"
|
|
691
|
+
: "walk";
|
|
692
|
+
}
|
|
693
|
+
/** Hosts the review of the session: the plan for its author, or the code for a reviewer. */
|
|
666
694
|
async function review(cwd, args) {
|
|
667
|
-
const session = await
|
|
695
|
+
const session = await openFresh(cwd, args);
|
|
668
696
|
const timeout = timeoutOf(args);
|
|
669
697
|
if (timeout !== undefined && !args.flags.has("wait")) {
|
|
670
698
|
throw new UsageError("--timeout only applies with --wait");
|
|
671
699
|
}
|
|
672
|
-
const
|
|
700
|
+
const open = await openOnServer(session, null);
|
|
701
|
+
const mode = open?.summary.mode ?? reviewMode(session);
|
|
702
|
+
const prepare = mode === "split" ? prepareSplit(session, args) : () => walkPlan(session, args);
|
|
703
|
+
const hosted = await hostReview(session, mode, args, prepare, reportSaved);
|
|
673
704
|
if (hosted.kind !== "elsewhere")
|
|
674
705
|
return exitCode(hosted);
|
|
675
706
|
if (!args.flags.has("wait")) {
|
|
676
|
-
next("end the review on the page, then follow-me
|
|
707
|
+
next("end the review on the page, then follow-me report");
|
|
677
708
|
return 0;
|
|
678
709
|
}
|
|
679
710
|
const ended = await waitForEnd(session, hosted, timeout);
|
|
@@ -683,7 +714,7 @@ async function review(cwd, args) {
|
|
|
683
714
|
}
|
|
684
715
|
if (ended === null)
|
|
685
716
|
return exitCode({ kind: "stopped" });
|
|
686
|
-
|
|
717
|
+
reportSaved(ended);
|
|
687
718
|
return 0;
|
|
688
719
|
}
|
|
689
720
|
/** Why split needs a new draft before it can go on, or null when draft.json is ready. */
|
|
@@ -721,49 +752,255 @@ function draftNeed(session, cwd, args) {
|
|
|
721
752
|
function handOff(session, cwd, args, why) {
|
|
722
753
|
session.writePrompt(renderPrompt(promptInput(session, "split", args)));
|
|
723
754
|
const prompt = relative(cwd, session.promptPath);
|
|
724
|
-
say(`${why}
|
|
755
|
+
say(`${why}.`);
|
|
725
756
|
say(`Have your coding agent follow ${prompt} and write ${relative(cwd, session.draftPath)}, then run:`);
|
|
726
757
|
say(` follow-me check${branchFlag(args)}`);
|
|
727
758
|
say(` follow-me split${branchFlag(args)}`);
|
|
728
759
|
next(`hand ${prompt} to your agent`);
|
|
729
760
|
}
|
|
730
761
|
/**
|
|
731
|
-
* The author's one command: extract,
|
|
732
|
-
*
|
|
762
|
+
* The author's one command: extract, apply the agent's draft, and open the review on the user's
|
|
763
|
+
* server. It stops with a handoff whenever there is no valid draft.
|
|
733
764
|
*/
|
|
734
765
|
async function split(cwd, args) {
|
|
735
766
|
const session = await openFresh(cwd, args);
|
|
736
767
|
say(headline(session));
|
|
737
768
|
const open = await openOnServer(session, "split");
|
|
738
769
|
if (open !== null) {
|
|
739
|
-
announce(
|
|
740
|
-
next("end the review on the page, then follow-me
|
|
770
|
+
announce(sessionUrl(open.port, open.summary), args);
|
|
771
|
+
next("end the review on the page, then follow-me report");
|
|
741
772
|
return 0;
|
|
742
773
|
}
|
|
743
774
|
const need = draftNeed(session, cwd, args);
|
|
744
775
|
if (need === "invalid")
|
|
745
776
|
return 1;
|
|
746
777
|
if (need !== null) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
return 1;
|
|
750
|
-
}
|
|
751
|
-
say(`${need}; asking the Anthropic API for one`);
|
|
752
|
-
const drafted = await draftWithApi(session, cwd, "split", args);
|
|
753
|
-
if (drafted === null)
|
|
754
|
-
return 1;
|
|
755
|
-
listPlan(drafted);
|
|
778
|
+
handOff(session, cwd, args, need);
|
|
779
|
+
return 1;
|
|
756
780
|
}
|
|
757
781
|
const applied = await locked(session, "apply", () => applyLocked(session, args));
|
|
758
782
|
if (applied !== 0)
|
|
759
783
|
return applied;
|
|
760
784
|
const detached = { ...args, flags: new Set([...args.flags, "detach"]) };
|
|
761
|
-
const hosted = await hostReview(session, "split", detached, prepareSplit(session, args),
|
|
785
|
+
const hosted = await hostReview(session, "split", detached, prepareSplit(session, args), reportSaved);
|
|
762
786
|
if (hosted.kind !== "elsewhere")
|
|
763
787
|
return exitCode(hosted);
|
|
764
|
-
next("end the review on the page, then follow-me
|
|
788
|
+
next("end the review on the page, then follow-me report");
|
|
789
|
+
return 0;
|
|
790
|
+
}
|
|
791
|
+
/** The mode a bare `follow-me` works in: a split already applied, else whoever wrote the branch. */
|
|
792
|
+
async function entryMode(session, args) {
|
|
793
|
+
if (args.flags.has("author") && args.flags.has("reviewer")) {
|
|
794
|
+
throw new UsageError("--author and --reviewer cannot be used together");
|
|
795
|
+
}
|
|
796
|
+
if (args.flags.has("reviewer"))
|
|
797
|
+
return "walk";
|
|
798
|
+
if (args.flags.has("author"))
|
|
799
|
+
return "split";
|
|
800
|
+
if (reviewMode(session) === "split")
|
|
801
|
+
return "split";
|
|
802
|
+
return (await isOwnWork(session.root, session.anchor.base, session.anchor.tip))
|
|
803
|
+
? "split"
|
|
804
|
+
: "walk";
|
|
805
|
+
}
|
|
806
|
+
function draftState(session) {
|
|
807
|
+
let draft;
|
|
808
|
+
try {
|
|
809
|
+
draft = session.readDraft();
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (error instanceof MissingDraftError)
|
|
813
|
+
return { kind: "missing" };
|
|
814
|
+
throw error;
|
|
815
|
+
}
|
|
816
|
+
const verdict = validatePlan(session.diff, draft, { lastReview: session.lastReview() });
|
|
817
|
+
if (verdict.ok)
|
|
818
|
+
return { kind: "valid", prepared: verdict.plan };
|
|
819
|
+
if (verdict.issues.some((issue) => issue.kind === "stale"))
|
|
820
|
+
return { kind: "stale" };
|
|
821
|
+
for (const issue of verdict.issues)
|
|
822
|
+
say(formatIssue(issue));
|
|
823
|
+
return { kind: "invalid" };
|
|
824
|
+
}
|
|
825
|
+
/** The newest review of a plan that still fits the branch, or null. */
|
|
826
|
+
function reviewedPlan(session) {
|
|
827
|
+
const reviews = reviewsOf(session);
|
|
828
|
+
for (const stored of reviews.toReversed()) {
|
|
829
|
+
const plan = session.plans().find((p) => p.version === stored.version);
|
|
830
|
+
if (plan !== undefined && validatePlan(session.diff, serializePlanFile(plan.file)).ok) {
|
|
831
|
+
return stored;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
/** Prints the finished review's report, which is what the agent works from from here on. */
|
|
837
|
+
function finished(session, stored) {
|
|
838
|
+
const text = renderStored(session, stored);
|
|
839
|
+
if (text === null)
|
|
840
|
+
return 1;
|
|
841
|
+
say(`review v${stored.version} ended with ${plural(stored.review.comments.length, "comment")}`);
|
|
842
|
+
process.stdout.write(text);
|
|
843
|
+
say();
|
|
844
|
+
next("done; the user can port it with follow-me report --json");
|
|
845
|
+
return 0;
|
|
846
|
+
}
|
|
847
|
+
/** Writes the prompt for the agent and says where; used whenever there is no plan to review. */
|
|
848
|
+
function promptHandoff(session, cwd, mode, args) {
|
|
849
|
+
session.writePrompt(renderPrompt(promptInput(session, mode, args)));
|
|
850
|
+
const prompt = relative(cwd, session.promptPath);
|
|
851
|
+
const draft = relative(cwd, session.draftPath);
|
|
852
|
+
say(`wrote ${prompt}`);
|
|
853
|
+
next(`write ${draft} from ${prompt}, then run follow-me again`);
|
|
765
854
|
return 0;
|
|
766
855
|
}
|
|
856
|
+
function cursorPath(session) {
|
|
857
|
+
return join(session.dir, "cursor.json");
|
|
858
|
+
}
|
|
859
|
+
/** Where the last `follow-me` stopped reading this hosted session's events; 0 for a new one. */
|
|
860
|
+
function readCursor(session, summary) {
|
|
861
|
+
let raw;
|
|
862
|
+
try {
|
|
863
|
+
raw = JSON.parse(readFileSync(cursorPath(session), "utf8"));
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
return 0;
|
|
867
|
+
}
|
|
868
|
+
if (typeof raw !== "object" || raw === null)
|
|
869
|
+
return 0;
|
|
870
|
+
const seen = "session" in raw ? raw.session : undefined;
|
|
871
|
+
const seq = "seq" in raw ? raw.seq : undefined;
|
|
872
|
+
const same = seen === `${summary.id}@${summary.openedAt}`;
|
|
873
|
+
return same && typeof seq === "number" && Number.isInteger(seq) && seq >= 0 ? seq : 0;
|
|
874
|
+
}
|
|
875
|
+
function writeCursor(session, summary, seq) {
|
|
876
|
+
const cursor = { session: `${summary.id}@${summary.openedAt}`, seq };
|
|
877
|
+
writeFileSync(cursorPath(session), `${JSON.stringify(cursor)}\n`);
|
|
878
|
+
}
|
|
879
|
+
function waitOf(args) {
|
|
880
|
+
const raw = args.values.get("wait");
|
|
881
|
+
if (raw === undefined)
|
|
882
|
+
return ENTRY_WAIT_SECONDS;
|
|
883
|
+
const seconds = Number(raw);
|
|
884
|
+
if (!Number.isFinite(seconds) || seconds < 0) {
|
|
885
|
+
throw new UsageError(`--wait ${raw} is not a number of seconds`);
|
|
886
|
+
}
|
|
887
|
+
return seconds;
|
|
888
|
+
}
|
|
889
|
+
const ENTRY_WAIT_SECONDS = 60;
|
|
890
|
+
/**
|
|
891
|
+
* Listens to the hosted review for up to --wait seconds and prints what the reviewer did. It
|
|
892
|
+
* returns at once with news, and with the report when the reviewer submitted or ended the review.
|
|
893
|
+
*/
|
|
894
|
+
async function listen(session, open, args) {
|
|
895
|
+
const { port, summary } = open;
|
|
896
|
+
const waitMs = waitOf(args) * 1000;
|
|
897
|
+
const deadline = Date.now() + waitMs;
|
|
898
|
+
let since = readCursor(session, summary);
|
|
899
|
+
let version = summary.version;
|
|
900
|
+
for (;;) {
|
|
901
|
+
const left = Math.max(0, deadline - Date.now());
|
|
902
|
+
let batch;
|
|
903
|
+
try {
|
|
904
|
+
batch = await pollEvents(port, summary, since, Math.min(left, POLL_MS));
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
if (!(error instanceof RegistrationError))
|
|
908
|
+
throw error;
|
|
909
|
+
// The log restarted under us (a new server); read it from the start.
|
|
910
|
+
since = 0;
|
|
911
|
+
writeCursor(session, summary, 0);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
since = batch.seq;
|
|
915
|
+
writeCursor(session, summary, since);
|
|
916
|
+
for (const event of batch.events)
|
|
917
|
+
if (event.type === "version")
|
|
918
|
+
version = event.version;
|
|
919
|
+
const news = batch.events.filter(isNews);
|
|
920
|
+
if (news.length > 0) {
|
|
921
|
+
const ctx = { commits: projectedCommits(session, version), units: session.diff.units };
|
|
922
|
+
process.stdout.write(renderEvents(news, ctx));
|
|
923
|
+
const ended = news.some((e) => e.type === "ended");
|
|
924
|
+
if (ended || news.some((e) => e.type === "submitted")) {
|
|
925
|
+
say();
|
|
926
|
+
if (ended) {
|
|
927
|
+
const stored = reviewsOf(session).find((r) => r.version === version);
|
|
928
|
+
if (stored !== undefined)
|
|
929
|
+
return finished(session, stored);
|
|
930
|
+
}
|
|
931
|
+
process.stdout.write(await fetchReport(port, summary));
|
|
932
|
+
say();
|
|
933
|
+
next("done; the user can port it with follow-me report --json");
|
|
934
|
+
return 0;
|
|
935
|
+
}
|
|
936
|
+
next('answer with follow-me reply <id> --body "<markdown>" [--diagram file], or run follow-me again to keep listening');
|
|
937
|
+
return 0;
|
|
938
|
+
}
|
|
939
|
+
if (left <= 0) {
|
|
940
|
+
say(`nothing new on the review page in ${Math.round(waitMs / 1000)}s`);
|
|
941
|
+
next("run follow-me again to keep listening");
|
|
942
|
+
return 0;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* The agent's one command. Each call does the next thing that is missing and returns: it writes
|
|
948
|
+
* the prompt, reports a bad draft, applies and opens the review, then listens to the reviewer
|
|
949
|
+
* and hands back the report once the review is submitted.
|
|
950
|
+
*/
|
|
951
|
+
async function entry(cwd, args) {
|
|
952
|
+
syncSkills(homedir(), say);
|
|
953
|
+
const session = await openFresh(cwd, args);
|
|
954
|
+
const hosted = await openOnServer(session, null);
|
|
955
|
+
if (hosted !== null)
|
|
956
|
+
return listen(session, hosted, args);
|
|
957
|
+
const mode = await entryMode(session, args);
|
|
958
|
+
const state = draftState(session);
|
|
959
|
+
if (state.kind === "invalid") {
|
|
960
|
+
next("fix the draft, then run follow-me again");
|
|
961
|
+
return 1;
|
|
962
|
+
}
|
|
963
|
+
const done = reviewedPlan(session);
|
|
964
|
+
if (state.kind === "valid") {
|
|
965
|
+
const reviewed = reviewsOf(session).find((r) => session.plans().find((p) => p.version === r.version)?.hash === state.prepared.hash);
|
|
966
|
+
if (reviewed !== undefined)
|
|
967
|
+
return finished(session, reviewed);
|
|
968
|
+
}
|
|
969
|
+
else if (done !== null) {
|
|
970
|
+
return finished(session, done);
|
|
971
|
+
}
|
|
972
|
+
let prepared = null;
|
|
973
|
+
if (state.kind === "valid") {
|
|
974
|
+
const syntax = await checkSyntax(state.prepared, syntaxOverride(args));
|
|
975
|
+
if (syntax.issues.length > 0) {
|
|
976
|
+
for (const line of formatSyntax(syntax))
|
|
977
|
+
say(line);
|
|
978
|
+
next("fix the draft, then run follow-me again");
|
|
979
|
+
return 1;
|
|
980
|
+
}
|
|
981
|
+
if (mode === "split") {
|
|
982
|
+
const code = await locked(session, "apply", () => applyLocked(session, args));
|
|
983
|
+
if (code !== 0) {
|
|
984
|
+
next("fix the draft, then run follow-me again");
|
|
985
|
+
return code;
|
|
986
|
+
}
|
|
987
|
+
prepared = latestPlan(session);
|
|
988
|
+
}
|
|
989
|
+
else
|
|
990
|
+
prepared = { version: session.freeze(state.prepared), plan: state.prepared };
|
|
991
|
+
}
|
|
992
|
+
else if (mode === "walk") {
|
|
993
|
+
prepared = walkablePlan(session);
|
|
994
|
+
}
|
|
995
|
+
if (prepared === null)
|
|
996
|
+
return promptHandoff(session, cwd, mode, args);
|
|
997
|
+
const prep = prepared;
|
|
998
|
+
const detached = { ...args, flags: new Set([...args.flags, "detach"]) };
|
|
999
|
+
const result = await hostReview(session, mode, detached, async () => prep, reportSaved);
|
|
1000
|
+
if (result.kind !== "elsewhere")
|
|
1001
|
+
return exitCode(result);
|
|
1002
|
+
return listen(session, result, args);
|
|
1003
|
+
}
|
|
767
1004
|
/** Opens the session, re-extracting instead of failing when the branch moved or --base changed. */
|
|
768
1005
|
async function openFresh(cwd, args) {
|
|
769
1006
|
const opts = sessionOpts(args);
|
|
@@ -801,41 +1038,24 @@ function walkablePlan(session) {
|
|
|
801
1038
|
const verdict = validatePlan(session.diff, draft, { lastReview });
|
|
802
1039
|
return verdict.ok ? { version: session.freeze(verdict.plan), plan: verdict.plan } : null;
|
|
803
1040
|
}
|
|
804
|
-
|
|
805
|
-
return Boolean(process.env["ANTHROPIC_API_KEY"] || process.env["ANTHROPIC_AUTH_TOKEN"]);
|
|
806
|
-
}
|
|
1041
|
+
/** The old name of review; it prints one line saying so and goes on. */
|
|
807
1042
|
async function walk(cwd, args) {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const saved = join(session.reviewsDir("walk"), `v${review.planVersion}.json`);
|
|
811
|
-
say(`code review saved to ${relative(cwd, saved)} with ${plural(review.comments.length, "comment")}`);
|
|
812
|
-
next(`follow-me feedback --walk${branchFlag(args)}, which prints it as a code review for the author`);
|
|
813
|
-
};
|
|
814
|
-
const hosted = await hostReview(session, "walk", args, () => walkPlan(session, cwd, args), report);
|
|
815
|
-
if (hosted.kind === "elsewhere") {
|
|
816
|
-
next(`end the code review on the page, then follow-me feedback --walk${branchFlag(args)}`);
|
|
817
|
-
}
|
|
818
|
-
return exitCode(hosted);
|
|
1043
|
+
console.error("follow-me: walk is now review; running review");
|
|
1044
|
+
return review(cwd, args);
|
|
819
1045
|
}
|
|
820
|
-
async function walkPlan(session,
|
|
821
|
-
|
|
1046
|
+
async function walkPlan(session, args) {
|
|
1047
|
+
const latest = walkablePlan(session);
|
|
822
1048
|
if (latest === null) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return null;
|
|
829
|
-
}
|
|
830
|
-
const drafted = await draftWithApi(session, cwd, "walk", args);
|
|
831
|
-
if (drafted === null)
|
|
832
|
-
return null;
|
|
833
|
-
latest = { version: session.freeze(drafted), plan: drafted };
|
|
1049
|
+
say(`no plan for ${session.anchor.branch} yet.`);
|
|
1050
|
+
say("Have a coding agent follow the prompt, then review again:");
|
|
1051
|
+
say(` follow-me prompt --walk${branchFlag(args)}`);
|
|
1052
|
+
say(` follow-me review${branchFlag(args)}`);
|
|
1053
|
+
return null;
|
|
834
1054
|
}
|
|
835
|
-
return alreadyReviewed(session,
|
|
1055
|
+
return alreadyReviewed(session, latest.version, args) ? null : latest;
|
|
836
1056
|
}
|
|
837
1057
|
/** The consolidated report of a saved review, or null after saying why it cannot be rendered. */
|
|
838
|
-
function renderStored(session, stored) {
|
|
1058
|
+
function renderStored(session, stored, format = "markdown") {
|
|
839
1059
|
const plan = session.plans().find((p) => p.version === stored.version);
|
|
840
1060
|
if (plan === undefined) {
|
|
841
1061
|
console.error(`follow-me: review v${stored.version} refers to a plan that is missing`);
|
|
@@ -851,59 +1071,54 @@ function renderStored(session, stored) {
|
|
|
851
1071
|
.plans()
|
|
852
1072
|
.filter((p) => p.version > stored.version)
|
|
853
1073
|
.flatMap((p) => p.file.responses);
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
}
|
|
859
|
-
async function feedback(cwd, args) {
|
|
860
|
-
const session = await openSession(cwd, sessionOpts(args));
|
|
861
|
-
const walk = args.flags.has("walk");
|
|
862
|
-
const walks = session.walkReviews();
|
|
863
|
-
const stored = walk ? walks.at(-1) : session.reviews().findLast((r) => r.review.mode === "split");
|
|
864
|
-
if (stored === undefined) {
|
|
865
|
-
const branch = branchFlag(args);
|
|
866
|
-
console.error(walk
|
|
867
|
-
? `follow-me: no code review yet, run \`follow-me walk${branch}\` first`
|
|
868
|
-
: walks.length > 0
|
|
869
|
-
? `follow-me: no plan review yet; \`follow-me feedback --walk${branch}\` prints the code review`
|
|
870
|
-
: "follow-me: no review yet, run `follow-me review` first");
|
|
871
|
-
return 1;
|
|
1074
|
+
const opts = { branch: session.anchor.branch, responses };
|
|
1075
|
+
if (format === "json") {
|
|
1076
|
+
const items = reportItems(stored.review, verdict.plan.commits, session.diff.units, opts);
|
|
1077
|
+
return `${JSON.stringify(items, null, 2)}\n`;
|
|
872
1078
|
}
|
|
873
|
-
|
|
874
|
-
if (text === null)
|
|
875
|
-
return 1;
|
|
876
|
-
process.stdout.write(text);
|
|
877
|
-
say();
|
|
878
|
-
next(stored.review.mode === "walk"
|
|
879
|
-
? "hand this to the branch's author"
|
|
880
|
-
: `hand this to your agent, then follow-me check${branchFlag(args)}`);
|
|
881
|
-
return 0;
|
|
1079
|
+
return renderReport(stored.review, verdict.plan.commits, session.diff.units, opts);
|
|
882
1080
|
}
|
|
883
|
-
/**
|
|
884
|
-
|
|
1081
|
+
/**
|
|
1082
|
+
* The report of the hosted review as it stands, else of the latest one that ended. --json lists
|
|
1083
|
+
* the items to carry to a merge request; --markdown, the default, reads as the agent's to-do.
|
|
1084
|
+
* --walk keeps reading only reviewer-mode reviews, as `feedback --walk` did.
|
|
1085
|
+
*/
|
|
1086
|
+
async function report(cwd, args, walkOnly = false) {
|
|
1087
|
+
if (args.flags.has("json") && args.flags.has("markdown")) {
|
|
1088
|
+
throw new UsageError("--json and --markdown cannot be used together");
|
|
1089
|
+
}
|
|
885
1090
|
const session = await openSession(cwd, sessionOpts(args));
|
|
886
|
-
const
|
|
1091
|
+
const format = args.flags.has("json") ? "json" : "markdown";
|
|
1092
|
+
const open = walkOnly ? null : await openOnServer(session, null);
|
|
887
1093
|
if (open !== null) {
|
|
888
|
-
|
|
1094
|
+
const text = await fetchReport(open.port, open.summary, format);
|
|
1095
|
+
process.stdout.write(format === "json" ? `${JSON.stringify(JSON.parse(text), null, 2)}\n` : text);
|
|
889
1096
|
return 0;
|
|
890
1097
|
}
|
|
891
|
-
const ended =
|
|
1098
|
+
const ended = (walkOnly ? session.walkReviews() : reviewsOf(session)).sort((a, b) => a.review.endedAt.localeCompare(b.review.endedAt));
|
|
892
1099
|
const stored = ended.at(-1);
|
|
893
1100
|
if (stored === undefined) {
|
|
894
1101
|
console.error("follow-me: no review is open and none has ended; run `follow-me review` first");
|
|
895
1102
|
return 1;
|
|
896
1103
|
}
|
|
897
|
-
const text = renderStored(session, stored);
|
|
1104
|
+
const text = renderStored(session, stored, format);
|
|
898
1105
|
if (text === null)
|
|
899
1106
|
return 1;
|
|
900
1107
|
process.stdout.write(text);
|
|
901
1108
|
return 0;
|
|
902
1109
|
}
|
|
903
|
-
/**
|
|
1110
|
+
/** The old name of report. */
|
|
1111
|
+
async function feedback(cwd, args) {
|
|
1112
|
+
console.error("follow-me: feedback is now report; printing the same");
|
|
1113
|
+
return report(cwd, args, args.flags.has("walk"));
|
|
1114
|
+
}
|
|
1115
|
+
/** Events the reviewer caused; the agent's own replies, explanations and version swaps are not news. */
|
|
904
1116
|
function isNews(event) {
|
|
905
1117
|
if (event.type === "version")
|
|
906
1118
|
return false;
|
|
1119
|
+
if (event.type === "comment" || event.type === "comment-updated") {
|
|
1120
|
+
return event.comment.author !== "agent";
|
|
1121
|
+
}
|
|
907
1122
|
return !(event.type === "reply" && event.reply.author === "agent");
|
|
908
1123
|
}
|
|
909
1124
|
const POLL_MS = 25_000;
|
|
@@ -926,17 +1141,26 @@ async function watch(cwd, args) {
|
|
|
926
1141
|
if (!Number.isInteger(since) || since < 0) {
|
|
927
1142
|
throw new UsageError(`--since ${sinceRaw} is not an event number`);
|
|
928
1143
|
}
|
|
1144
|
+
// Comments are quoted as the plan they were made on wrote them; a swap changes which that is.
|
|
1145
|
+
let version = open.summary.version;
|
|
1146
|
+
const quoting = (events) => {
|
|
1147
|
+
for (const event of events)
|
|
1148
|
+
if (event.type === "version")
|
|
1149
|
+
version = event.version;
|
|
1150
|
+
return { commits: projectedCommits(session, version), units: session.diff.units };
|
|
1151
|
+
};
|
|
929
1152
|
if (args.flags.has("follow")) {
|
|
930
1153
|
let ended = false;
|
|
931
1154
|
await followEvents(open.port, open.summary, since, (event) => {
|
|
932
1155
|
if (event.type === "ended")
|
|
933
1156
|
ended = true;
|
|
1157
|
+
const ctx = quoting([event]);
|
|
934
1158
|
if (!isNews(event))
|
|
935
1159
|
return;
|
|
936
1160
|
if (args.flags.has("json"))
|
|
937
1161
|
say(JSON.stringify(event));
|
|
938
1162
|
else
|
|
939
|
-
process.stdout.write(renderEvents([event]));
|
|
1163
|
+
process.stdout.write(renderEvents([event], ctx));
|
|
940
1164
|
});
|
|
941
1165
|
if (ended)
|
|
942
1166
|
return 0;
|
|
@@ -952,6 +1176,7 @@ async function watch(cwd, args) {
|
|
|
952
1176
|
}
|
|
953
1177
|
const batch = await pollEvents(open.port, open.summary, since, Math.min(left, POLL_MS));
|
|
954
1178
|
since = batch.seq;
|
|
1179
|
+
const ctx = quoting(batch.events);
|
|
955
1180
|
const news = batch.events.filter(isNews);
|
|
956
1181
|
if (news.length === 0)
|
|
957
1182
|
continue;
|
|
@@ -959,7 +1184,7 @@ async function watch(cwd, args) {
|
|
|
959
1184
|
for (const event of news)
|
|
960
1185
|
say(JSON.stringify(event));
|
|
961
1186
|
else {
|
|
962
|
-
process.stdout.write(renderEvents(news));
|
|
1187
|
+
process.stdout.write(renderEvents(news, ctx));
|
|
963
1188
|
next(news.some((e) => e.type === "ended")
|
|
964
1189
|
? "follow-me report"
|
|
965
1190
|
: `follow-me reply <comment> --body <text>, or write a new draft and apply; then follow-me watch --since ${since}`);
|
|
@@ -974,16 +1199,7 @@ async function reply(cwd, args) {
|
|
|
974
1199
|
if (commentId === undefined || extra !== undefined || body === undefined) {
|
|
975
1200
|
throw new UsageError("usage: follow-me reply <commentId> --body <text> [--diagram <file>]");
|
|
976
1201
|
}
|
|
977
|
-
const
|
|
978
|
-
let diagram;
|
|
979
|
-
if (diagramFile !== undefined) {
|
|
980
|
-
try {
|
|
981
|
-
diagram = readFileSync(diagramFile, "utf8");
|
|
982
|
-
}
|
|
983
|
-
catch (error) {
|
|
984
|
-
throw new UsageError(`cannot read ${diagramFile}: ${error instanceof Error ? error.message : error}`);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
1202
|
+
const diagram = diagramOf(args);
|
|
987
1203
|
const session = await openSession(cwd, sessionOpts(args));
|
|
988
1204
|
const open = await openOnServer(session, null);
|
|
989
1205
|
if (open === null)
|
|
@@ -997,6 +1213,123 @@ async function reply(cwd, args) {
|
|
|
997
1213
|
next("follow-me watch");
|
|
998
1214
|
return 0;
|
|
999
1215
|
}
|
|
1216
|
+
/** The Mermaid source in --diagram's file, or undefined without the flag. */
|
|
1217
|
+
function diagramOf(args) {
|
|
1218
|
+
const file = args.values.get("diagram");
|
|
1219
|
+
if (file === undefined)
|
|
1220
|
+
return undefined;
|
|
1221
|
+
try {
|
|
1222
|
+
return readFileSync(file, "utf8");
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
throw new UsageError(`cannot read ${file}: ${error instanceof Error ? error.message : error}`);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
/** "3" or "2-5" as the change indices they name. */
|
|
1229
|
+
function parseLineRange(raw) {
|
|
1230
|
+
const m = /^(\d+)(?:-(\d+))?$/.exec(raw);
|
|
1231
|
+
const from = Number(m?.[1]);
|
|
1232
|
+
const to = Number(m?.[2] ?? m?.[1]);
|
|
1233
|
+
if (m === null || to < from) {
|
|
1234
|
+
throw new UsageError(`--lines ${raw} is not a change line or a range like 2-5`);
|
|
1235
|
+
}
|
|
1236
|
+
return Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Where an explanation points: --unit with --lines names change lines of a hunk, as the prompt
|
|
1240
|
+
* numbers them, and the page marks their rows. --commit alone is the commit itself; nothing is
|
|
1241
|
+
* the whole plan. Commits count from 1, as the prompt prints them.
|
|
1242
|
+
*/
|
|
1243
|
+
function explainTarget(session, version, args) {
|
|
1244
|
+
const commitRaw = args.values.get("commit");
|
|
1245
|
+
const unitRaw = args.values.get("unit");
|
|
1246
|
+
const linesRaw = args.values.get("lines");
|
|
1247
|
+
if (linesRaw !== undefined && unitRaw === undefined) {
|
|
1248
|
+
throw new UsageError("--lines needs --unit");
|
|
1249
|
+
}
|
|
1250
|
+
if (commitRaw === undefined && unitRaw === undefined) {
|
|
1251
|
+
return { anchor: { kind: "plan" }, context: { commitMessage: "", path: null } };
|
|
1252
|
+
}
|
|
1253
|
+
const commits = projectedCommits(session, version);
|
|
1254
|
+
if (commits.length === 0) {
|
|
1255
|
+
throw new UsageError(`plan v${version} no longer fits ${session.anchor.branch}`);
|
|
1256
|
+
}
|
|
1257
|
+
const unit = unitRaw === undefined || !isUnitId(unitRaw) ? undefined : session.diff.units.get(unitRaw);
|
|
1258
|
+
if (unitRaw !== undefined && unit === undefined) {
|
|
1259
|
+
throw new UsageError(`--unit ${unitRaw} is not a unit of this branch`);
|
|
1260
|
+
}
|
|
1261
|
+
let index = -1;
|
|
1262
|
+
if (commitRaw !== undefined) {
|
|
1263
|
+
index = Number(commitRaw) - 1;
|
|
1264
|
+
if (!Number.isInteger(index) || index < 0 || index >= commits.length) {
|
|
1265
|
+
throw new UsageError(`--commit ${commitRaw} is not one of the ${commits.length} commits`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
index = commits.findIndex((c) => c.refs.some((r) => r.unit === unit?.id));
|
|
1270
|
+
}
|
|
1271
|
+
const commit = commits[index];
|
|
1272
|
+
if (commit === undefined)
|
|
1273
|
+
throw new UsageError(`no commit of plan v${version} takes ${unitRaw}`);
|
|
1274
|
+
if (unit === undefined) {
|
|
1275
|
+
return {
|
|
1276
|
+
anchor: { kind: "message", commit: index },
|
|
1277
|
+
context: { commitMessage: commit.message, path: null },
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const ref = commit.refs.find((r) => r.unit === unit.id);
|
|
1281
|
+
if (ref === undefined)
|
|
1282
|
+
throw new UsageError(`commit ${index + 1} does not take ${unit.id}`);
|
|
1283
|
+
const context = { commitMessage: commit.message, path: unit.path };
|
|
1284
|
+
if (linesRaw === undefined) {
|
|
1285
|
+
return { anchor: { kind: "hunk", unit: unit.id, commit: index, changes: null }, context };
|
|
1286
|
+
}
|
|
1287
|
+
if (unit.kind !== "hunk")
|
|
1288
|
+
throw new UsageError(`--lines needs a text hunk, ${unit.id} is not one`);
|
|
1289
|
+
const changes = parseLineRange(linesRaw);
|
|
1290
|
+
const taken = ref.select.kind === "lines" ? ref.select.changes : null;
|
|
1291
|
+
for (const n of changes) {
|
|
1292
|
+
if (n >= unit.changeCount || (taken !== null && !taken.includes(n))) {
|
|
1293
|
+
throw new UsageError(`commit ${index + 1} does not take change line ${n} of ${unit.id}`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const rows = unit.body.flatMap((line, offset) => line.op !== " " && changes.includes(line.change) ? [{ line, offset }] : []);
|
|
1297
|
+
return {
|
|
1298
|
+
anchor: {
|
|
1299
|
+
kind: "hunk",
|
|
1300
|
+
unit: unit.id,
|
|
1301
|
+
commit: index,
|
|
1302
|
+
changes,
|
|
1303
|
+
offsets: rows.map((r) => r.offset),
|
|
1304
|
+
},
|
|
1305
|
+
context: {
|
|
1306
|
+
...context,
|
|
1307
|
+
lines: rows.map(({ line }) => `${line.op}${line.text.replace(/\r?\n$/, "")}`),
|
|
1308
|
+
},
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
/** Starts a thread on the hosted page: the agent explains before anyone asks. */
|
|
1312
|
+
async function explain(cwd, args) {
|
|
1313
|
+
const body = args.values.get("body");
|
|
1314
|
+
if (body === undefined || args.positionals.length > 0) {
|
|
1315
|
+
throw new UsageError("usage: follow-me explain [--commit <n>] [--unit <id>] [--lines <a-b>] --body <text> [--diagram <file>]");
|
|
1316
|
+
}
|
|
1317
|
+
const diagram = diagramOf(args);
|
|
1318
|
+
const session = await openSession(cwd, sessionOpts(args));
|
|
1319
|
+
const open = await openOnServer(session, null);
|
|
1320
|
+
if (open === null)
|
|
1321
|
+
return notOpen(session);
|
|
1322
|
+
const target = explainTarget(session, open.summary.version, args);
|
|
1323
|
+
const id = await postComment(open.port, open.summary, {
|
|
1324
|
+
...target,
|
|
1325
|
+
body,
|
|
1326
|
+
author: "agent",
|
|
1327
|
+
...(diagram === undefined ? {} : { diagram }),
|
|
1328
|
+
});
|
|
1329
|
+
say(`explained ${id}`);
|
|
1330
|
+
next("follow-me watch");
|
|
1331
|
+
return 0;
|
|
1332
|
+
}
|
|
1000
1333
|
function describeLock(lock) {
|
|
1001
1334
|
if (lock.kind === "free")
|
|
1002
1335
|
return "free";
|
|
@@ -1111,17 +1444,53 @@ function skillTargets(args) {
|
|
|
1111
1444
|
};
|
|
1112
1445
|
}
|
|
1113
1446
|
function missingDraft(error, cwd) {
|
|
1114
|
-
return `no draft at ${relative(cwd, error.path)} yet; your coding agent writes it from \`follow-me prompt\`, or \`follow-me plan\`
|
|
1447
|
+
return `no draft at ${relative(cwd, error.path)} yet; your coding agent writes it from \`follow-me prompt\`, or \`follow-me plan --naive\` writes one commit per file`;
|
|
1115
1448
|
}
|
|
1449
|
+
const PACKAGE_NAME = "@shiyuphan/follow-me";
|
|
1450
|
+
/**
|
|
1451
|
+
* Updates the global package, then refreshes every installed skill copy, which otherwise
|
|
1452
|
+
* stays at the old version. FOLLOW_ME_UPGRADE_COMMAND replaces the npm call in tests.
|
|
1453
|
+
*/
|
|
1454
|
+
function upgrade(targets, cwd) {
|
|
1455
|
+
const override = process.env["FOLLOW_ME_UPGRADE_COMMAND"];
|
|
1456
|
+
const command = override === undefined
|
|
1457
|
+
? ["npm", "install", "-g", `${PACKAGE_NAME}@latest`]
|
|
1458
|
+
: ["sh", "-c", override];
|
|
1459
|
+
const [file, ...args] = command;
|
|
1460
|
+
if (file === undefined)
|
|
1461
|
+
return 1;
|
|
1462
|
+
say(`${targets.dryRun ? "would run" : "running"}: ${override ?? command.join(" ")}`);
|
|
1463
|
+
if (!targets.dryRun) {
|
|
1464
|
+
try {
|
|
1465
|
+
execFileSync(file, args, { stdio: "inherit" });
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
console.error(`follow-me: upgrade failed: ${error instanceof Error ? error.message : error}`);
|
|
1469
|
+
return 1;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
const code = runInstall({ ...targets, list: false }, homedir(), cwd, say);
|
|
1473
|
+
if (code === 0)
|
|
1474
|
+
say(`next: follow-me --version`);
|
|
1475
|
+
return code;
|
|
1476
|
+
}
|
|
1477
|
+
const HELP_OR_VERSION = new Set(["--help", "--version"]);
|
|
1116
1478
|
export async function main(argv, cwd) {
|
|
1117
1479
|
const [command, ...rest] = argv;
|
|
1118
1480
|
try {
|
|
1481
|
+
if (command === undefined || (command.startsWith("--") && !HELP_OR_VERSION.has(command))) {
|
|
1482
|
+
const spec = {
|
|
1483
|
+
values: ["wait", "base", "branch", "port", "syntax", "name"],
|
|
1484
|
+
flags: ["no-open", "author", "reviewer", "allow-syntax-errors", "force", "sign", "no-sign"],
|
|
1485
|
+
};
|
|
1486
|
+
return await entry(cwd, parseArgs(argv, spec));
|
|
1487
|
+
}
|
|
1119
1488
|
switch (command) {
|
|
1120
1489
|
case "extract": {
|
|
1121
1490
|
const args = parseArgs(rest, { values: ["base", "branch"] });
|
|
1122
1491
|
summarize(await extractSession(cwd, sessionOpts(args)), cwd);
|
|
1123
1492
|
const branch = branchFlag(args);
|
|
1124
|
-
next(`follow-me prompt${branch}, or follow-me plan${branch}
|
|
1493
|
+
next(`follow-me prompt${branch}, or follow-me plan --naive${branch}`);
|
|
1125
1494
|
return 0;
|
|
1126
1495
|
}
|
|
1127
1496
|
case "check":
|
|
@@ -1141,34 +1510,41 @@ export async function main(argv, cwd) {
|
|
|
1141
1510
|
return 0;
|
|
1142
1511
|
}
|
|
1143
1512
|
case "plan":
|
|
1144
|
-
return await plan(cwd, parseArgs(rest, { values: ["
|
|
1513
|
+
return await plan(cwd, parseArgs(rest, { values: ["branch"], flags: ["walk", "naive"] }));
|
|
1145
1514
|
case "split": {
|
|
1146
1515
|
const spec = {
|
|
1147
|
-
values: ["base", "branch", "
|
|
1516
|
+
values: ["base", "branch", "syntax"],
|
|
1148
1517
|
flags: ["no-open", "allow-syntax-errors"],
|
|
1149
1518
|
};
|
|
1150
1519
|
return await split(cwd, parseArgs(rest, spec));
|
|
1151
1520
|
}
|
|
1152
1521
|
case "review": {
|
|
1153
|
-
const spec = {
|
|
1522
|
+
const spec = {
|
|
1523
|
+
values: ["base", "branch", "port", "timeout"],
|
|
1524
|
+
flags: ["no-open", "detach", "wait"],
|
|
1525
|
+
};
|
|
1154
1526
|
return await review(cwd, parseArgs(rest, spec));
|
|
1155
1527
|
}
|
|
1156
1528
|
case "walk": {
|
|
1157
|
-
const spec = { values: ["base", "branch", "
|
|
1529
|
+
const spec = { values: ["base", "branch", "port"], flags: ["no-open", "detach"] };
|
|
1158
1530
|
return await walk(cwd, parseArgs(rest, spec));
|
|
1159
1531
|
}
|
|
1160
1532
|
case "server":
|
|
1161
1533
|
return await serverCommand(rest);
|
|
1162
1534
|
case "feedback":
|
|
1163
|
-
return await feedback(cwd, parseArgs(rest, { values: ["branch"], flags: ["walk"] }));
|
|
1535
|
+
return await feedback(cwd, parseArgs(rest, { values: ["branch"], flags: ["walk", "json", "markdown"] }));
|
|
1164
1536
|
case "watch": {
|
|
1165
1537
|
const spec = { values: ["since", "timeout", "branch"], flags: ["json", "follow"] };
|
|
1166
1538
|
return await watch(cwd, parseArgs(rest, spec));
|
|
1167
1539
|
}
|
|
1168
1540
|
case "reply":
|
|
1169
1541
|
return await reply(cwd, parseArgs(rest, { values: ["body", "diagram", "branch"] }));
|
|
1542
|
+
case "explain": {
|
|
1543
|
+
const spec = { values: ["commit", "unit", "lines", "body", "diagram", "branch"] };
|
|
1544
|
+
return await explain(cwd, parseArgs(rest, spec));
|
|
1545
|
+
}
|
|
1170
1546
|
case "report":
|
|
1171
|
-
return await report(cwd, parseArgs(rest, { values: ["branch"] }));
|
|
1547
|
+
return await report(cwd, parseArgs(rest, { values: ["branch"], flags: ["json", "markdown"] }));
|
|
1172
1548
|
case "clean": {
|
|
1173
1549
|
const args = parseArgs(rest, { flags: ["all", "dry-run"] });
|
|
1174
1550
|
const opts = { all: args.flags.has("all"), dryRun: args.flags.has("dry-run") };
|
|
@@ -1187,18 +1563,21 @@ export async function main(argv, cwd) {
|
|
|
1187
1563
|
const args = parseArgs(rest, { values: ["harness"], flags: ["all", "project", "dry-run"] });
|
|
1188
1564
|
return runUninstall(skillTargets(args), homedir(), cwd, say);
|
|
1189
1565
|
}
|
|
1190
|
-
case
|
|
1566
|
+
case "upgrade": {
|
|
1567
|
+
const args = parseArgs(rest, { values: ["harness"], flags: ["all", "project", "dry-run"] });
|
|
1568
|
+
return upgrade(skillTargets(args), cwd);
|
|
1569
|
+
}
|
|
1191
1570
|
case "help":
|
|
1192
1571
|
case "-h":
|
|
1193
1572
|
case "--help":
|
|
1194
|
-
say(USAGE);
|
|
1195
|
-
return
|
|
1573
|
+
say(parseArgs(rest, { flags: ["all"] }).flags.has("all") ? USAGE : SHORT_USAGE);
|
|
1574
|
+
return 0;
|
|
1196
1575
|
case "-v":
|
|
1197
1576
|
case "--version":
|
|
1198
1577
|
say(`follow-me ${version()}`);
|
|
1199
1578
|
return 0;
|
|
1200
1579
|
default:
|
|
1201
|
-
throw new UsageError(`unknown command "${command}"\n${
|
|
1580
|
+
throw new UsageError(`unknown command "${command}"\n${SHORT_USAGE}`);
|
|
1202
1581
|
}
|
|
1203
1582
|
}
|
|
1204
1583
|
catch (error) {
|
|
@@ -1210,7 +1589,6 @@ export async function main(argv, cwd) {
|
|
|
1210
1589
|
SessionLockedError,
|
|
1211
1590
|
RegistrationError,
|
|
1212
1591
|
GitError,
|
|
1213
|
-
PlanRequestError,
|
|
1214
1592
|
NotARepositoryError,
|
|
1215
1593
|
];
|
|
1216
1594
|
if (error instanceof MissingDraftError) {
|