fanout-cli 0.7.0 → 0.9.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 +8 -2
- package/dist/cli.js +0 -0
- package/dist/cli.js.map +7 -1
- package/dist/fake-agent.js +0 -0
- package/dist/fake-agent.js.map +7 -0
- package/dist/view.html +863 -0
- package/package.json +15 -11
- package/plugin/.claude-plugin/plugin.json +12 -0
- package/plugin/.mcp.json +10 -0
- package/plugin/README.md +44 -0
- package/plugin/bin/fanout +48 -0
- package/plugin/commands/crew.md +11 -0
- package/plugin/commands/fanout.md +30 -0
- package/plugin/commands/watch.md +20 -0
- package/plugin/hooks/hooks.json +28 -0
- package/plugin/skills/fanout/SKILL.md +126 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/demo.d.ts +0 -32
- package/dist/demo.d.ts.map +0 -1
- package/dist/demo.js +0 -207
- package/dist/demo.js.map +0 -1
- package/dist/format.d.ts +0 -4
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js +0 -64
- package/dist/format.js.map +0 -1
- package/dist/home.d.ts +0 -9
- package/dist/home.d.ts.map +0 -1
- package/dist/home.js +0 -13
- package/dist/home.js.map +0 -1
- package/dist/main.d.ts +0 -21
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js +0 -602
- package/dist/main.js.map +0 -1
- package/dist/unfinished.d.ts +0 -34
- package/dist/unfinished.d.ts.map +0 -1
- package/dist/unfinished.js +0 -84
- package/dist/unfinished.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fanout-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Let Claude Code lead the other coding-agent CLIs you already pay for.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,27 +18,31 @@
|
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
20
|
".": {
|
|
21
|
-
"
|
|
22
|
-
"default": "./dist/main.js"
|
|
21
|
+
"default": "./dist/cli.js"
|
|
23
22
|
}
|
|
24
23
|
},
|
|
25
24
|
"files": [
|
|
26
25
|
"dist",
|
|
26
|
+
"plugin",
|
|
27
27
|
"README.md",
|
|
28
28
|
"LICENSE"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"fanout-
|
|
37
|
-
"fanout-adapter-
|
|
38
|
-
"fanout-
|
|
32
|
+
"ws": "8.21.3",
|
|
33
|
+
"zod": "4.6.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"fanout-adapter-claude": "0.9.0",
|
|
37
|
+
"fanout-adapter-codex": "0.9.0",
|
|
38
|
+
"fanout-core": "0.9.0",
|
|
39
|
+
"fanout-adapter-grok": "0.9.0",
|
|
40
|
+
"fanout-adapter-fake": "0.9.0",
|
|
41
|
+
"fanout-daemon": "0.9.0",
|
|
42
|
+
"fanout-mcp": "0.9.0"
|
|
39
43
|
},
|
|
40
44
|
"scripts": {
|
|
41
45
|
"typecheck": "tsc -p tsconfig.json",
|
|
42
|
-
"build": "
|
|
46
|
+
"build": "node ../../scripts/bundle-cli.mjs"
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fanout",
|
|
3
|
+
"description": "Lead a crew of the coding-agent CLIs you already pay for: plan, fan out into isolated git worktrees, watch, and review before anything merges.",
|
|
4
|
+
"version": "0.6.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "elberacasa"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/elberacasa/fanout",
|
|
9
|
+
"repository": "https://github.com/elberacasa/fanout",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": ["agents", "orchestration", "codex", "grok", "worktrees", "local-first"]
|
|
12
|
+
}
|
package/plugin/.mcp.json
ADDED
package/plugin/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# The Fanout plugin for Claude Code
|
|
2
|
+
|
|
3
|
+
Claude Code becomes the lead: it plans a mission, fans it out to the other agent CLIs on your machine, watches them
|
|
4
|
+
work in isolated git worktrees, and reviews every diff. Nothing merges without you.
|
|
5
|
+
|
|
6
|
+
## What you get
|
|
7
|
+
|
|
8
|
+
| | |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `/fanout <goal>` | Plan a mission, check it against the safety gate, launch it, and watch it |
|
|
11
|
+
| `/fanout:crew` | Which CLIs are installed, signed in, and ready |
|
|
12
|
+
| `/fanout:watch [mission]` | Subscribe to the live feed and review each run as it finishes |
|
|
13
|
+
| The `fanout` skill | The lead's judgment: when to fan out, how to write a prompt an agent can follow, how to review what comes back |
|
|
14
|
+
| Hooks | The crew at the start of a session; a warning if runs are still going when you stop |
|
|
15
|
+
| Seven MCP tools | `seats`, `repo_overview`, `plan_check`, `launch`, `mission_status`, `run_diff`, `cancel_mission` |
|
|
16
|
+
|
|
17
|
+
## Installing
|
|
18
|
+
|
|
19
|
+
The plugin runs `fanout mcp`, so the `fanout` command must be on your `PATH`.
|
|
20
|
+
|
|
21
|
+
**From a clone**, while the package is not yet published:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
git clone https://github.com/elberacasa/fanout.git && cd fanout
|
|
25
|
+
corepack enable && pnpm install
|
|
26
|
+
pnpm --filter fanout-cli link --global # puts `fanout` on your PATH
|
|
27
|
+
claude --plugin-dir "$PWD/plugin" # try it in one session
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Check it before you rely on it:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
fanout status # your crew
|
|
34
|
+
claude plugin validate plugin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What it will not do
|
|
38
|
+
|
|
39
|
+
- **It will not merge.** It reads a run's diff from the workspace and hands you the decision. The merge gate, with
|
|
40
|
+
review, your project's checks and proof that a fix fails on the old code, is the next milestone.
|
|
41
|
+
- **It will not work around a usage limit.** A seat that is out of quota is out; the lead says so and uses another.
|
|
42
|
+
- **It will not hand an agent your secrets.** Workspaces exclude ignored and deny-listed files, and a run gets an
|
|
43
|
+
allowlisted environment only — never your shell's.
|
|
44
|
+
- **It sends nothing anywhere.** The daemon listens on `127.0.0.1` with a token only you can read.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Run as `node bin/fanout`, never relied on as an executable. npm strips the executable bit from every file it
|
|
4
|
+
* does not list in `bin`, so the published copy of this arrives at mode 644 and spawning it directly fails with
|
|
5
|
+
* EACCES on a stranger's machine and nowhere else. Going through `node` removes the whole question, along with
|
|
6
|
+
* the same question on Windows.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Finding the CLI from inside the plugin, whichever way Fanout was installed.
|
|
13
|
+
*
|
|
14
|
+
* The plugin's `.mcp.json` and hooks used to point straight at `${CLAUDE_PLUGIN_ROOT}/../packages/cli/src/cli.ts`,
|
|
15
|
+
* which is the layout of a git checkout and of nothing else. Anyone who installed from npm got a plugin whose
|
|
16
|
+
* every entry point resolved to a file that was not there — and the README's first sentence calls this a Claude
|
|
17
|
+
* Code plugin, so the published package delivered half of what it claimed.
|
|
18
|
+
*
|
|
19
|
+
* There are two real layouts and this script is the one place that knows the difference:
|
|
20
|
+
*
|
|
21
|
+
* published <package>/plugin/bin/fanout → <package>/dist/cli.js
|
|
22
|
+
* checkout <repo>/plugin/bin/fanout → <repo>/packages/cli/src/cli.ts
|
|
23
|
+
*
|
|
24
|
+
* Ordered with the published layout first: that is the one a stranger has, and the one nobody here would notice
|
|
25
|
+
* was broken.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const here = new URL(".", import.meta.url);
|
|
29
|
+
const candidates = ["../../dist/cli.js", "../../packages/cli/src/cli.ts"];
|
|
30
|
+
|
|
31
|
+
const cli = candidates.map((path) => fileURLToPath(new URL(path, here))).find((path) => existsSync(path));
|
|
32
|
+
|
|
33
|
+
if (cli === undefined) {
|
|
34
|
+
process.stderr.write(
|
|
35
|
+
"fanout: this plugin cannot find the Fanout CLI beside it.\n" +
|
|
36
|
+
" Looked for dist/cli.js and packages/cli/src/cli.ts next to the plugin directory.\n" +
|
|
37
|
+
" If you installed from npm, reinstall fanout-cli; if this is a checkout, run pnpm install first.\n",
|
|
38
|
+
);
|
|
39
|
+
process.exit(69);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* Imported rather than spawned. A second process would double the startup and, more importantly, put a pipe
|
|
44
|
+
* between Claude Code and an MCP server that speaks over stdin and stdout — the one place an extra hop can
|
|
45
|
+
* silently reorder or buffer a conversation.
|
|
46
|
+
*/
|
|
47
|
+
process.argv = [process.argv[0] ?? "node", cli, ...process.argv.slice(2)];
|
|
48
|
+
await import(cli);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show which agent CLIs are installed, signed in and ready to work
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Call the `seats` tool and report the crew in one short table: seat, version, and whether it is ready.
|
|
6
|
+
|
|
7
|
+
Be exact about what is not known. A CLI with no way to report sign-in is **unknown**, never "ready". If a seat is
|
|
8
|
+
installed but signed out, say the command that fixes it (`codex login`, `cursor-agent login`, and so on). If a seat
|
|
9
|
+
is an unsupported version, say which versions this build was verified against rather than guessing that it works.
|
|
10
|
+
|
|
11
|
+
Then, in one line, say what the crew could take on right now.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Plan a mission, fan it out to your other agent CLIs, and watch it
|
|
3
|
+
argument-hint: "[what you want done]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
The user wants this done by the crew: **$ARGUMENTS**
|
|
7
|
+
|
|
8
|
+
You are the lead. Work in this order, and do not skip the parts that keep it safe.
|
|
9
|
+
|
|
10
|
+
1. **Look before planning.** Call `repo_overview` and `seats`. If no seat is ready, say so and stop: there is no
|
|
11
|
+
crew to lead.
|
|
12
|
+
2. **Split the work honestly.** One line per piece that can stand alone, each with:
|
|
13
|
+
- a **write scope** narrow enough that no two lines that run at once can touch the same file;
|
|
14
|
+
- a **prompt** that names the exact files, the expected behaviour with an example, the project's check commands,
|
|
15
|
+
and "do not commit";
|
|
16
|
+
- a **seat**: give the risky or subtle piece to the strongest seat, the mechanical piece to the cheapest, and
|
|
17
|
+
read-only work to an auditor. Claude is opt-in; prefer the other subscriptions.
|
|
18
|
+
Dependent work goes in `dependsOn` rather than in one big line.
|
|
19
|
+
3. **Check before launching.** Call `plan_check`. Fix what it blocks; do not argue with it. Show the user the plan
|
|
20
|
+
as a short list (line, seat, scope) and the dry run if they ask.
|
|
21
|
+
4. **Launch** with `launch`, then tell the user the mission id and what each line is doing.
|
|
22
|
+
5. **Watch** with `mission_status`. While runs are going, do not start unrelated work in this session: you are the
|
|
23
|
+
lead, and reviewing is your job.
|
|
24
|
+
6. **Review each finished run** with `run_diff`. Read the risky lines yourself. Anything written outside its scope
|
|
25
|
+
is the first thing you look at. Then tell the user, per run: what it changed, what you would keep, what you would
|
|
26
|
+
not, and what you could not verify.
|
|
27
|
+
7. **Merging is the user's call, and it is not automatic.** Apply a diff yourself only when they say so, run the
|
|
28
|
+
project's real checks afterwards, and never present an agent's "tests pass" as your own verification.
|
|
29
|
+
|
|
30
|
+
If something fails, say which line, why, and what you propose — never "it didn't work".
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Watch a running mission and review each run as it finishes
|
|
3
|
+
argument-hint: "[mission id]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Watch the mission **$ARGUMENTS** (if no id is given, use the most recent one you launched).
|
|
7
|
+
|
|
8
|
+
Subscribe to the live feed rather than polling in a loop: start a Monitor on the daemon's WebSocket, which only
|
|
9
|
+
sends the events a lead acts on.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
ws://127.0.0.1:<port>/events?for=lead&missionId=$ARGUMENTS&token=<token>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The port is in `~/.fanout/daemon.json` and the token in `~/.fanout/token`. Read both; never print the token.
|
|
16
|
+
|
|
17
|
+
When a run finishes, review it straight away with `run_diff` while the rest are still going: read what it changed,
|
|
18
|
+
look first at anything written outside its declared scope, and tell the user in two or three lines what you would
|
|
19
|
+
keep and what you would not. When every run has finished, give the mission summary: what succeeded, what failed and
|
|
20
|
+
why, and what you recommend merging.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node",
|
|
9
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/bin/fanout", "status"],
|
|
10
|
+
"timeout": 20
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"Stop": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node",
|
|
21
|
+
"args": ["${CLAUDE_PLUGIN_ROOT}/bin/fanout", "owed"],
|
|
22
|
+
"timeout": 20
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fanout
|
|
3
|
+
description: Lead a crew of agent CLIs (Codex, Grok, Claude) through the Fanout tools - plan a mission, fan it out into isolated git worktrees, watch it, and review every diff before anything merges. Use when a task splits into independent pieces that can run in parallel, or when the user asks for the crew, a fan-out, or a mission. Not for a single edit you can make yourself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Leading a crew with Fanout
|
|
7
|
+
|
|
8
|
+
The Fanout MCP server runs the other agent CLIs on this machine. You stay the lead: you plan, you review, and the
|
|
9
|
+
user decides what merges. The tools do the mechanics — worktrees, launching, limits, the ledger — so that your
|
|
10
|
+
judgment is the only thing that has to be good.
|
|
11
|
+
|
|
12
|
+
## Before you say anything is done
|
|
13
|
+
|
|
14
|
+
This part applies to **your own** code, not only to work you fanned out. Most of the code in a session is written
|
|
15
|
+
by you and read by nobody else, and your own reasoning is exactly what makes your mistakes invisible to you: you
|
|
16
|
+
know why it is right, so it looks right.
|
|
17
|
+
|
|
18
|
+
So before telling the user work is finished, call `check_claims` with two to four things you believe about your own
|
|
19
|
+
uncommitted changes. Another vendor's CLI reads them cold — no plan, no justification, just the diff — and tries to
|
|
20
|
+
falsify each one.
|
|
21
|
+
|
|
22
|
+
**Write claims that can be proven false.**
|
|
23
|
+
|
|
24
|
+
| Write this | Not this |
|
|
25
|
+
|---|---|
|
|
26
|
+
| "No caller of `total()` passes fewer than two arguments" | "The refactor is safe" |
|
|
27
|
+
| "Nothing outside `src/api/` changed behaviour" | "It works" |
|
|
28
|
+
| "Every new branch in `parse()` has a test" | "Well tested" |
|
|
29
|
+
| "A symlink cannot lead the reviewer outside the copy" | "Isolation is handled" |
|
|
30
|
+
|
|
31
|
+
Half the value arrives before the tool runs: a claim you cannot phrase falsifiably is usually one you have not
|
|
32
|
+
actually checked.
|
|
33
|
+
|
|
34
|
+
**Read the verdicts as written.** `confirmed` means it was actively checked. `unclear` means the reader could not
|
|
35
|
+
tell — which is *not* a pass, and is worth a second look at whether the claim was answerable. `refuted` means stop:
|
|
36
|
+
fix it, and when you fix it, add the test that would have caught it, so it cannot come back.
|
|
37
|
+
|
|
38
|
+
State the refutations to the user in the reader's own words. A second opinion that you summarise into agreement is
|
|
39
|
+
not a second opinion.
|
|
40
|
+
|
|
41
|
+
## When this is worth it
|
|
42
|
+
|
|
43
|
+
Fan out when the work splits into pieces that touch **different files** and can be described precisely. Two lines
|
|
44
|
+
that need the same file are one line, or two lines in sequence. A single edit you can make in a minute is not a
|
|
45
|
+
mission; making it yourself is faster and better.
|
|
46
|
+
|
|
47
|
+
Good shapes:
|
|
48
|
+
|
|
49
|
+
| Shape | Seat | Scope |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| Map a system, find bugs, write a spec | auditor (read-only) | no write scope |
|
|
52
|
+
| Implement a well-specified change with tests | builder | the files it owns |
|
|
53
|
+
| Write tests for a rule before it exists | tester | the test files |
|
|
54
|
+
| One server contract, several clients | builders in parallel | one area each |
|
|
55
|
+
|
|
56
|
+
## The loop
|
|
57
|
+
|
|
58
|
+
1. `repo_overview` and `seats` before planning. No ready seat, no mission.
|
|
59
|
+
2. Write the plan. Per line: a narrow write scope, a prompt that names files, an example, the project's check
|
|
60
|
+
commands and "do not commit", and the seat that suits the work. Risky or subtle work goes to the strongest seat;
|
|
61
|
+
mechanical work to the cheapest. Claude is opt-in, because the lead already spends that subscription.
|
|
62
|
+
3. `plan_check`. It refuses overlapping scopes, missing seats, dangerous flags and uncommitted work inside a scope.
|
|
63
|
+
Fix the plan rather than arguing with the gate.
|
|
64
|
+
4. `launch`. Tell the user the mission id and what each line will do.
|
|
65
|
+
5. `mission_status` while it runs; review each run with `run_diff` as it finishes rather than all at the end.
|
|
66
|
+
6. Report per run: what changed, what you would keep, what you would not, what you could not verify. The user
|
|
67
|
+
decides what merges.
|
|
68
|
+
|
|
69
|
+
## Writing a prompt an agent can actually follow
|
|
70
|
+
|
|
71
|
+
Name the files. Give one example of input and expected output. State the invariants it must not break. Give the
|
|
72
|
+
exact check commands. Say "do not commit, do not create branches". Ask for a final message that lists what changed,
|
|
73
|
+
what it deliberately left alone, and the test counts — and say plainly when a sandbox could not run a check.
|
|
74
|
+
|
|
75
|
+
Agents do better with a checklist than with a goal.
|
|
76
|
+
|
|
77
|
+
## Reviewing what comes back
|
|
78
|
+
|
|
79
|
+
- Read the diff, not the report. `run_diff` reads the workspace itself; the report is the agent's own account.
|
|
80
|
+
- Look first at anything written **outside its declared scope**. That is where the surprises are.
|
|
81
|
+
- Check the invariants yourself: money, auth, migrations, anything that picks a default value, anything that could
|
|
82
|
+
leak a real value into a test.
|
|
83
|
+
- A bug fix needs a test that fails on the old code. If it does not have one, that is the rework.
|
|
84
|
+
- Never present an agent's "tests pass" as your own verification. Run the project's checks yourself after applying.
|
|
85
|
+
|
|
86
|
+
## Bringing a run home
|
|
87
|
+
|
|
88
|
+
When an agent finishes, the gate is four tools in order, and each one records the revision it judged. If the work
|
|
89
|
+
changes between any two of them, the later ones refuse — which is the point.
|
|
90
|
+
|
|
91
|
+
1. **`run_diff`** — read it yourself, line by line, before anything else. An agent's report is what it believes it
|
|
92
|
+
did.
|
|
93
|
+
2. **`review_run`** — your verdict, and *what you actually checked*. "Looks fine" is not a review. `rework` sends
|
|
94
|
+
it back to the same session; `reject` ends the line.
|
|
95
|
+
3. **`rework_run`** if you asked for changes — it continues the same conversation in the same worktree, so the
|
|
96
|
+
agent still has its own reasoning about the code. Running the line again instead throws that away and costs the
|
|
97
|
+
same. Two rounds; after that, decide rather than asking a third time.
|
|
98
|
+
4. **`run_checks`** — the project's own commands, run by the gate rather than reported by the agent. A line that
|
|
99
|
+
declared no checks comes back **unverified**, which is not the same as passing.
|
|
100
|
+
5. **`prove_fix`** — only for a line the plan marked `fixesBug`. The test goes onto the *old* code and must fail
|
|
101
|
+
there. If it passes, it would have passed before the fix.
|
|
102
|
+
6. **Ask the user**, in the chat, in their own words. Then **`merge_run`** with what they said.
|
|
103
|
+
|
|
104
|
+
You cannot merge your own way past any of this: `merge_run` asks a pure function over recorded facts, and reports
|
|
105
|
+
the refusals rather than working around them. Read them out to the user as written.
|
|
106
|
+
|
|
107
|
+
**Never call `merge_run` without having asked.** The tool records the user as the authority, and a replay months
|
|
108
|
+
from now will show that. Putting words in their mouth there is the worst thing you can do with these tools.
|
|
109
|
+
|
|
110
|
+
## The claim loop, in full
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
claim → refuted → failing test → fix → checked again → confirmed
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The middle step is the one people skip. A refutation you fix without a test is a bug you will write again; a
|
|
117
|
+
refutation you fix *with* a test that fails on the old code is the project's fourth non-negotiable, satisfied
|
|
118
|
+
without anybody having to be reminded of it.
|
|
119
|
+
|
|
120
|
+
## What this skill will not do
|
|
121
|
+
|
|
122
|
+
- It will not merge. Applying a diff is the user's decision, and the merge gate (with review, checks and proof)
|
|
123
|
+
arrives in a later milestone.
|
|
124
|
+
- It will not work around a seat's usage limit. A seat that is out of quota is out; say so and use another.
|
|
125
|
+
- It will not hand an agent your secrets: workspaces exclude ignored and deny-listed files, and a run gets an
|
|
126
|
+
allowlisted environment only.
|
package/dist/cli.d.ts
DELETED
package/dist/cli.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/demo.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { PlanLine } from "fanout-core";
|
|
2
|
-
import type { ScenarioInput } from "fanout-adapter-fake";
|
|
3
|
-
/** A small repository worth changing: three areas, one seeded bug, one commit. */
|
|
4
|
-
export declare function buildDemoRepo(root: string): string;
|
|
5
|
-
export declare const DEMO_GOAL = "Add CSV export and fix the December date bug";
|
|
6
|
-
/**
|
|
7
|
-
* Three lines that touch three different areas.
|
|
8
|
-
*
|
|
9
|
-
* One of them is marked `fixesBug`, which is the flag the merge gate holds to the fourth non-negotiable: that
|
|
10
|
-
* line cannot merge without a test proven to fail on the old code. The demo exists partly to show that refusal.
|
|
11
|
-
*/
|
|
12
|
-
export declare function demoLines(): PlanLine[];
|
|
13
|
-
/**
|
|
14
|
-
* What each simulated agent does, second by second.
|
|
15
|
-
*
|
|
16
|
-
* `timeScale` is the only dishonesty about time and it is the useful kind: a real run takes minutes and nobody
|
|
17
|
-
* watches a demo for minutes. The phases, the tool calls and the files are what a real run of this shape does.
|
|
18
|
-
*/
|
|
19
|
-
export declare function demoScenario(line: PlanLine): ScenarioInput;
|
|
20
|
-
/**
|
|
21
|
-
* The claim check the demo shows, written here rather than asked of anyone.
|
|
22
|
-
*
|
|
23
|
-
* Marked `simulated` all the way through to the screen. A real check needs a real second vendor and a
|
|
24
|
-
* subscription; inventing one and presenting it as read would be faking the single thing this product claims to
|
|
25
|
-
* do, which is worse than having no demo at all.
|
|
26
|
-
*/
|
|
27
|
-
export declare function demoClaims(): {
|
|
28
|
-
claim: string;
|
|
29
|
-
verdict: "confirmed" | "refuted" | "unclear";
|
|
30
|
-
evidence: string;
|
|
31
|
-
}[];
|
|
32
|
-
//# sourceMappingURL=demo.d.ts.map
|
package/dist/demo.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../src/demo.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAezD,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoClD;AAED,eAAO,MAAM,SAAS,iDAAiD,CAAC;AAExE;;;;;GAKG;AACH,wBAAgB,SAAS,IAAI,QAAQ,EAAE,CA2CtC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,aAAa,CAoF1D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;CAClB,EAAE,CAkBF"}
|
package/dist/demo.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
/*
|
|
5
|
-
* A whole mission, on a machine with no accounts on it.
|
|
6
|
-
*
|
|
7
|
-
* Everything here is the real thing except the thinking. Real git worktrees, the real safety gate, the real
|
|
8
|
-
* append-only ledger, real diffs collected from real files — driven by the `fake` seat, which is a genuine CLI
|
|
9
|
-
* speaking the genuine protocol and taking a script instead of a model. Nothing is stubbed out inside the daemon,
|
|
10
|
-
* because a demo that exercised a special path would be a demo of something nobody ships.
|
|
11
|
-
*
|
|
12
|
-
* It is honest about the one thing it cannot do. A cold reader's verdicts need a real second vendor; the demo's
|
|
13
|
-
* are written here, and every surface that shows them says `simulated` out loud. Faking the one claim the product
|
|
14
|
-
* makes would be the single most dishonest thing this repository could contain.
|
|
15
|
-
*/
|
|
16
|
-
/** A small repository worth changing: three areas, one seeded bug, one commit. */
|
|
17
|
-
export function buildDemoRepo(root) {
|
|
18
|
-
rmSync(root, { recursive: true, force: true });
|
|
19
|
-
mkdirSync(join(root, "src", "api"), { recursive: true });
|
|
20
|
-
mkdirSync(join(root, "src", "ui"), { recursive: true });
|
|
21
|
-
mkdirSync(join(root, "docs"), { recursive: true });
|
|
22
|
-
writeFileSync(join(root, "src", "api", "orders.ts"), "export interface Order {\n id: string;\n total: number;\n}\n\n" +
|
|
23
|
-
"export function ordersFor(customer: string): Order[] {\n return [];\n}\n");
|
|
24
|
-
writeFileSync(join(root, "src", "api", "dates.ts"), "/** Formats a day. Off by one in December: the bug this demo fixes. */\n" +
|
|
25
|
-
"export function monthOf(date: Date): number {\n return date.getMonth();\n}\n");
|
|
26
|
-
writeFileSync(join(root, "src", "ui", "table.ts"), "export const columns = ['id', 'total'];\n");
|
|
27
|
-
writeFileSync(join(root, "docs", "orders.md"), "# Orders\n\nThe orders API.\n");
|
|
28
|
-
writeFileSync(join(root, "package.json"), `${JSON.stringify({ name: "demo-shop", private: true, scripts: { check: "echo ok" } }, null, 2)}\n`);
|
|
29
|
-
const git = (args) => {
|
|
30
|
-
execFileSync("git", args, {
|
|
31
|
-
cwd: root,
|
|
32
|
-
stdio: "ignore",
|
|
33
|
-
env: { PATH: process.env["PATH"] ?? "", HOME: process.env["HOME"] ?? "", GIT_CONFIG_NOSYSTEM: "1" },
|
|
34
|
-
});
|
|
35
|
-
};
|
|
36
|
-
git(["init", "--quiet", "-b", "main"]);
|
|
37
|
-
git(["config", "user.email", "demo@example.invalid"]);
|
|
38
|
-
git(["config", "user.name", "Fanout demo"]);
|
|
39
|
-
git(["add", "-A"]);
|
|
40
|
-
git(["commit", "--quiet", "-m", "the shop, before the crew arrives"]);
|
|
41
|
-
return root;
|
|
42
|
-
}
|
|
43
|
-
export const DEMO_GOAL = "Add CSV export and fix the December date bug";
|
|
44
|
-
/**
|
|
45
|
-
* Three lines that touch three different areas.
|
|
46
|
-
*
|
|
47
|
-
* One of them is marked `fixesBug`, which is the flag the merge gate holds to the fourth non-negotiable: that
|
|
48
|
-
* line cannot merge without a test proven to fail on the old code. The demo exists partly to show that refusal.
|
|
49
|
-
*/
|
|
50
|
-
export function demoLines() {
|
|
51
|
-
return [
|
|
52
|
-
{
|
|
53
|
-
id: "api",
|
|
54
|
-
title: "CSV export endpoint",
|
|
55
|
-
role: "builder",
|
|
56
|
-
prompt: "Add GET /orders.csv, streaming rows and escaping quotes.",
|
|
57
|
-
seat: { id: "fake", model: "demo" },
|
|
58
|
-
// Narrowed to the file it writes. `src/api/**` swallowed the dates line's scope, and the safety gate
|
|
59
|
-
// refused the plan — on the product's own demo, which is the best argument for the check there is.
|
|
60
|
-
scope: { write: ["src/api/csv.ts"] },
|
|
61
|
-
dependsOn: [],
|
|
62
|
-
checks: ["npm run check"],
|
|
63
|
-
fixesBug: false,
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: "dates",
|
|
67
|
-
title: "Fix the December month bug",
|
|
68
|
-
role: "builder",
|
|
69
|
-
prompt: "monthOf() is off by one in December. Fix it and prove it with a test.",
|
|
70
|
-
seat: { id: "fake", model: "demo" },
|
|
71
|
-
scope: { write: ["src/api/dates.ts", "src/api/dates.test.ts"] },
|
|
72
|
-
dependsOn: [],
|
|
73
|
-
checks: ["npm run check"],
|
|
74
|
-
fixesBug: true,
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
id: "ui",
|
|
78
|
-
title: "Export button",
|
|
79
|
-
role: "builder",
|
|
80
|
-
/*
|
|
81
|
-
* The one line that asks for a seat this machine does not have. Nothing here fakes the consequence: the
|
|
82
|
-
* demo's crew really is the simulated seat alone, so the router really does move this line and really does
|
|
83
|
-
* say why — which is the behaviour worth showing, and the only honest way to show it offline.
|
|
84
|
-
*/
|
|
85
|
-
prompt: "Add the export column and a button that hits the new endpoint.",
|
|
86
|
-
seat: { id: "codex", model: "gpt-5-codex" },
|
|
87
|
-
scope: { write: ["src/ui/**"] },
|
|
88
|
-
dependsOn: [],
|
|
89
|
-
checks: ["npm run check"],
|
|
90
|
-
fixesBug: false,
|
|
91
|
-
},
|
|
92
|
-
];
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* What each simulated agent does, second by second.
|
|
96
|
-
*
|
|
97
|
-
* `timeScale` is the only dishonesty about time and it is the useful kind: a real run takes minutes and nobody
|
|
98
|
-
* watches a demo for minutes. The phases, the tool calls and the files are what a real run of this shape does.
|
|
99
|
-
*/
|
|
100
|
-
export function demoScenario(line) {
|
|
101
|
-
const scenarios = {
|
|
102
|
-
api: {
|
|
103
|
-
steps: [
|
|
104
|
-
{ phase: "reading", delayMs: 900 },
|
|
105
|
-
{ tool: "read", summary: "src/api/orders.ts", delayMs: 700 },
|
|
106
|
-
{ phase: "coding", delayMs: 600 },
|
|
107
|
-
{
|
|
108
|
-
tool: "edit",
|
|
109
|
-
summary: "add the csv writer",
|
|
110
|
-
delayMs: 1400,
|
|
111
|
-
write: {
|
|
112
|
-
"src/api/csv.ts": "import type { Order } from './orders.ts';\n\n" +
|
|
113
|
-
"/** One row per order. A quote inside a field is doubled, per RFC 4180. */\n" +
|
|
114
|
-
"export function toCsv(orders: Order[]): string {\n" +
|
|
115
|
-
" const rows = orders.map((order) => `${quote(order.id)},${order.total}`);\n" +
|
|
116
|
-
" return ['id,total', ...rows].join('\\n');\n}\n\n" +
|
|
117
|
-
"function quote(value: string): string {\n" +
|
|
118
|
-
" return value.includes(',') || value.includes('\"')\n" +
|
|
119
|
-
' ? `"${value.split(\'"\').join(\'""\')}"`\n : value;\n}\n',
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
{ phase: "testing", delayMs: 900 },
|
|
123
|
-
{ tool: "shell", summary: "npm run check", delayMs: 1100 },
|
|
124
|
-
{ usage: 3 },
|
|
125
|
-
{ phase: "reporting", delayMs: 400 },
|
|
126
|
-
],
|
|
127
|
-
report: "Added toCsv() with RFC 4180 quoting. Streaming is left for a follow-up.",
|
|
128
|
-
timeScale: 1,
|
|
129
|
-
},
|
|
130
|
-
dates: {
|
|
131
|
-
steps: [
|
|
132
|
-
{ phase: "reading", delayMs: 800 },
|
|
133
|
-
{ phase: "coding", delayMs: 900 },
|
|
134
|
-
{
|
|
135
|
-
tool: "edit",
|
|
136
|
-
summary: "months are zero-based",
|
|
137
|
-
delayMs: 1200,
|
|
138
|
-
write: {
|
|
139
|
-
"src/api/dates.ts": "/** Formats a day. getMonth() is zero-based, which is where December went wrong. */\n" +
|
|
140
|
-
"export function monthOf(date: Date): number {\n return date.getMonth() + 1;\n}\n",
|
|
141
|
-
"src/api/dates.test.ts": "import { monthOf } from './dates.ts';\n\n" +
|
|
142
|
-
"// Fails on the old code: it returned 11 for December.\n" +
|
|
143
|
-
"test('December is the twelfth month', () => {\n" +
|
|
144
|
-
" expect(monthOf(new Date('2026-12-01'))).toBe(12);\n});\n",
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
{ phase: "testing", delayMs: 1000 },
|
|
148
|
-
{ tool: "shell", summary: "npm run check", delayMs: 900 },
|
|
149
|
-
{ usage: 2 },
|
|
150
|
-
{ phase: "reporting", delayMs: 400 },
|
|
151
|
-
],
|
|
152
|
-
report: "monthOf() was zero-based. Fixed, with a test that fails on the old code.",
|
|
153
|
-
timeScale: 1,
|
|
154
|
-
},
|
|
155
|
-
ui: {
|
|
156
|
-
steps: [
|
|
157
|
-
{ phase: "reading", delayMs: 1000 },
|
|
158
|
-
{ phase: "coding", delayMs: 1500 },
|
|
159
|
-
{
|
|
160
|
-
tool: "edit",
|
|
161
|
-
summary: "export column and button",
|
|
162
|
-
delayMs: 1600,
|
|
163
|
-
write: {
|
|
164
|
-
"src/ui/table.ts": "export const columns = ['id', 'total', 'export'];\n",
|
|
165
|
-
"src/ui/export-button.ts": "export function exportButton(): string {\n" +
|
|
166
|
-
" return '<button data-href=\"/orders.csv\">Export CSV</button>';\n}\n",
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
{ usage: 4 },
|
|
170
|
-
{ phase: "reporting", delayMs: 600 },
|
|
171
|
-
],
|
|
172
|
-
report: "Added the column and the button.",
|
|
173
|
-
timeScale: 1,
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
const scenario = scenarios[line.id];
|
|
177
|
-
if (scenario === undefined)
|
|
178
|
-
throw new Error(`the demo has no script for line "${line.id}"`);
|
|
179
|
-
return scenario;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* The claim check the demo shows, written here rather than asked of anyone.
|
|
183
|
-
*
|
|
184
|
-
* Marked `simulated` all the way through to the screen. A real check needs a real second vendor and a
|
|
185
|
-
* subscription; inventing one and presenting it as read would be faking the single thing this product claims to
|
|
186
|
-
* do, which is worse than having no demo at all.
|
|
187
|
-
*/
|
|
188
|
-
export function demoClaims() {
|
|
189
|
-
return [
|
|
190
|
-
{
|
|
191
|
-
claim: "The December fix comes with a test that fails on the old code",
|
|
192
|
-
verdict: "confirmed",
|
|
193
|
-
evidence: "dates.test.ts expects 12; the old monthOf returned 11 · src/api/dates.ts:3",
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
claim: "Nothing outside the three declared scopes was touched",
|
|
197
|
-
verdict: "confirmed",
|
|
198
|
-
evidence: "every changed path falls inside a declared write scope",
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
claim: "toCsv escapes every field that needs it",
|
|
202
|
-
verdict: "refuted",
|
|
203
|
-
evidence: "a field containing a newline is not quoted · src/api/csv.ts:10",
|
|
204
|
-
},
|
|
205
|
-
];
|
|
206
|
-
}
|
|
207
|
-
//# sourceMappingURL=demo.js.map
|