@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 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
- # Temporary Holding Version
1
+ # follow-me
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }