@shiyuphan/follow-me 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/bin/follow-me.js +9 -0
- package/dist/src/anthropic.js +101 -0
- package/dist/src/cli.js +475 -0
- package/dist/src/diff.js +359 -0
- package/dist/src/git.js +210 -0
- package/dist/src/plan.js +418 -0
- package/dist/src/prompt.js +333 -0
- package/dist/src/server.js +272 -0
- package/dist/src/session.js +278 -0
- package/dist/src/types.js +15 -0
- package/dist/ui/app.js +770 -0
- package/dist/ui/index.html +39 -0
- package/dist/ui/playback.js +175 -0
- package/dist/ui/style.css +431 -0
- package/package.json +52 -4
- package/skills/follow-me/SKILL.md +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 follow-me contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# follow-me
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read a branch the way it was thought, not the way it was committed.
|
|
4
|
+
|
|
5
|
+
follow-me takes a branch's net change and splits it into fine commits ordered so a reader
|
|
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.
|
|
10
|
+
|
|
11
|
+
Two people use it:
|
|
12
|
+
|
|
13
|
+
- **The author** runs extract, check, apply and review to turn a messy branch into a readable
|
|
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.
|
|
19
|
+
|
|
20
|
+
Inspired by [spark-joy](https://github.com/Dansyuqri/spark-joy)'s "split your own changes".
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
Requires Node 24 or newer and git. There is no build step; Node runs the TypeScript sources.
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
git clone https://github.com/<you>/follow-me
|
|
28
|
+
cd follow-me && npm install && npm link # puts `follow-me` on your PATH
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Author flow
|
|
32
|
+
|
|
33
|
+
Run everything from the repo whose branch you want to split, with that branch checked out.
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
follow-me extract --base main # anchor a session, list the units (hunks, files, modes)
|
|
37
|
+
follow-me prompt # the planner prompt, for a coding agent to read
|
|
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
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
With Anthropic credentials in the environment, `follow-me plan` writes the draft itself.
|
|
46
|
+
Otherwise the Claude Code skill in `skills/follow-me/` does it; link it into
|
|
47
|
+
`.claude/skills/follow-me/` and ask Claude to split the branch.
|
|
48
|
+
|
|
49
|
+
After a review, the agent reads the comments, writes a new draft that answers each one in
|
|
50
|
+
`responses`, and runs `check` and `apply` again. Plans are versioned under
|
|
51
|
+
`.follow-me/<branch>/plans/`, reviews under `reviews/`, and a comment counts as answered when
|
|
52
|
+
a later plan lists it.
|
|
53
|
+
|
|
54
|
+
## Reviewer flow
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
git checkout their-branch
|
|
58
|
+
follow-me walk --base main # extract, plan, and open playback
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Playback walks the commits in order, speaks the narration the planner wrote, and types each
|
|
62
|
+
added line out. Space pauses, `n` and `p` move between commits, and the speed and voice are
|
|
63
|
+
adjustable. Pause on a line to comment on it. Finishing writes `reviews/vN.json`, and
|
|
64
|
+
`follow-me feedback` renders it as a code review grouped by file.
|
|
65
|
+
|
|
66
|
+
Narration uses the browser's built-in speech synthesis. No key, no network.
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
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`.
|
|
74
|
+
|
|
75
|
+
The user's working tree, index and branches are never touched. Commits are built in a
|
|
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.
|
|
78
|
+
|
|
79
|
+
## Safety
|
|
80
|
+
|
|
81
|
+
- `apply` refuses to move a branch it did not create unless `--force`.
|
|
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
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
npm run check # biome, strict tsc, node --test
|
|
89
|
+
node bin/follow-me.ts <command>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Tests run against real git repositories built by `test/fixture-repo.ts`.
|
|
93
|
+
Decisions are logged in `docs/decisions.tsv`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from "../src/cli.js";
|
|
3
|
+
// `follow-me prompt | head` closes stdout early; that is not an error worth a stack trace.
|
|
4
|
+
process.stdout.on("error", (error) => {
|
|
5
|
+
if (error.code === "EPIPE")
|
|
6
|
+
process.exit(0);
|
|
7
|
+
throw error;
|
|
8
|
+
});
|
|
9
|
+
process.exitCode = await main(process.argv.slice(2), process.cwd());
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
export const DEFAULT_MODEL = "claude-opus-5-5";
|
|
3
|
+
const MAX_TOKENS = 64000;
|
|
4
|
+
export class PlanRequestError extends Error {
|
|
5
|
+
failure;
|
|
6
|
+
constructor(failure, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "PlanRequestError";
|
|
9
|
+
this.failure = failure;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const narratedUnit = {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: { unit: { type: "string" }, narration: { type: "string" } },
|
|
15
|
+
required: ["unit", "narration"],
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
};
|
|
18
|
+
const unitList = { type: "array", items: { anyOf: [{ type: "string" }, narratedUnit] } };
|
|
19
|
+
/**
|
|
20
|
+
* Mirrors PlanFile on the wire. Structured outputs need every property required and closed
|
|
21
|
+
* objects, so commit narration is nullable instead of optional; the plan parser reads null as none.
|
|
22
|
+
*/
|
|
23
|
+
export const PLAN_SCHEMA = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
schema: { type: "string", enum: ["follow-me/plan@1"] },
|
|
27
|
+
base: { type: "string" },
|
|
28
|
+
tip: { type: "string" },
|
|
29
|
+
commits: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
message: { type: "string" },
|
|
35
|
+
narration: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
36
|
+
units: unitList,
|
|
37
|
+
},
|
|
38
|
+
required: ["message", "narration", "units"],
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
responses: {
|
|
43
|
+
type: "array",
|
|
44
|
+
items: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
comment: { type: "string" },
|
|
48
|
+
status: { type: "string", enum: ["addressed", "declined", "deferred"] },
|
|
49
|
+
note: { type: "string" },
|
|
50
|
+
},
|
|
51
|
+
required: ["comment", "status", "note"],
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
required: ["schema", "base", "tip", "commits", "responses"],
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
};
|
|
59
|
+
const sendWithAnthropic = async ({ model, prompt }) => {
|
|
60
|
+
const client = new Anthropic();
|
|
61
|
+
// Streaming because a 64k-token reply outlives the SDK's non-streaming timeout.
|
|
62
|
+
const stream = client.beta.messages.stream({
|
|
63
|
+
model,
|
|
64
|
+
max_tokens: MAX_TOKENS,
|
|
65
|
+
output_config: { effort: "high", format: { type: "json_schema", schema: PLAN_SCHEMA } },
|
|
66
|
+
betas: ["server-side-fallback-2026-07-01"],
|
|
67
|
+
fallbacks: "default",
|
|
68
|
+
messages: [{ role: "user", content: prompt }],
|
|
69
|
+
});
|
|
70
|
+
const message = await stream.finalMessage();
|
|
71
|
+
const text = message.content.flatMap((block) => (block.type === "text" ? [block.text] : []));
|
|
72
|
+
return {
|
|
73
|
+
stopReason: message.stop_reason,
|
|
74
|
+
text: text.join(""),
|
|
75
|
+
refusalExplanation: message.stop_details?.explanation ?? null,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Asks the model for a plan and returns the parsed JSON. The caller validates it as a plan,
|
|
80
|
+
* because a schema-conforming reply can still name units that do not exist.
|
|
81
|
+
*/
|
|
82
|
+
export async function requestPlan(prompt, opts = {}) {
|
|
83
|
+
const send = opts.send ?? sendWithAnthropic;
|
|
84
|
+
const reply = await send({ model: opts.model ?? DEFAULT_MODEL, prompt });
|
|
85
|
+
if (reply.stopReason === "refusal") {
|
|
86
|
+
const why = reply.refusalExplanation ? `: ${reply.refusalExplanation}` : "";
|
|
87
|
+
throw new PlanRequestError("refusal", `the model declined to write a plan${why}`);
|
|
88
|
+
}
|
|
89
|
+
if (reply.stopReason === "max_tokens") {
|
|
90
|
+
throw new PlanRequestError("truncated", `the plan did not fit in ${MAX_TOKENS} tokens; try a smaller diff`);
|
|
91
|
+
}
|
|
92
|
+
if (reply.text.trim() === "") {
|
|
93
|
+
throw new PlanRequestError("empty", "the model returned no text");
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(reply.text);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new PlanRequestError("not-json", "the model's reply was not valid JSON");
|
|
100
|
+
}
|
|
101
|
+
}
|