@shiyuphan/follow-me 0.1.1 → 0.2.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 +127 -66
- package/dist/src/anthropic.js +41 -9
- package/dist/src/cli.js +632 -81
- package/dist/src/comments.js +300 -0
- package/dist/src/git.js +20 -3
- package/dist/src/host.js +92 -6
- package/dist/src/install.js +1 -1
- package/dist/src/naive.js +26 -0
- package/dist/src/plan.js +281 -25
- package/dist/src/prompt.js +391 -82
- package/dist/src/server.js +461 -137
- package/dist/src/session.js +104 -104
- package/dist/src/syntax.js +199 -0
- package/dist/src/types.js +3 -1
- package/dist/ui/app.js +1676 -272
- package/dist/ui/concerns.js +165 -0
- package/dist/ui/highlight.js +221 -0
- package/dist/ui/index.html +61 -7
- package/dist/ui/live.js +184 -0
- package/dist/ui/playback.js +480 -76
- package/dist/ui/questions.js +189 -0
- package/dist/ui/style.css +955 -90
- package/dist/ui/summary.js +45 -0
- package/package.json +4 -2
- package/skills/follow-me/SKILL.md +34 -6
package/README.md
CHANGED
|
@@ -1,104 +1,165 @@
|
|
|
1
|
+
<p align="center"><img src="assets/logo.png" width="200" alt="A sweaty cyan blob with mismatched googly eyes and its tongue out chases a smug red blob in a party hat labelled HEAD along a git branch"></p>
|
|
2
|
+
|
|
1
3
|
# follow-me
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
follow-me splits a branch's net change into small commits in an order a reader can follow, proves the result equals the original, and lets you review it line by line.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@shiyuphan/follow-me)
|
|
8
|
+
[](https://github.com/phanshiyu/follow-me/actions/workflows/ci.yml)
|
|
9
|
+
|
|
10
|
+
<p align="center"><img src="assets/playback.gif" width="960" alt="Playback of an eight-commit branch: each commit opens with its narration, then its changed lines appear chunk by chunk beside a sequence diagram"></p>
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
can follow the author's thought process. Individual commits need not compile. The last
|
|
7
|
-
commit's tree is byte-identical to the original tip, and follow-me proves it with git's own
|
|
8
|
-
tree hash. Then it serves a local page where you walk the commits one by one, comment on any
|
|
9
|
-
hunk or line, and hand the comments to a coding agent for the next version.
|
|
12
|
+
*Playback at 1x, sped up 1.5 times for the GIF. The voice is not in the GIF; the browser reads the narration aloud.*
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
## Why
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
stack before asking for review. Comments on the split go back to the agent, which writes
|
|
15
|
-
the next plan.
|
|
16
|
-
- **The reviewer** runs `follow-me walk` on someone else's branch to watch it being written,
|
|
17
|
-
with narration spoken as each line appears, and leaves comments on the code. The result is
|
|
18
|
-
a code review for the author.
|
|
16
|
+
People commit in the order things happened: wip, fix, actually fix, revert. A reviewer then reads that history or gives up and reads one big diff. Neither shows how the change was thought through.
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
The blob with its tongue out is you, on commit 31 of someone's 47-commit branch.
|
|
19
|
+
|
|
20
|
+
follow-me is for two people:
|
|
21
|
+
|
|
22
|
+
- **The author** commits however they like, then wants a branch that reads like a story before asking for review.
|
|
23
|
+
- **The reviewer** wants to watch someone else's branch being written, one line at a time, with the author's reasoning read aloud.
|
|
21
24
|
|
|
22
25
|
## Install
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
You need Node 24 or newer and git.
|
|
25
28
|
|
|
26
29
|
```sh
|
|
27
|
-
|
|
28
|
-
cd follow-me && npm install && npm link # puts `follow-me` on your PATH
|
|
30
|
+
npm i -g @shiyuphan/follow-me
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
follow-me ships an Agent Skill that teaches a coding agent to drive the CLI. Install it once:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
follow-me install
|
|
37
|
+
```
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
`install` looks in your home directory for the config directories of Claude Code, OpenCode, Codex CLI, Cursor, Gemini CLI, and GitHub Copilot CLI. It copies the skill into the user-level skill directory of each one it finds, for example `~/.claude/skills/follow-me/SKILL.md`, so every repo sees it. Add `--project` to install into the current repo instead. Other flags are `--harness claude,codex`, `--all`, and `--dry-run`.
|
|
34
40
|
|
|
35
41
|
```sh
|
|
36
|
-
follow-me
|
|
37
|
-
follow-me
|
|
38
|
-
# the agent writes .follow-me/<branch>/draft.json
|
|
39
|
-
follow-me check # every problem with the draft, at once
|
|
40
|
-
follow-me apply # write <branch>-split, prove the tree matches
|
|
41
|
-
follow-me review # open the review page; end it to save reviews/v1.json
|
|
42
|
-
follow-me feedback # the comments as markdown, for the agent
|
|
42
|
+
follow-me install --list # each harness, where its skill goes, and its docs
|
|
43
|
+
follow-me uninstall # removes exactly the files install writes
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
[Using follow-me from your coding agent](docs/harnesses.md) has the per-harness details.
|
|
47
|
+
|
|
48
|
+
## Split your own branch
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
Check out the branch you want to split and run this from the repo root:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
follow-me split --base main # extract, draft, apply and open the review in one go
|
|
54
|
+
```
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
With an Anthropic key in the environment, `split` asks the API for the draft. Without one, it writes `.follow-me/<branch>/prompt.md` and stops: ask your coding agent to follow it, then run `follow-me check` and `follow-me split` again. Every command ends with a `next:` line naming the step after it. The steps `split` runs are also commands of their own:
|
|
55
57
|
|
|
56
58
|
```sh
|
|
57
|
-
|
|
58
|
-
follow-me
|
|
59
|
+
follow-me extract --base main # snapshot the diff from main to the tip, list the units
|
|
60
|
+
follow-me prompt # instructions and every unit, for your coding agent
|
|
61
|
+
# the agent writes .follow-me/<branch>/draft.json
|
|
62
|
+
follow-me check # print every problem with the draft at once
|
|
63
|
+
follow-me apply # write the commits to <branch>-split
|
|
64
|
+
follow-me review # open the review page in your browser
|
|
65
|
+
follow-me feedback # print the review as markdown for the agent
|
|
59
66
|
```
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
A unit is a text hunk (`h_…`), a whole file that cannot be split by line (`f_…`), or a mode change (`m_…`). The draft assigns every change line to exactly one commit. `h_x:0-3,7` takes change lines 0 to 3 and 7 of hunk `h_x`, so one hunk can spread across several commits.
|
|
69
|
+
|
|
70
|
+
If you skip the agent, `follow-me plan` asks the Anthropic API for the draft. It needs `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN` in the environment, and takes `--model <id>`.
|
|
71
|
+
|
|
72
|
+
`check` and `apply` also parse every file each commit writes, so a split can leave a commit that does not build but never one that ends inside a function. `.js`, `.ts`, `.json`, `.sh` and `.py` files are checked and other types are listed as skipped; `--syntax "<command with {file}>"` swaps in your own checker and `apply --allow-syntax-errors` goes ahead anyway. With no API key and no agent, `follow-me plan --naive` writes one `wip: <path>` commit per touched file, which is enough to run `check`, `apply` and the tree proof on any branch.
|
|
73
|
+
|
|
74
|
+
`apply` never writes your original branch, your index, or your working tree. It builds the commits in a temporary index and points `<branch>-split` at them, or the branch you name with `--name`. It refuses the branch being split and any branch checked out in a worktree, even with `--force`. `--force` only lets it move a branch follow-me did not create. Then it compares the last commit's tree hash with the original tip's and prints both:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
created feature-split at c90eb5350284 from plan v1 (3 commits)
|
|
78
|
+
signing: off
|
|
79
|
+
proof: feature-split^{tree} ae948488c55a3606fa2419eb1c018fff8014093d equals feature^{tree} ae948488c55a3606fa2419eb1c018fff8014093d
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
If the hashes differ, `apply` exits with code 1. The commits are signed when your git config signs commits (`commit.gpgsign`), and `--sign` or `--no-sign` overrides that. Running `apply` again with the same plan prints `unchanged` and leaves the branch alone, signed or not.
|
|
83
|
+
|
|
84
|
+
The red blob in the party hat is your branch tip. Nothing follow-me does moves it.
|
|
85
|
+
|
|
86
|
+

|
|
87
|
+
*The review page. Pick a commit on the left. Comment on its message, its position ("This belongs elsewhere"), a hunk, or single lines.*
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
*One hunk split across commits. Commit 3 takes 2 of 7 change lines. The others are dimmed and marked "→ commit 4" or "→ commit 5".*
|
|
91
|
+
|
|
92
|
+
On the review page, `n` or `j` goes to the next commit, `p` or `k` to the previous one. **End review** saves `.follow-me/<branch>/reviews/vN.json`. An unfinished review autosaves and comes back when you open the session again.
|
|
93
|
+
|
|
94
|
+
## Watch someone else's branch
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
Check out their branch and run:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
follow-me walk --base main
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`walk` extracts the branch and plans it as a narrated story. With an Anthropic key in the environment it asks the API. Without one, it prints `follow-me prompt --walk` for your coding agent, and you run `follow-me walk` again once the agent's draft passes `follow-me check`. `walk` applies nothing and writes no branch.
|
|
103
|
+
|
|
104
|
+

|
|
105
|
+
*Playback. Each line types in while the narration for its commit or hunk is read aloud. The caption shows the narration being read.*
|
|
106
|
+
|
|
107
|
+
Playback goes commit by commit. Space pauses, **Previous** and **Next** move between commits, and you can change the speed, pick a voice, or mute. Pause on any line to comment on it. Narration uses the browser's built-in speech synthesis, so it needs no key and no network.
|
|
108
|
+
|
|
109
|
+
When you end the review, `follow-me feedback --walk` prints it as a code review for the author, grouped by file. Code reviews are saved apart from the author's plan reviews, in `.follow-me/<branch>/reviews/walk/vN.json`, so you can walk a plan the author already reviewed. Each comment quotes its hunk as it reads in that commit, with the line number on the branch. The author's agent fixes the branch itself. A walk review never feeds the split loop below.
|
|
110
|
+
|
|
111
|
+
## One server for every review
|
|
112
|
+
|
|
113
|
+
The first `review` or `walk` starts a review server for your user and serves the page. Every later `review` or `walk`, from any repo or worktree, adds its session to that server, prints the URL, and exits. The server's index page lists every open session.
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
follow-me review --detach # start the server in the background and return
|
|
117
|
+
follow-me review --no-open # print the URL without opening a browser
|
|
118
|
+
follow-me review --wait # return only once the review ends, so an agent can go on to feedback
|
|
119
|
+
follow-me server status # the server, its port, and the sessions it hosts
|
|
120
|
+
follow-me server stop # stop it, autosaved reviews are kept
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The server listens on `127.0.0.1` only. If the `--port` you ask for is taken by another program, it picks a free port and prints `port N is in use by another program, using M`. It exits on its own 30 seconds after its last session ends. It is the only one at the party who knows when to leave. Its record lives in `$FOLLOW_ME_HOME`, else `$XDG_STATE_HOME/follow-me`, else `~/.local/state/follow-me`.
|
|
124
|
+
|
|
125
|
+
Each worktree has its own `.follow-me/` directory, which git ignores, and each branch has its own session in it. A lock stops two agents from applying or reviewing one session at once. `follow-me clean` removes what no live session needs: sessions of deleted branches, review drafts that a finished review replaced, and locks and temp files of dead processes. It keeps plan versions, because reviews refer to them. `clean --dry-run` lists what it would remove, and `clean --all` removes `.follow-me/` entirely unless a live process holds a session.
|
|
67
126
|
|
|
68
127
|
## How it works
|
|
69
128
|
|
|
70
|
-
Every unit comes from one `git diff base..tip`, so hunks of a file never overlap in base
|
|
71
|
-
coordinates. A commit's version of a file is the base lines with that commit's change lines
|
|
72
|
-
swapped in. Nothing is patched, so hunks can be assigned in any order and split by line.
|
|
73
|
-
See `docs/DESIGN.md`.
|
|
129
|
+
Every unit comes from one `git diff base..tip`, so the hunks of a file never overlap in base coordinates. A commit's version of a file is therefore the base lines with that commit's change lines swapped in. Nothing is patched. Hunks can go in any order and split at any line, and nothing can fail to apply.
|
|
74
130
|
|
|
75
|
-
|
|
76
|
-
temporary index with dates derived from the tip, so the same plan always yields the same
|
|
77
|
-
SHAs and re-running `apply` is a no-op.
|
|
131
|
+
Two checks replace patching. While parsing, every context line and removed line must equal the base file at its position. After the last commit, every path's bytes and mode must equal the tip, and then git's own tree hash is compared once more.
|
|
78
132
|
|
|
79
|
-
|
|
133
|
+
There is one limit. A commit can only contain lines that exist in the base or in the tip, so follow-me cannot invent an intermediate version of a line that the author typed and later deleted.
|
|
80
134
|
|
|
81
|
-
|
|
82
|
-
- The review server binds `127.0.0.1` only and rejects cross-origin requests.
|
|
83
|
-
- Everything under `.follow-me/` is ignored by git.
|
|
84
|
-
- Outside the repo, follow-me writes only its state dir (`~/.local/state/follow-me`, or
|
|
85
|
-
`$XDG_STATE_HOME/follow-me`), which holds the review server's `server.json` and `server.log`.
|
|
86
|
-
- One review server per user hosts every session. `follow-me server stop` signals it only after
|
|
87
|
-
it answers as follow-me on its recorded port, so a reused pid is never killed.
|
|
135
|
+
[docs/DESIGN.md](docs/DESIGN.md) has the full design.
|
|
88
136
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
137
|
+
## The agent loop
|
|
138
|
+
|
|
139
|
+
Each `apply` freezes the draft as the next plan version, `.follow-me/<branch>/plans/vN.json`. Each review belongs to one version. `follow-me feedback` gives every comment an id like `c_abc234`. After a review, `follow-me prompt` includes the previous plan and the open comments, and the agent answers each comment in the next draft:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
"responses": [
|
|
143
|
+
{ "comment": "c_abc234", "status": "addressed", "note": "Moved the guard before its first use." }
|
|
144
|
+
]
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`status` is `addressed`, `declined`, or `deferred`. A comment counts as answered once a later plan lists it. `check` warns about unanswered comments but does not block `apply`. `follow-me status` shows the plans, the reviews, how many comments are still open, and the branch `apply` writes.
|
|
95
148
|
|
|
96
149
|
## Development
|
|
97
150
|
|
|
98
151
|
```sh
|
|
99
|
-
npm
|
|
100
|
-
node
|
|
152
|
+
npm install
|
|
153
|
+
npm run check # biome, strict tsc, and node --test, the same as CI
|
|
154
|
+
node bin/follow-me.ts <command> # run the CLI from source
|
|
101
155
|
```
|
|
102
156
|
|
|
103
|
-
Tests run
|
|
104
|
-
|
|
157
|
+
There is no build step for development. Node 24 runs the TypeScript sources directly, and `npm run build` exists only for publishing. Tests run real git against repositories built by `test/fixture-repo.ts`. Non-obvious decisions go in `docs/decisions.tsv` through `scripts/log-decision.sh`.
|
|
158
|
+
|
|
159
|
+
To release, run `npm version patch`, `minor`, or `major`, then `git push --follow-tags`. The tag triggers the publish workflow. [docs/releasing.md](docs/releasing.md) has the details.
|
|
160
|
+
|
|
161
|
+
## Credits
|
|
162
|
+
|
|
163
|
+
Inspired by the "split your own changes" idea in [spark-joy](https://github.com/Dansyuqri/spark-joy). Built with [pstack](https://github.com/alexanderop/pstack).
|
|
164
|
+
|
|
165
|
+
MIT licensed.
|
package/dist/src/anthropic.js
CHANGED
|
@@ -9,16 +9,35 @@ export class PlanRequestError extends Error {
|
|
|
9
9
|
this.failure = failure;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
const
|
|
12
|
+
const optionalText = { anyOf: [{ type: "string" }, { type: "null" }] };
|
|
13
|
+
const concern = {
|
|
13
14
|
type: "object",
|
|
14
|
-
properties: {
|
|
15
|
-
|
|
15
|
+
properties: {
|
|
16
|
+
severity: { type: "string", enum: ["blocker", "major", "minor"] },
|
|
17
|
+
kind: { type: "string", enum: ["bug", "missing-test", "naming", "risk"] },
|
|
18
|
+
text: { type: "string" },
|
|
19
|
+
lines: { anyOf: [{ type: "array", items: { type: "integer" } }, { type: "null" }] },
|
|
20
|
+
},
|
|
21
|
+
required: ["severity", "kind", "text", "lines"],
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
};
|
|
24
|
+
const concerns = { anyOf: [{ type: "array", items: concern }, { type: "null" }] };
|
|
25
|
+
const annotatedUnit = {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
unit: { type: "string" },
|
|
29
|
+
narration: optionalText,
|
|
30
|
+
diagram: optionalText,
|
|
31
|
+
concerns,
|
|
32
|
+
},
|
|
33
|
+
required: ["unit", "narration", "diagram", "concerns"],
|
|
16
34
|
additionalProperties: false,
|
|
17
35
|
};
|
|
18
|
-
const unitList = { type: "array", items: { anyOf: [{ type: "string" },
|
|
36
|
+
const unitList = { type: "array", items: { anyOf: [{ type: "string" }, annotatedUnit] } };
|
|
19
37
|
/**
|
|
20
38
|
* Mirrors PlanFile on the wire. Structured outputs need every property required and closed
|
|
21
|
-
* objects, so
|
|
39
|
+
* objects, so narration, diagram, concerns and a response's `at` are nullable instead of
|
|
40
|
+
* optional; the plan parser reads null as none.
|
|
22
41
|
*/
|
|
23
42
|
export const PLAN_SCHEMA = {
|
|
24
43
|
type: "object",
|
|
@@ -32,10 +51,12 @@ export const PLAN_SCHEMA = {
|
|
|
32
51
|
type: "object",
|
|
33
52
|
properties: {
|
|
34
53
|
message: { type: "string" },
|
|
35
|
-
narration:
|
|
54
|
+
narration: optionalText,
|
|
55
|
+
diagram: optionalText,
|
|
56
|
+
concerns,
|
|
36
57
|
units: unitList,
|
|
37
58
|
},
|
|
38
|
-
required: ["message", "narration", "units"],
|
|
59
|
+
required: ["message", "narration", "diagram", "concerns", "units"],
|
|
39
60
|
additionalProperties: false,
|
|
40
61
|
},
|
|
41
62
|
},
|
|
@@ -45,10 +66,21 @@ export const PLAN_SCHEMA = {
|
|
|
45
66
|
type: "object",
|
|
46
67
|
properties: {
|
|
47
68
|
comment: { type: "string" },
|
|
48
|
-
status: { type: "string", enum: ["addressed", "declined", "deferred"] },
|
|
69
|
+
status: { type: "string", enum: ["addressed", "declined", "deferred", "answered"] },
|
|
49
70
|
note: { type: "string" },
|
|
71
|
+
at: {
|
|
72
|
+
anyOf: [
|
|
73
|
+
{
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: { commit: { type: "integer" }, unit: optionalText },
|
|
76
|
+
required: ["commit", "unit"],
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
{ type: "null" },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
50
82
|
},
|
|
51
|
-
required: ["comment", "status", "note"],
|
|
83
|
+
required: ["comment", "status", "note", "at"],
|
|
52
84
|
additionalProperties: false,
|
|
53
85
|
},
|
|
54
86
|
},
|