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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +473 -0
  3. package/fapony.ts +78 -0
  4. package/package.json +42 -0
  5. package/skill/git-commit-conventional/SKILL.md +68 -0
  6. package/skill/git-ship/SKILL.md +144 -0
  7. package/skill/move-to-done/SKILL.md +126 -0
  8. package/skill/plan-with-pony/SKILL.md +263 -0
  9. package/skill/review-pony/SKILL.md +254 -0
  10. package/src/analyze.ts +517 -0
  11. package/src/context/index.ts +11 -0
  12. package/src/context/projectHealth.ts +359 -0
  13. package/src/conventions-seed.ts +420 -0
  14. package/src/db/defaults.ts +26 -0
  15. package/src/db/getters.ts +33 -0
  16. package/src/db/index.ts +7 -0
  17. package/src/db/load.ts +57 -0
  18. package/src/db/store.ts +286 -0
  19. package/src/db/types.ts +79 -0
  20. package/src/debt.ts +667 -0
  21. package/src/digest/cli.ts +75 -0
  22. package/src/digest/collect.ts +625 -0
  23. package/src/digest/html.ts +208 -0
  24. package/src/digest/text.ts +191 -0
  25. package/src/gate.ts +153 -0
  26. package/src/gates.ts +194 -0
  27. package/src/hook.ts +436 -0
  28. package/src/init-mem.ts +71 -0
  29. package/src/init.ts +237 -0
  30. package/src/install/claude.ts +361 -0
  31. package/src/install/codex.ts +61 -0
  32. package/src/install/cursor.ts +167 -0
  33. package/src/install/detect.ts +78 -0
  34. package/src/install/opencode.ts +234 -0
  35. package/src/install/skills.ts +106 -0
  36. package/src/install/types.ts +69 -0
  37. package/src/install/utils.ts +29 -0
  38. package/src/install/zcode.ts +120 -0
  39. package/src/install.ts +176 -0
  40. package/src/lint-baseline.ts +260 -0
  41. package/src/map.ts +320 -0
  42. package/src/math.ts +13 -0
  43. package/src/mcp/evidence.ts +332 -0
  44. package/src/mcp/primitives.ts +316 -0
  45. package/src/mcp/tools/check.ts +243 -0
  46. package/src/mcp/tools/collect.ts +157 -0
  47. package/src/mcp/tools/context.ts +66 -0
  48. package/src/mcp/tools/index.ts +309 -0
  49. package/src/mcp/tools/mem.ts +95 -0
  50. package/src/mcp/tools/plans.ts +255 -0
  51. package/src/mcp/tools/report.ts +285 -0
  52. package/src/mcp/tools/stats.ts +96 -0
  53. package/src/mcp/tools/usage.ts +211 -0
  54. package/src/mcp/tools/verdict.ts +148 -0
  55. package/src/mcp/transport.ts +241 -0
  56. package/src/mcp/types.ts +54 -0
  57. package/src/mcp/worktree.ts +27 -0
  58. package/src/memory.ts +264 -0
  59. package/src/parse.ts +71 -0
  60. package/src/plan-seed.ts +599 -0
  61. package/src/price/fetch.ts +146 -0
  62. package/src/price/index.ts +8 -0
  63. package/src/price/resolve.ts +213 -0
  64. package/src/report/cli.ts +92 -0
  65. package/src/report/format.ts +37 -0
  66. package/src/report/index.ts +4 -0
  67. package/src/report/render.ts +206 -0
  68. package/src/review-seed.ts +932 -0
  69. package/src/safety.ts +18 -0
  70. package/src/session/activeSession.ts +153 -0
  71. package/src/session/claude-code.ts +412 -0
  72. package/src/session/codex.ts +347 -0
  73. package/src/session/findModel.ts +376 -0
  74. package/src/session/helpers.ts +640 -0
  75. package/src/session/index.ts +31 -0
  76. package/src/session/opencode.ts +167 -0
  77. package/src/session/registry.ts +45 -0
  78. package/src/session/types.ts +128 -0
  79. package/src/session/zcode.ts +151 -0
  80. package/src/setup.ts +242 -0
  81. package/src/stats/cli.ts +44 -0
  82. package/src/stats/data.ts +1019 -0
  83. package/src/stats/format.ts +584 -0
  84. package/src/stats/index.ts +19 -0
  85. package/src/telemetry.ts +364 -0
  86. package/src/test.ts +2 -0
  87. package/src/update.ts +212 -0
  88. package/src/usage/cache.ts +125 -0
  89. package/src/usage/cli.ts +120 -0
  90. package/src/usage/format.ts +29 -0
  91. package/src/usage/index.ts +4 -0
  92. package/src/usage/render.ts +523 -0
  93. package/src/usage/scan.ts +161 -0
  94. package/src/util.ts +32 -0
  95. package/src/web/html.ts +33 -0
  96. package/templates/PLAN.md +90 -0
  97. package/templates/SPEC.md +30 -0
  98. package/templates/mem/commands/plan.ts +360 -0
  99. package/templates/mem/commands/read.ts +194 -0
  100. package/templates/mem/commands/rotate.ts +59 -0
  101. package/templates/mem/commands/selftest.ts +450 -0
  102. package/templates/mem/commands/write.ts +214 -0
  103. package/templates/mem/mem.ts +68 -0
  104. package/templates/mem/render.ts +63 -0
  105. package/templates/mem/selectors.ts +144 -0
  106. 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
+ ```