fapony 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 +473 -0
- package/fapony.ts +78 -0
- package/package.json +42 -0
- package/skill/git-commit-conventional/SKILL.md +68 -0
- package/skill/git-ship/SKILL.md +144 -0
- package/skill/move-to-done/SKILL.md +126 -0
- package/skill/plan-with-pony/SKILL.md +263 -0
- package/skill/review-pony/SKILL.md +254 -0
- package/src/analyze.ts +517 -0
- package/src/context/index.ts +11 -0
- package/src/context/projectHealth.ts +359 -0
- package/src/conventions-seed.ts +420 -0
- package/src/db/defaults.ts +26 -0
- package/src/db/getters.ts +33 -0
- package/src/db/index.ts +7 -0
- package/src/db/load.ts +57 -0
- package/src/db/store.ts +286 -0
- package/src/db/types.ts +79 -0
- package/src/debt.ts +667 -0
- package/src/digest/cli.ts +75 -0
- package/src/digest/collect.ts +625 -0
- package/src/digest/html.ts +208 -0
- package/src/digest/text.ts +191 -0
- package/src/gate.ts +153 -0
- package/src/gates.ts +194 -0
- package/src/hook.ts +436 -0
- package/src/init-mem.ts +71 -0
- package/src/init.ts +237 -0
- package/src/install/claude.ts +361 -0
- package/src/install/codex.ts +61 -0
- package/src/install/cursor.ts +167 -0
- package/src/install/detect.ts +78 -0
- package/src/install/opencode.ts +234 -0
- package/src/install/skills.ts +106 -0
- package/src/install/types.ts +69 -0
- package/src/install/utils.ts +29 -0
- package/src/install/zcode.ts +120 -0
- package/src/install.ts +176 -0
- package/src/lint-baseline.ts +260 -0
- package/src/map.ts +320 -0
- package/src/math.ts +13 -0
- package/src/mcp/evidence.ts +332 -0
- package/src/mcp/primitives.ts +316 -0
- package/src/mcp/tools/check.ts +243 -0
- package/src/mcp/tools/collect.ts +157 -0
- package/src/mcp/tools/context.ts +66 -0
- package/src/mcp/tools/index.ts +309 -0
- package/src/mcp/tools/mem.ts +95 -0
- package/src/mcp/tools/plans.ts +255 -0
- package/src/mcp/tools/report.ts +285 -0
- package/src/mcp/tools/stats.ts +96 -0
- package/src/mcp/tools/usage.ts +211 -0
- package/src/mcp/tools/verdict.ts +148 -0
- package/src/mcp/transport.ts +241 -0
- package/src/mcp/types.ts +54 -0
- package/src/mcp/worktree.ts +27 -0
- package/src/memory.ts +264 -0
- package/src/parse.ts +71 -0
- package/src/plan-seed.ts +599 -0
- package/src/price/fetch.ts +146 -0
- package/src/price/index.ts +8 -0
- package/src/price/resolve.ts +213 -0
- package/src/report/cli.ts +92 -0
- package/src/report/format.ts +37 -0
- package/src/report/index.ts +4 -0
- package/src/report/render.ts +206 -0
- package/src/review-seed.ts +932 -0
- package/src/safety.ts +18 -0
- package/src/session/activeSession.ts +153 -0
- package/src/session/claude-code.ts +412 -0
- package/src/session/codex.ts +347 -0
- package/src/session/findModel.ts +376 -0
- package/src/session/helpers.ts +640 -0
- package/src/session/index.ts +31 -0
- package/src/session/opencode.ts +167 -0
- package/src/session/registry.ts +45 -0
- package/src/session/types.ts +128 -0
- package/src/session/zcode.ts +151 -0
- package/src/setup.ts +242 -0
- package/src/stats/cli.ts +44 -0
- package/src/stats/data.ts +1019 -0
- package/src/stats/format.ts +584 -0
- package/src/stats/index.ts +19 -0
- package/src/telemetry.ts +364 -0
- package/src/test.ts +2 -0
- package/src/update.ts +212 -0
- package/src/usage/cache.ts +125 -0
- package/src/usage/cli.ts +120 -0
- package/src/usage/format.ts +29 -0
- package/src/usage/index.ts +4 -0
- package/src/usage/render.ts +523 -0
- package/src/usage/scan.ts +161 -0
- package/src/util.ts +32 -0
- package/src/web/html.ts +33 -0
- package/templates/PLAN.md +90 -0
- package/templates/SPEC.md +30 -0
- package/templates/mem/commands/plan.ts +360 -0
- package/templates/mem/commands/read.ts +194 -0
- package/templates/mem/commands/rotate.ts +59 -0
- package/templates/mem/commands/selftest.ts +450 -0
- package/templates/mem/commands/write.ts +214 -0
- package/templates/mem/mem.ts +68 -0
- package/templates/mem/render.ts +63 -0
- package/templates/mem/selectors.ts +144 -0
- package/templates/mem/store.ts +285 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-commit-conventional
|
|
3
|
+
description: Commit split by concern with conventional message — use with Claude Code / OpenCode / Codex / ZCode. Trigger on /git-commit and when the user asks to commit changes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git Commit Conventional — split by concern
|
|
7
|
+
|
|
8
|
+
**Hard rule: 1 commit per concern.** Never bundle unrelated changes.
|
|
9
|
+
|
|
10
|
+
Splitting is not only for the reader: a repo whose commits each carry one concern is the
|
|
11
|
+
only kind whose history can be mined for convention migrations (a commit that deletes
|
|
12
|
+
`A(` and adds `B(` is evidence that `B` replaced `A`). Measured on a real repo
|
|
13
|
+
2026-09-18: restricting the mine to commits touching 10+ files erased every real
|
|
14
|
+
convention it had found, and loosening the matcher to compensate produced only noise.
|
|
15
|
+
Bundled commits destroy that signal permanently — history cannot be re-split later.
|
|
16
|
+
|
|
17
|
+
## Before commit
|
|
18
|
+
|
|
19
|
+
1. `git status --porcelain` — in a shared/multi-agent worktree (fapony's `wt-*`), files already
|
|
20
|
+
dirty before this session started are another session's in-progress work, not a problem.
|
|
21
|
+
STOP and report only if a file changes between two consecutive `git status` calls (someone is
|
|
22
|
+
writing right now — wait for it to settle), or if content matches nothing in this
|
|
23
|
+
conversation and doesn't look like a coherent feature.
|
|
24
|
+
2. `git diff --stat HEAD` — see what changed
|
|
25
|
+
3. Split by type — `feat` `fix` `refactor` `docs` `chore` `test` — one commit each.
|
|
26
|
+
`git add -p` is unavailable here (interactive flags unsupported), so split at file
|
|
27
|
+
granularity. If one file mixes your work with someone else's, put it with the concern it
|
|
28
|
+
mostly belongs to and say so in the body — don't force a line-level split you can't do safely.
|
|
29
|
+
|
|
30
|
+
## Format
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
<type>(<scope>): <subject, max 72 chars>
|
|
34
|
+
|
|
35
|
+
<body — what changed and why, wrapped at 76>
|
|
36
|
+
|
|
37
|
+
Ref <PLAN-file, if any>
|
|
38
|
+
|
|
39
|
+
Co-Authored-By: <the model you are running as> <its vendor's noreply address>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
If your harness already gave you an exact `Co-Authored-By` line, use that verbatim — it wins over
|
|
43
|
+
this template. Otherwise name the model you actually are; never copy another vendor's address.
|
|
44
|
+
|
|
45
|
+
`<type>` is exactly one of the six words above — no `feat/fix:`, no two-word types.
|
|
46
|
+
|
|
47
|
+
### Example
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
feat(analyze): structural health diagnosis
|
|
51
|
+
|
|
52
|
+
Reports hub / orphan / cycle / changed-untested files from an import
|
|
53
|
+
graph built live with Bun.Transpiler.scan(). Nothing persisted: 114
|
|
54
|
+
files scan in 16.6ms, so a cache table would be pure debt.
|
|
55
|
+
|
|
56
|
+
Ref PLAN-analyze.md
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Rules
|
|
60
|
+
|
|
61
|
+
- **NEVER push from this skill** — it only commits. Push/PR/merge is [git-ship](../git-ship/SKILL.md)'s job
|
|
62
|
+
- **Report in lines, not paragraphs** — what you did, what needs the user, nothing else. Never
|
|
63
|
+
narrate the steps; the commands are already in the transcript.
|
|
64
|
+
- **NEVER `--amend`** an existing commit unless explicitly authorized
|
|
65
|
+
- **NEVER `--no-verify`** — if a pre-commit hook (lint/typecheck) fails on pre-existing code you
|
|
66
|
+
didn't write, fix it for real (safe autofix + minimal manual fix) and note in the body that
|
|
67
|
+
the fix wasn't for your own change
|
|
68
|
+
- Conflict with the base branch → STOP and report, don't merge yourself
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-ship
|
|
3
|
+
description: Ship a branch end to end — push it, open a PR with an AI-drafted title/body, merge it, then bring the branch back in line with the base. Stops at the PR for team review with `pr`, lands an approved one with `land`. Use with Claude Code. Trigger on /git-ship, /ship, /pr, and when the user asks to open a PR, ship a branch, or merge for them.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git Ship — push, PR, merge, realign
|
|
7
|
+
|
|
8
|
+
You are shipping a branch: push it, open a PR with a drafted title/body, merge it, put the
|
|
9
|
+
branch back in line with the base.
|
|
10
|
+
Commits should already be split by concern — see [git-commit-conventional](../git-commit-conventional/SKILL.md)
|
|
11
|
+
if they aren't yet.
|
|
12
|
+
|
|
13
|
+
## Where to stop
|
|
14
|
+
|
|
15
|
+
The argument picks the stop point. Everything else on this page is identical in all three.
|
|
16
|
+
|
|
17
|
+
| Invocation | Does | For |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `/git-ship` | push → PR → merge → realign | working solo, or a branch nobody else reviews |
|
|
20
|
+
| `/git-ship pr` | push → PR, then **stop** and report the URL | a team — someone else reviews and merges |
|
|
21
|
+
| `/git-ship land` | the PR exists and is approved → merge → realign | a team, after approval lands |
|
|
22
|
+
|
|
23
|
+
No argument, and the repo's default branch requires a review to merge
|
|
24
|
+
(`gh api repos/{owner}/{repo}/branches/<default> --jq '.protection.required_pull_request_reviews'`
|
|
25
|
+
returns anything but `null`)? Treat it as `pr` and say why — merging is not yours to do when the
|
|
26
|
+
repo says a human must approve first. `--admin` stays off the table either way.
|
|
27
|
+
|
|
28
|
+
For `pr`, stop after `gh pr create` and report the URL, plus who needs to review if the repo
|
|
29
|
+
declares owners. Don't merge, don't realign — the branch must stay as the reviewer sees it.
|
|
30
|
+
|
|
31
|
+
For `land`, skip drafting: the PR is already written. Start at the CI gate below, then merge and
|
|
32
|
+
realign. Re-read the PR body first and say in one line whether it still matches the commits, since
|
|
33
|
+
review may have added some.
|
|
34
|
+
|
|
35
|
+
## Before anything
|
|
36
|
+
|
|
37
|
+
1. `git status --porcelain` — uncommitted changes? STOP, ask whether to commit first
|
|
38
|
+
2. `git branch --show-current` — refuse if this is `main`/`master` (or the repo's default branch):
|
|
39
|
+
tell the user to branch first
|
|
40
|
+
3. `git log <default-branch>..HEAD --oneline` — the commits this PR will contain
|
|
41
|
+
|
|
42
|
+
## Draft
|
|
43
|
+
|
|
44
|
+
- **PR title**: one line, conventional-commit style (`feat: ...`, `fix: ...`), summarizing the
|
|
45
|
+
whole branch — not just the last commit
|
|
46
|
+
- **PR body**: short summary of what changed and why (from the commit messages + diff, not
|
|
47
|
+
invented), plus a one-line test plan if there's an obvious one (tests run, command output).
|
|
48
|
+
End with an attribution line:
|
|
49
|
+
```
|
|
50
|
+
🤖 Generated with <the coding agent you are running in>
|
|
51
|
+
```
|
|
52
|
+
Name the agent you actually are — Claude Code, Codex, opencode, ZCode — not a hardcoded one.
|
|
53
|
+
If your harness already gave you an exact attribution line to use, use that verbatim instead;
|
|
54
|
+
it wins over this template.
|
|
55
|
+
- **Merge method**: default to a regular merge (`--merge`). Squash only when the branch is
|
|
56
|
+
throwaway *and* its commits are noise — `wip`, `fix typo`, `try again`. Two reasons the
|
|
57
|
+
default runs this way:
|
|
58
|
+
- **Squash throws away the split.** If the commits are already one concern each, squashing
|
|
59
|
+
collapses them into a single commit on the base, and `git blame` then answers every line
|
|
60
|
+
with a message about the whole branch. The work of splitting them is undone at merge time,
|
|
61
|
+
every time.
|
|
62
|
+
- **Squash puts a step you can forget in the critical path.** It writes a *new* commit on the
|
|
63
|
+
base, so the branch's own commits are never ancestors of it — the two diverge, and every
|
|
64
|
+
later PR opens with a conflict nobody caused. A regular merge carries the commits over as
|
|
65
|
+
they are, so there is nothing to remember.
|
|
66
|
+
|
|
67
|
+
A long-lived branch (`dev`, `develop`) that someone keeps checked out is the strongest case
|
|
68
|
+
for `--merge`: it is exactly where a forgotten realign hurts, and it never gets deleted, so
|
|
69
|
+
the divergence compounds.
|
|
70
|
+
|
|
71
|
+
## Confirm once, then run
|
|
72
|
+
|
|
73
|
+
Show the drafted title + body + merge method together and get one go-ahead — don't ask
|
|
74
|
+
separately for push, then PR, then merge. Then:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git status --porcelain # re-check right before pushing, not just at the start —
|
|
78
|
+
# time passed drafting/waiting on CI; in a shared/multi-agent
|
|
79
|
+
# worktree another session may have added uncommitted work.
|
|
80
|
+
# If dirty, wait for it to be committed (or ask) before pushing —
|
|
81
|
+
# don't push around it and don't commit someone else's changes yourself.
|
|
82
|
+
git push -u origin <branch>
|
|
83
|
+
gh pr create --title "<title>" --body "<body>"
|
|
84
|
+
gh pr merge --merge # or --squash, per the chosen method
|
|
85
|
+
|
|
86
|
+
# post-merge, after --merge: the branch is already an ancestor of the base, so just
|
|
87
|
+
# catch it up. No force, nothing to destroy.
|
|
88
|
+
git fetch origin
|
|
89
|
+
git merge --ff-only origin/<default-branch>
|
|
90
|
+
git push origin <branch>
|
|
91
|
+
|
|
92
|
+
# post-merge, after --squash ONLY — and then it is mandatory, not optional:
|
|
93
|
+
git fetch origin
|
|
94
|
+
git reset --hard origin/<default-branch>
|
|
95
|
+
git push --force-with-lease origin <branch>
|
|
96
|
+
|
|
97
|
+
# if you shipped from a git worktree, fast-forward the primary checkout too —
|
|
98
|
+
# `git worktree list` prints it first:
|
|
99
|
+
git -C <primary-checkout> pull --ff-only
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Either way the branch must end up pointing at the merged base — that is what keeps the next PR
|
|
103
|
+
clean. How much work that takes is decided by the merge method, which is the practical reason the
|
|
104
|
+
default is `--merge`: it is a fast-forward, no force, nothing that can go wrong if you get
|
|
105
|
+
distracted.
|
|
106
|
+
|
|
107
|
+
After a squash it is a `reset --hard`, and it is **not optional**. Squashing rewrites the commits,
|
|
108
|
+
so the branch keeps originals the base will never have — the two diverge a little more every ship,
|
|
109
|
+
and GitHub answers every later PR with *"Can't automatically merge"* even when the content is
|
|
110
|
+
identical. Skip it only for a throwaway feature branch you're about to delete. It force-pushes, so
|
|
111
|
+
say so — and check `git status --porcelain` is clean first (uncommitted work would be destroyed).
|
|
112
|
+
|
|
113
|
+
Verify before claiming either is done: `git merge-base --is-ancestor origin/<branch>
|
|
114
|
+
origin/<default-branch>` exits 0 when the branch holds nothing the base lacks.
|
|
115
|
+
|
|
116
|
+
The primary-checkout pull matters when anything outside the repo points *into* it — symlinked
|
|
117
|
+
skills, an editor workspace, a tool resolving its root from that path. Ship from a worktree and
|
|
118
|
+
that checkout silently falls a commit behind every time, serving yesterday's content. Only
|
|
119
|
+
`--ff-only`, and only when it's clean and on the default branch: report and leave it alone
|
|
120
|
+
otherwise, it isn't the branch you were asked to ship.
|
|
121
|
+
|
|
122
|
+
## Rules
|
|
123
|
+
|
|
124
|
+
- **Never push to `main`/`master` directly** — always via PR
|
|
125
|
+
- **Report in lines, not paragraphs** — what you did, what needs the user, nothing else. Never
|
|
126
|
+
narrate the steps; the commands are already in the transcript.
|
|
127
|
+
- **Never merge with failing CI** — check `gh pr checks` first; if red or pending, report and wait
|
|
128
|
+
- **Never `--admin` merge** (bypassing branch protection) unless the user explicitly says to
|
|
129
|
+
- **Never force-push** an existing PR branch without saying so first
|
|
130
|
+
- One PR per concern, same as commits — don't bundle unrelated branches into one PR
|
|
131
|
+
|
|
132
|
+
## If fail
|
|
133
|
+
|
|
134
|
+
- `gh` not authenticated → tell the user to run `gh auth login`
|
|
135
|
+
- `gh pr create` warns *"Can't automatically merge"* → that's the base's problem, not `gh`'s.
|
|
136
|
+
The PR still gets created. Don't stop there — go verify it as the next bullet says.
|
|
137
|
+
- Merge conflict with base branch → STOP, report, don't resolve unilaterally. Exception: a
|
|
138
|
+
long-lived branch squash-merged earlier that skipped the reset above — then the "conflict" is
|
|
139
|
+
squash history mismatch, not divergent content. Verify: `git diff origin/<base> HEAD --stat` versus
|
|
140
|
+
the diff of the branch's own unmerged commits (`git diff <first-unmerged>~1 HEAD --stat`).
|
|
141
|
+
Identical means base holds nothing the branch lacks, so `git merge origin/<base> -X ours` is
|
|
142
|
+
safe (branch wins every textual conflict, content is a strict superset) — say so, merge, then
|
|
143
|
+
do the post-merge reset so it stops recurring. Not identical → STOP and report, don't guess.
|
|
144
|
+
- CI red → report which check failed, don't merge, don't retry blindly
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: move-to-done
|
|
3
|
+
description: Move a shipped PLAN to .fapony/done/. Trigger on /move-to-done and when the user asks to archive a completed plan.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Move to Done — archive PLAN after ship
|
|
7
|
+
|
|
8
|
+
You are about to move a PLAN that has been shipped to the archive.
|
|
9
|
+
|
|
10
|
+
## Rules
|
|
11
|
+
|
|
12
|
+
0. **Report in lines, not paragraphs** — what moved, what the inbound-link sweep found, what
|
|
13
|
+
needs the user. Never narrate the steps; the commands are already in the transcript.
|
|
14
|
+
|
|
15
|
+
1. **PLAN must have shipped header** — regex: `^> ✅ \*\*.*shipped.*\*\*$`
|
|
16
|
+
If missing, add it yourself, don't ask — invoking this skill *is* the ship claim (the user
|
|
17
|
+
has already verified the work landed; this step is paperwork). Replace the plan's
|
|
18
|
+
status/header line with `> ✅ **shipped <YYYY-MM-DD>** (<hash>)` — today's date plus
|
|
19
|
+
`git rev-parse --short HEAD`. The date is load-bearing: it is the *only* record of when this
|
|
20
|
+
shipped that a later question can read, since the filename does not carry one.
|
|
21
|
+
Say in the summary that you stamped it, so a wrong HEAD is visible and correctable.
|
|
22
|
+
STOP only if there's no git repo / no commits to hash from.
|
|
23
|
+
|
|
24
|
+
1b. **A plan can also leave `plan/` without shipping** — it got absorbed into another plan, or the
|
|
25
|
+
redesign deleted the thing it planned. That is normal during a UI/UX sweep and is the main
|
|
26
|
+
reason `plan/` grows forever: there is no state for "dead" so it just sits there. Archive it
|
|
27
|
+
the same way, with two differences — header `> ⛔ **superseded by [PLAN-bar.md](PLAN-bar.md)**
|
|
28
|
+
(<date>)` instead of the shipped header, and frontmatter on the successor's side left alone
|
|
29
|
+
while this file gets:
|
|
30
|
+
```yaml
|
|
31
|
+
status: superseded
|
|
32
|
+
superseded_by: PLAN-bar.md
|
|
33
|
+
```
|
|
34
|
+
Then skip step 5 — no work shipped, so there is no verdict to record. Never archive a plan as
|
|
35
|
+
superseded on your own reading; the user says which plan replaced it.
|
|
36
|
+
|
|
37
|
+
A plan that is merely *waiting* (on a person, a customer, a decision) is **not** dead and does
|
|
38
|
+
not move — mark it `status: blocked` + `blocked_by: <what you are waiting for>` and leave it in
|
|
39
|
+
`plan/`, where `plan_list` will report it as blocked instead of as backlog.
|
|
40
|
+
|
|
41
|
+
2. **Check inbound links, then `git mv`** — `.fapony/done/` sits *beside* `.fapony/plan/`, at the
|
|
42
|
+
same depth, so every relative link *inside* the plan (`../spec/SPEC-x.md`, `../../src/...`)
|
|
43
|
+
keeps working untouched. Nothing to normalize. What does change is how *other plans* reach
|
|
44
|
+
this one — a sibling reference becomes a `done/` one:
|
|
45
|
+
```bash
|
|
46
|
+
grep -rln 'PLAN-foo.md' .fapony/plan/ .fapony/spec/ docs/ # who points at it
|
|
47
|
+
git mv .fapony/plan/PLAN-foo.md .fapony/done/PLAN-foo.md # same name, same depth
|
|
48
|
+
# in .fapony/plan/*.md: (PLAN-foo.md) -> (../done/PLAN-foo.md)
|
|
49
|
+
```
|
|
50
|
+
Fewer than 5 inbound files → fix them yourself · more → report the list.
|
|
51
|
+
|
|
52
|
+
**The filename gets no date prefix.** The ship date is already in the header (step 1), and
|
|
53
|
+
duplicating it into the name buys a sortable `ls` at the price of rewriting every inbound link
|
|
54
|
+
on every ship, forever. "What shipped on which day" is a question to derive, not to store:
|
|
55
|
+
```bash
|
|
56
|
+
grep -h 'shipped' .fapony/done/*.md | sort
|
|
57
|
+
```
|
|
58
|
+
If git refuses ("not under version control" — `.fapony/` is gitignored in
|
|
59
|
+
this repo), plain `mv` instead; there's nothing to commit for an untracked path, so skip
|
|
60
|
+
step 4 in that case.
|
|
61
|
+
|
|
62
|
+
3. **Leave the spec where it is** — `.fapony/spec/` is a reference library, not a queue. A spec
|
|
63
|
+
answers "how does this work", which is asked long after the plan that ordered it shipped, and
|
|
64
|
+
a spec that never moves is a link that never breaks. Nothing to do here; there is no
|
|
65
|
+
`spec/done/`.
|
|
66
|
+
|
|
67
|
+
4. **Commit split by concern** (only when the moved files are actually tracked by git):
|
|
68
|
+
```
|
|
69
|
+
chore(plan): archive PLAN-foo.md (shipped <hash>)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
5. **Record the verdict** — call the `verdict_submit` MCP tool (fapony) so this ship counts
|
|
73
|
+
toward what this project knows about the model that did the work. No `run_id` needed:
|
|
74
|
+
- `verdict`: `pass` (adjust if the ship had known rough edges — see VERDICT_GRADES)
|
|
75
|
+
- `regime`: **required** — the shape of the work that shipped: `code` for a feature or
|
|
76
|
+
refactor, `fix` for a bug fix, `plan` when what shipped was the plan or spec itself
|
|
77
|
+
- `reason_code`: **`none` when the ship was clean** — not `other`. `other` means "a real
|
|
78
|
+
problem none of the buckets name", so a clean ship filed there shows up in the
|
|
79
|
+
recurring-fail-reasons list and crowds out the reasons that mean something. Otherwise
|
|
80
|
+
`missing_test` / `scope_mismatch` / `unsafe_command` / `spec_gap` / `incomplete`, and
|
|
81
|
+
`other` (with a `note`, which it requires) only when a real finding fits none of them
|
|
82
|
+
- `note`: **omit it on a clean ship.** A verdict with no note still counts toward the plan
|
|
83
|
+
history future drafts read ("passed round 1 before"), but only notes reach the three
|
|
84
|
+
free-text slots `project_health_context` shows — so "clean ship" evicts a note that would
|
|
85
|
+
have taught the next session something. Write one only when this plan hit something a
|
|
86
|
+
reader could not get from the diff: what the symptom looked like, where the cause actually
|
|
87
|
+
was, and the rule that follows. Standalone prose — it is read months later with no access
|
|
88
|
+
to this conversation.
|
|
89
|
+
- `worktree`: **absolute path** to this repo/worktree (`git rev-parse --show-toplevel`) —
|
|
90
|
+
every other fapony tool (`fapony_usage`, `fapony_stats`, `project_health_context`)
|
|
91
|
+
scopes by absolute path too; a bare repo name won't match those queries
|
|
92
|
+
- `plan`: the archived plan's path (post-move, e.g. `.fapony/done/PLAN-foo.md`)
|
|
93
|
+
- `files`: repo-relative paths this plan touched (`git diff --name-only <base>..HEAD`) —
|
|
94
|
+
the only input to per-file risk history; without it the verdict says something happened
|
|
95
|
+
but not where
|
|
96
|
+
Skip only if fapony's MCP tools aren't available in this session — don't block the archive on it.
|
|
97
|
+
|
|
98
|
+
## Example
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
Input: .fapony/plan/PLAN-kickoff.md, no shipped header yet
|
|
102
|
+
Steps:
|
|
103
|
+
1. stamp header: > ✅ **shipped 2026-09-13** (a1b2c3)
|
|
104
|
+
2. inbound: README.md, .fapony/plan/PLAN-loop.md → (PLAN-kickoff.md) becomes (../done/PLAN-kickoff.md)
|
|
105
|
+
git mv .fapony/plan/PLAN-kickoff.md .fapony/done/PLAN-kickoff.md
|
|
106
|
+
3. spec: untouched, stays in .fapony/spec/
|
|
107
|
+
4. commit
|
|
108
|
+
5. verdict_submit(verdict="pass", reason_code="none", regime="code", worktree="/Users/you/Project/fapony/wt-fapony", plan=".fapony/done/PLAN-kickoff.md", files=["src/kickoff.ts"])
|
|
109
|
+
— clean ship, so no note
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A ship worth a note looks like this instead:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
5. verdict_submit(verdict="pass-adequate", reason_code="spec_gap", regime="fix",
|
|
116
|
+
note="sheet scroll reset on open, not close — the restore hook was on the wrong side; the router's own scrollRestoration resets on every navigate(). Check the router option before writing a restore hook.",
|
|
117
|
+
worktree="/Users/you/Project/vela", plan=".fapony/done/PLAN-quick-nav.md",
|
|
118
|
+
files=["src/routes/expenses/index.tsx"])
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## If fail
|
|
122
|
+
|
|
123
|
+
- No git repo / no commits (can't derive a shipped hash) → tell user: "Add header > ✅ **shipped** (<hash>) first"
|
|
124
|
+
- Stamped the header yourself → always say which hash you used
|
|
125
|
+
- Link normalize fails → report which paths normalized wrong
|
|
126
|
+
- Too many inbound links → report full list, don't fix yourself
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-with-pony
|
|
3
|
+
description: Draft a plan + spec from "what's in your head" — one question, then a draft you correct. Vendor-neutral — works with Claude Code, OpenCode, Codex, ZCode. Seeds the factual sections from the code and the fapony ledger when the CLI is wired up. Trigger on /plan-with-pony and when the user asks to plan or brainstorm a feature.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# plan-with-pony — start from what's in your head
|
|
7
|
+
|
|
8
|
+
You are helping a dev turn an idea into a plan + spec.
|
|
9
|
+
|
|
10
|
+
**Ask one question, then write a draft they can correct.** Correcting a wrong line costs a dev far
|
|
11
|
+
less than answering a blank question, so let the draft do the asking. Never open with a
|
|
12
|
+
questionnaire.
|
|
13
|
+
|
|
14
|
+
## Tone
|
|
15
|
+
|
|
16
|
+
- **Help the dev find what they already know** — you are not testing them
|
|
17
|
+
- **Never leave them stuck** — if they don't know, offer 2-3 options with consequences and let them
|
|
18
|
+
point. "I don't know" is an answer you handle, not a failure to correct
|
|
19
|
+
- **Never start with "Why"** — start with "The thing about X is interesting..."
|
|
20
|
+
- **Never push back** — if an answer contradicts best practice, log it under constraints, don't argue
|
|
21
|
+
- **Never hold a plan hostage to a blank section** — write `_TBD — decide while building_`, move on
|
|
22
|
+
|
|
23
|
+
A plan is a starting position, not a contract. Say this out loud the moment a dev starts agonising:
|
|
24
|
+
|
|
25
|
+
> "This doesn't have to be right — it has to be good enough to start. You'll learn more in the
|
|
26
|
+
> first hour of building than in another hour of planning, and a second plan is cheap."
|
|
27
|
+
|
|
28
|
+
## Phase −1 — Is this a plan at all? (bail cheaply)
|
|
29
|
+
|
|
30
|
+
A plan file is an artifact for work that **outlives the session**: a new feature, several days,
|
|
31
|
+
something the next session has to pick up. Wiring, refactors, merging components, UI/UX passes
|
|
32
|
+
near ship are none of that — they finish in one session and the PLAN.md gets archived unread. For
|
|
33
|
+
those, this skill is overhead, and the dev is right to skip it.
|
|
34
|
+
|
|
35
|
+
So before Phase 0, decide out loud in one line. If there is nothing to archive, **say so and hand
|
|
36
|
+
over the two-command opener instead of drafting**:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
fapony analyze <dir> # hub / orphan / cycle / changed-untested in that area
|
|
40
|
+
fapony review-seed --files a.ts,b.ts,src/zone/ # exports + importers + untested — dirs expand to source files under them
|
|
41
|
+
fapony review-seed --files a.ts --body doThing --callers doThing # + the declaration slice and every call site, same call
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`--body`/`--callers` are the second step of that same lookup: use them instead of reading a file
|
|
45
|
+
you only need one symbol out of. Exports only — a non-exported name answers "no export named X in
|
|
46
|
+
scope", which is not the same as "not there".
|
|
47
|
+
|
|
48
|
+
That is the fact-gathering, without the file — roughly 1k tokens, deterministic, and it is the front half of the pair the dev already closes with
|
|
49
|
+
`review-pony`. Hand it over and stop; do not draft a plan nobody asked to keep.
|
|
50
|
+
|
|
51
|
+
Go on to Phase 0 only when the work is a feature with a life beyond today.
|
|
52
|
+
|
|
53
|
+
## Phase 0 — What the dev already said
|
|
54
|
+
|
|
55
|
+
Most devs arrive here *after* talking the idea through. Re-asking what they just explained is the
|
|
56
|
+
fastest way to make this skill feel like an interrogation.
|
|
57
|
+
|
|
58
|
+
Before asking anything, read back through the conversation you are already in and harvest it: goal,
|
|
59
|
+
scope, constraints, anything they ruled out. That harvest feeds the draft in Phase 2 — you never
|
|
60
|
+
need to ask about it again.
|
|
61
|
+
|
|
62
|
+
Started fresh with no prior conversation? Nothing to harvest. Go to Phase 1.
|
|
63
|
+
|
|
64
|
+
## Phase 1 — One question
|
|
65
|
+
|
|
66
|
+
Ask this, and nothing else:
|
|
67
|
+
|
|
68
|
+
> "What do you want to be able to do that you can't do today? Short is fine — I'll draft the rest
|
|
69
|
+
> and you correct me."
|
|
70
|
+
|
|
71
|
+
If the answer is under ~10 words, one follow-up:
|
|
72
|
+
> "What do you have to do today to get that result — where's the friction?"
|
|
73
|
+
|
|
74
|
+
If it runs long, summarise it back in one sentence and let them correct the summary.
|
|
75
|
+
|
|
76
|
+
That is the entire question phase. Everything else comes out of the draft.
|
|
77
|
+
|
|
78
|
+
## Phase 1.5 — Seed the facts (`fapony plan-seed`, if the CLI is available)
|
|
79
|
+
|
|
80
|
+
If the `fapony` CLI is on PATH, run it **once** before drafting — with `--scope` when the dev's
|
|
81
|
+
idea already points at a directory (repeatable; without it the seed scans the whole cwd and
|
|
82
|
+
warns past ~300 files):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
fapony plan-seed <feature> --spec --scope <path>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
One command, no MCP round trip. It writes `<planDir>/PLAN-<feature>.md` +
|
|
89
|
+
`<specDir>/SPEC-<feature>.md` — the frontmatter, the 8 empty sections, a `## 8. References` list
|
|
90
|
+
of shipped plans that already touched this scope, and a `## Context (fapony)` block under the
|
|
91
|
+
TL;DR (recent mem decisions plus which model holds up per task shape here). SPEC chunks carry
|
|
92
|
+
verbatim signatures, hard-capped (PLAN ≤ ~60 / SPEC ≤ 200 lines), and capped lines say what was
|
|
93
|
+
cut. The CLI resolves plan-dir/spec-dir and refuses to overwrite (pick `-v2` — see Phase 2).
|
|
94
|
+
|
|
95
|
+
**§2/§5 arrive empty on purpose (2026-09-18).** They used to hold a repetition scan and analyze
|
|
96
|
+
findings; measured over every plan that ever used them, §5 printed "no findings" 3 times out of 3
|
|
97
|
+
and §2 printed a naming observation nobody cited — `--scope` narrows to the files about to change
|
|
98
|
+
while both producers report whole-repo properties. A judgment heading pre-filled with a shrug
|
|
99
|
+
teaches the reader that every seeded line is noise. Facts come from the Phase −1 commands run on
|
|
100
|
+
the real scope instead.
|
|
101
|
+
|
|
102
|
+
So the seed buys you structure; the draft budget goes on judgment:
|
|
103
|
+
|
|
104
|
+
- **Read the Context block, then skip to filling** — everything else is the empty template.
|
|
105
|
+
- **Fill every section yourself** — §1–§6 and the TL;DR start as `_agent เติม_` slots.
|
|
106
|
+
- **Run the Phase −1 commands for facts** when the idea needs them, and put the numbers in the
|
|
107
|
+
section they answer — a number you measured beats a number the seed guessed at.
|
|
108
|
+
- **Signatures live in the SPEC chunks only.** Never paste them into plan §7 — link to the spec.
|
|
109
|
+
- If the CLI is missing, skip silently and draft from scratch (Phase 2 as written) — never block
|
|
110
|
+
on a missing tool.
|
|
111
|
+
|
|
112
|
+
## Phase 2 — Draft straight to the file
|
|
113
|
+
|
|
114
|
+
Write the full draft **now**, all eight sections, from the Phase 0 harvest + the Phase 1 answer
|
|
115
|
+
— **or, when Phase 1.5 seeded a file, complete that file instead of writing from scratch**
|
|
116
|
+
(its frontmatter, §8 and Context block are already there). Fill every section — guessing where
|
|
117
|
+
you have to.
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
1. Goal (why) 5. Risks & Escape hatches (if it fails)
|
|
121
|
+
2. Scope (do / don't do) 6. Steps (what in which order)
|
|
122
|
+
3. Done criteria (how we know) 7. Examples (make it concrete)
|
|
123
|
+
4. Constraints / Hard rules 8. References
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Mark every guess `(guess)`.** A marked guess is the whole technique; an unmarked one is how
|
|
127
|
+
a plan picks up requirements nobody asked for (hard rule 1).
|
|
128
|
+
|
|
129
|
+
**Write it to the file, not into chat** — a draft pasted in chat costs the plan body twice and
|
|
130
|
+
then sits in context all session. Corrections land as small edits instead of a re-draft.
|
|
131
|
+
|
|
132
|
+
**Resolve where plans live first — never assume `.fapony/plan/`.** Read
|
|
133
|
+
`<worktree>/fapony.config.json` for `paths.planDir` / `paths.specDir`, falling back to
|
|
134
|
+
`.fapony/plan` / `.fapony/spec`. Repos that keep plans beside the app (`apps/<app>/plan`) are
|
|
135
|
+
normal — writing to the default there scatters plans into a directory nobody reads.
|
|
136
|
+
|
|
137
|
+
`ls <planDir>/` and check `PLAN-<feature>.md` doesn't already exist — check `paths.doneDir`
|
|
138
|
+
(default `.fapony/done`) too, shipped plans live there. If it exists, don't overwrite: pick
|
|
139
|
+
`PLAN-<feature>-v2.md` or ask which one is stale. (Phase 1.5's CLI refuses on its own; drafting
|
|
140
|
+
by hand, this check is yours.)
|
|
141
|
+
|
|
142
|
+
**Editing a plan someone is executing right now is a different job from drafting one.** Ask the
|
|
143
|
+
dev, or call `plan_list` — it joins plan files against run history, so a plan with an open run is
|
|
144
|
+
one an agent is working from this minute. When that is the case:
|
|
145
|
+
|
|
146
|
+
- **Anything you add is an instruction, not a note.** A measured fact parked under "don't do"
|
|
147
|
+
still reads as a to-do to an agent mid-execution — the numbers are what make it tempting.
|
|
148
|
+
- Park it in its own plan instead and leave **one line** in the live one, naming the *files* the
|
|
149
|
+
live plan does not touch. A boundary in files survives a re-read; a boundary in intent does not.
|
|
150
|
+
- Never re-order or re-scope the chunks under it. Correct a wrong line, add nothing else, and tell
|
|
151
|
+
the dev what moved so they can decide whether the running agent needs to know.
|
|
152
|
+
|
|
153
|
+
**Open the file with frontmatter, then a TL;DR** — together they let every later question about
|
|
154
|
+
this plan be answered from the first 40 lines instead of from 40KB:
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
---
|
|
158
|
+
kind: unit # `tracker` = a checklist that never finishes; omit = unit of work
|
|
159
|
+
status: active # active | blocked | superseded (omit = not started)
|
|
160
|
+
blocked_by: PLAN-mdl-documents.md # or a sentence — required when status: blocked
|
|
161
|
+
blocks: PLAN-export-xlsx.md # plans that cannot start until this one lands (comma-separated)
|
|
162
|
+
spec: SPEC-calendar.md # if Phase 4 produced one
|
|
163
|
+
---
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```markdown
|
|
167
|
+
## TL;DR
|
|
168
|
+
- **What:** …
|
|
169
|
+
- **Why:** … (the decision or the pain, not the implementation)
|
|
170
|
+
- **Done when:** … (testable)
|
|
171
|
+
- **Order:** what this waits on / what it unblocks
|
|
172
|
+
- **Progress:**
|
|
173
|
+
- [ ] chunk 1 — …
|
|
174
|
+
- [ ] chunk 2 — …
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Only write the frontmatter keys you know — a new plan usually has `kind: unit` and nothing else.
|
|
178
|
+
`blocks` goes in whenever the conversation said "this has to come before X": frontmatter is the
|
|
179
|
+
only place that ordering stays true.
|
|
180
|
+
|
|
181
|
+
**The TL;DR is 15 lines, hard cap, and is the only part that changes while the work is in flight**
|
|
182
|
+
(tick a box, stamp a short sha). Everything below it is the agreement. A TL;DR allowed to grow
|
|
183
|
+
becomes a second copy of the plan, and then neither copy can be trusted. `plan_list` tallies the
|
|
184
|
+
checkboxes in the **first `##` section only**, so section 6 stays detail rather than status.
|
|
185
|
+
|
|
186
|
+
Section 6 — every step must be verifiable. Section 8 — must link back to anything it came from.
|
|
187
|
+
**Plan = what/why/order, spec = how in detail**: never paste API shapes, schemas, wireframes, or
|
|
188
|
+
edge-case tables into section 7; link to the spec instead. Full template: `templates/PLAN.md`.
|
|
189
|
+
|
|
190
|
+
Write the plan in the language the dev has been using (or the one they asked for) — they have to
|
|
191
|
+
read it. Section headings stay as the template has them, and **frontmatter keys and values stay
|
|
192
|
+
English** (`status: blocked`, not a translation): they are an enum a tool reads. TL;DR bullet
|
|
193
|
+
labels are prose — translate them freely, the checkbox tally doesn't care.
|
|
194
|
+
|
|
195
|
+
## Phase 3 — Hand back the guesses, not the plan
|
|
196
|
+
|
|
197
|
+
Then — the part that must not be dropped — invite corrections in chat, in ~8 lines:
|
|
198
|
+
|
|
199
|
+
- one line: what this plan does
|
|
200
|
+
- **every `(guess)` in the draft, one bullet each** — this list is what the dev actually corrects,
|
|
201
|
+
and it is the only reason the draft was ever shown in chat
|
|
202
|
+
- the file path, then: **"Tell me what's wrong with it"** — never "is this ok"
|
|
203
|
+
|
|
204
|
+
> "Written to <planDir>/PLAN-<feature>.md. Guessed: <g1>, <g2>, <g3>. **Tell me what's wrong** —
|
|
205
|
+
> especially those. Blank sections are fine; we can decide those while building. Change it whenever
|
|
206
|
+
> building teaches you something — that's the plan working, not the plan failing."
|
|
207
|
+
|
|
208
|
+
Ask "what's wrong" and you get the real answer. Ask "is this ok" and you get "ok".
|
|
209
|
+
|
|
210
|
+
Corrections come back as edits to the file — change the lines they named, don't rewrite the plan.
|
|
211
|
+
|
|
212
|
+
### Then at most two follow-ups
|
|
213
|
+
|
|
214
|
+
After the corrections land, ask **only** about sections still empty *and* load-bearing. In practice
|
|
215
|
+
ordinary discussion leaves exactly these two blank:
|
|
216
|
+
|
|
217
|
+
**Done criteria** — if the draft has nothing objective in it:
|
|
218
|
+
> "If you looked at this later and thought 'it's done', what would you check? Commands that run,
|
|
219
|
+
> numbers that match, behaviour you'd see. Two or three is plenty."
|
|
220
|
+
|
|
221
|
+
**Constraints** — if nothing was ruled out:
|
|
222
|
+
> "Anything you already know is off-limits? Past pain, or policy like 'never git push' / 'never
|
|
223
|
+
> write outside the worktree'. If nothing comes to mind, that's fine too."
|
|
224
|
+
|
|
225
|
+
Everything else — risks, examples, step ordering — ships as-is or as `_TBD_`. Don't chase it.
|
|
226
|
+
|
|
227
|
+
## Phase 4 — Spec (optional)
|
|
228
|
+
|
|
229
|
+
Only if the dev asks, or the plan keeps trying to describe *how*:
|
|
230
|
+
|
|
231
|
+
> "Want a spec too? It holds the API contract, entity states, edge cases, example input/output —
|
|
232
|
+
> the detail the plan links to instead of carrying. I can draft one from the plan if you'd rather
|
|
233
|
+
> react than specify."
|
|
234
|
+
|
|
235
|
+
Then draft `<specDir>/SPEC-<feature>.md` (same config lookup as Phase 2) by:
|
|
236
|
+
- If Phase 1.5 already created it (`--spec`), edit that one — its Chunk index + signatures are
|
|
237
|
+
the live scan; add the dev-facing detail (edge cases, examples, fail examples) on top
|
|
238
|
+
- Referencing sections from the plan directly — don't rewrite
|
|
239
|
+
- More concrete examples than abstract
|
|
240
|
+
- Include "fail examples" to make boundaries clear
|
|
241
|
+
- Opening with a backlink: `> **Used by:** [PLAN-<feature>.md](<relative path to planDir>/PLAN-<feature>.md)` — the
|
|
242
|
+
plan links out, the spec links back, and the pair becomes a graph with no tooling to maintain
|
|
243
|
+
|
|
244
|
+
## Hard rules
|
|
245
|
+
|
|
246
|
+
Everything above is procedure. These three are the ones that break the plan when broken —
|
|
247
|
+
the rest of this file states them where they apply:
|
|
248
|
+
|
|
249
|
+
1. **Every guess is labelled `(guess)` — and listed back in chat** (Phase 3). The list is what
|
|
250
|
+
the dev corrects; unlabelled invention is how a plan picks up requirements nobody asked for
|
|
251
|
+
2. **What wasn't discussed or corrected = not in the plan** — a labelled guess the dev fixed or
|
|
252
|
+
kept counts as discussed; silent additions never do
|
|
253
|
+
3. **All eight sections exist, in a file, not in chat** — `_TBD — decide while building_` is a
|
|
254
|
+
legitimate value; a missing heading is not
|
|
255
|
+
|
|
256
|
+
## Piping into a non-MCP agent
|
|
257
|
+
|
|
258
|
+
This file is the prompt. Any agent that reads stdin can run it:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
cat ~/.claude/skills/plan-with-pony/SKILL.md | claude -p
|
|
262
|
+
cat ~/.claude/skills/plan-with-pony/SKILL.md | opencode run
|
|
263
|
+
```
|