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,161 @@
|
|
|
1
|
+
// src/usage/scan.ts — fapony usage-scan command
|
|
2
|
+
//
|
|
3
|
+
// Scans session logs for all four clients, writes usage-cache.jsonl.
|
|
4
|
+
// Windowed replace (30d default, all-time with --full) — never incremental.
|
|
5
|
+
// Per-worktree + global aggregate — the cache now carries a worktree dimension.
|
|
6
|
+
// Progress bar on TTY, plain lines on pipe/CI.
|
|
7
|
+
|
|
8
|
+
import { loadConfig, openDb } from "../db/index.js";
|
|
9
|
+
import { CLIENTS } from "../session/index.js";
|
|
10
|
+
import type { PassiveUsageResult } from "../session/types.js";
|
|
11
|
+
import {
|
|
12
|
+
type CacheEntry,
|
|
13
|
+
cacheMeta,
|
|
14
|
+
mergeEntries,
|
|
15
|
+
readCache,
|
|
16
|
+
writeCache,
|
|
17
|
+
} from "./cache.js";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_JSONL_LOOKBACK_DAYS = 30;
|
|
20
|
+
|
|
21
|
+
function toCacheEntry(
|
|
22
|
+
client: string,
|
|
23
|
+
result: PassiveUsageResult,
|
|
24
|
+
worktree?: string,
|
|
25
|
+
): CacheEntry {
|
|
26
|
+
return {
|
|
27
|
+
client,
|
|
28
|
+
worktree: worktree ?? null,
|
|
29
|
+
scanned_at: new Date().toISOString(),
|
|
30
|
+
session_count: result.session_count,
|
|
31
|
+
total_tokens_input: result.total_tokens_input,
|
|
32
|
+
total_tokens_output: result.total_tokens_output,
|
|
33
|
+
total_tokens_reasoning: result.total_tokens_reasoning,
|
|
34
|
+
total_tokens_cache_read: result.total_tokens_cache_read,
|
|
35
|
+
total_tokens_cache_write: result.total_tokens_cache_write,
|
|
36
|
+
total_cost: result.total_cost,
|
|
37
|
+
by_model: result.by_model.map((m) => ({
|
|
38
|
+
model: m.model,
|
|
39
|
+
provider: m.provider,
|
|
40
|
+
session_count: m.session_count,
|
|
41
|
+
tokens_input: m.tokens_input,
|
|
42
|
+
tokens_output: m.tokens_output,
|
|
43
|
+
tokens_reasoning: m.tokens_reasoning,
|
|
44
|
+
tokens_cache_read: m.tokens_cache_read,
|
|
45
|
+
tokens_cache_write: m.tokens_cache_write,
|
|
46
|
+
cost: m.cost,
|
|
47
|
+
})),
|
|
48
|
+
...(result.error ? { error: result.error } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Surface a per-client problem without aborting the scan of the others. */
|
|
53
|
+
function warn(scope: string, detail: string, isTTY: boolean): void {
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
isTTY ? `\n ⚠ ${scope}: ${detail}\n` : ` warn: ${scope}: ${detail}\n`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function progress(msg: string, isTTY: boolean): void {
|
|
60
|
+
if (isTTY) {
|
|
61
|
+
process.stderr.write(`\r\x1B[K${msg}`);
|
|
62
|
+
} else {
|
|
63
|
+
process.stderr.write(`${msg}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function cmdUsageScan(rawArgs: string[]): void {
|
|
68
|
+
const full = rawArgs.includes("--full");
|
|
69
|
+
const args = rawArgs.filter((a) => a !== "--full");
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
const isTTY = !!process.stderr.isTTY;
|
|
72
|
+
|
|
73
|
+
if (args[0] && args[0] !== "--full") {
|
|
74
|
+
console.error("usage: fapony usage-scan [--full]");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const existing = readCache(config);
|
|
79
|
+
|
|
80
|
+
const windowSince = full
|
|
81
|
+
? undefined
|
|
82
|
+
: Date.now() / 1000 - DEFAULT_JSONL_LOOKBACK_DAYS * 86400;
|
|
83
|
+
|
|
84
|
+
// Collect distinct worktrees from runs table (the source of truth for projects).
|
|
85
|
+
const db = openDb();
|
|
86
|
+
let worktrees: string[];
|
|
87
|
+
try {
|
|
88
|
+
worktrees = (
|
|
89
|
+
db
|
|
90
|
+
.prepare("SELECT DISTINCT worktree FROM runs WHERE worktree != ''")
|
|
91
|
+
.all() as { worktree: string }[]
|
|
92
|
+
)
|
|
93
|
+
.map((r) => r.worktree)
|
|
94
|
+
.sort();
|
|
95
|
+
} finally {
|
|
96
|
+
db.close();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Scan targets: per worktree + global (null = aggregate).
|
|
100
|
+
const scanTargets: Array<{ label: string; worktree?: string }> = [
|
|
101
|
+
...worktrees.map((wt) => ({ label: wt, worktree: wt })),
|
|
102
|
+
{ label: "all projects" },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const totalSteps = scanTargets.length * CLIENTS.length;
|
|
106
|
+
let step = 0;
|
|
107
|
+
|
|
108
|
+
const entries: CacheEntry[] = [];
|
|
109
|
+
|
|
110
|
+
for (const target of scanTargets) {
|
|
111
|
+
const wt = target.worktree;
|
|
112
|
+
|
|
113
|
+
for (const client of CLIENTS) {
|
|
114
|
+
step++;
|
|
115
|
+
const pct = Math.round((step / totalSteps) * 100);
|
|
116
|
+
const bar = `[${"█".repeat(Math.round(pct / 5))}${"░".repeat(20 - Math.round(pct / 5))}]`;
|
|
117
|
+
const label = client.scanLabel ?? client.key;
|
|
118
|
+
const scopeLabel = wt ? `${target.label}/${label}` : label;
|
|
119
|
+
progress(
|
|
120
|
+
`${scopeLabel} ${bar} ${step}/${totalSteps} (${pct}%)`,
|
|
121
|
+
isTTY,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const result = client.read(wt, windowSince, undefined, false, full);
|
|
126
|
+
// A failed read returns zeros WITH a code — the same shape as "no
|
|
127
|
+
// sessions", so it gets dropped below and would otherwise leave the
|
|
128
|
+
// cache silently short one client. Say it out loud instead.
|
|
129
|
+
if (result.error) warn(scopeLabel, result.error, isTTY);
|
|
130
|
+
// A broken client is cached too, precisely because it has no sessions
|
|
131
|
+
// to speak for it — otherwise it just vanishes from usage-web.
|
|
132
|
+
if (result.session_count > 0 || result.error) {
|
|
133
|
+
entries.push(toCacheEntry(client.key, result, wt));
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
warn(scopeLabel, String(err), isTTY);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Merge with existing and write.
|
|
142
|
+
const merged = mergeEntries(existing, entries);
|
|
143
|
+
writeCache(merged, config);
|
|
144
|
+
|
|
145
|
+
const finalMeta = cacheMeta(merged);
|
|
146
|
+
if (isTTY) {
|
|
147
|
+
process.stderr.write(
|
|
148
|
+
`\r\x1B[Kscan done — ${finalMeta?.total_sessions ?? 0} sessions cached (${entries.length} entries written, ${worktrees.length} projects)\n`,
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
process.stderr.write(
|
|
152
|
+
`scan done — ${finalMeta?.total_sessions ?? 0} sessions cached (${entries.length} entries written, ${worktrees.length} projects)\n`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`cache written to ${cachePathHint(config)}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cachePathHint(_config?: ReturnType<typeof loadConfig>): string {
|
|
160
|
+
return "usage-cache.jsonl";
|
|
161
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Replacement values are inserted via a function so `$&`, `$'` etc. in the
|
|
2
|
+
// value are treated as literal text, not replace() special patterns.
|
|
3
|
+
export function templateArgs(
|
|
4
|
+
arr: string[],
|
|
5
|
+
vars: Record<string, string>,
|
|
6
|
+
): string[] {
|
|
7
|
+
return arr.map((s) => {
|
|
8
|
+
let out = s;
|
|
9
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
10
|
+
out = out.replaceAll(`{${k}}`, () => v);
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Fill a prompt template with {{VARS}} (all occurrences). Missing vars → "". */
|
|
17
|
+
export function fillPrompt(
|
|
18
|
+
template: string,
|
|
19
|
+
vars: Record<string, string>,
|
|
20
|
+
): string {
|
|
21
|
+
let out = template;
|
|
22
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
23
|
+
out = out.replaceAll(`{{${k}}}`, () => v);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** True for "y"/"yes" (case-insensitive, trimmed) — the only affirmative answers. */
|
|
29
|
+
export function isAffirmative(answer: string): boolean {
|
|
30
|
+
const normalized = answer.trim().toLowerCase();
|
|
31
|
+
return normalized === "y" || normalized === "yes";
|
|
32
|
+
}
|
package/src/web/html.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/web/html.ts — shared HTML helpers for report-web and usage-web
|
|
2
|
+
|
|
3
|
+
/** HTML-escape interpolated strings. */
|
|
4
|
+
export function esc(s: string): string {
|
|
5
|
+
return s
|
|
6
|
+
.replaceAll("&", "&")
|
|
7
|
+
.replaceAll("<", "<")
|
|
8
|
+
.replaceAll(">", ">")
|
|
9
|
+
.replaceAll('"', """);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Common dark-theme CSS variables + base reset used by all HTML pages. */
|
|
13
|
+
export const DARK_THEME_CSS = `
|
|
14
|
+
:root { --bg: #0d1117; --fg: #c9d1d9; --border: #30363d; --accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922; --muted: #8b949e; }
|
|
15
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
16
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--fg); line-height: 1.6; padding: 2rem; max-width: 960px; margin: 0 auto; }
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
/** Shared table + typography CSS used by both report and usage pages. */
|
|
20
|
+
export const TABLE_CSS = `
|
|
21
|
+
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
22
|
+
h2 { font-size: 1.1rem; color: var(--accent); margin: 1.5rem 0 0.5rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
|
|
23
|
+
.meta { color: var(--muted); font-size: 0.85rem; margin-bottom: 1.5rem; }
|
|
24
|
+
table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; font-size: 0.9rem; }
|
|
25
|
+
th, td { padding: 0.5rem 0.8rem; text-align: left; border-bottom: 1px solid var(--border); }
|
|
26
|
+
th { color: var(--muted); font-weight: 600; font-size: 0.8rem; text-transform: uppercase; }
|
|
27
|
+
tr:hover { background: #161b22; }
|
|
28
|
+
.pass { color: var(--green); }
|
|
29
|
+
.fail { color: var(--red); }
|
|
30
|
+
.warn { color: var(--yellow); }
|
|
31
|
+
.muted { color: var(--muted); }
|
|
32
|
+
.sample { font-size: 0.8rem; color: var(--muted); }
|
|
33
|
+
`;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Plan Core — template for every plan file
|
|
2
|
+
|
|
3
|
+
> Use this template for every plan file (not just fapony) — see [CLAUDE.md](../CLAUDE.md) § Rules
|
|
4
|
+
> for AI Agents for the rules each plan must pass before an agent may execute it.
|
|
5
|
+
|
|
6
|
+
```markdown
|
|
7
|
+
---
|
|
8
|
+
kind: unit # `tracker` for a checklist that never finishes; omit = unit of work
|
|
9
|
+
status: active # active | blocked | superseded · omit = not started
|
|
10
|
+
blocked_by: <plan or sentence> # required when status: blocked
|
|
11
|
+
blocks: PLAN-<other>.md # plans that cannot start until this one lands (comma-separated)
|
|
12
|
+
superseded_by: PLAN-<other>.md # required when status: superseded
|
|
13
|
+
spec: SPEC-<feature>.md # if any
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# PLAN-<feature>.md — <short name>
|
|
17
|
+
|
|
18
|
+
> **Status:** 🚧 in-progress · **Owner:** <dev> · **Created:** <YYYY-MM-DD>
|
|
19
|
+
> **Source spec:** [spec/<feature>.md](../spec/<feature>.md) — if any
|
|
20
|
+
|
|
21
|
+
## TL;DR
|
|
22
|
+
- **What:** one line
|
|
23
|
+
- **Why:** one line — the decision or the pain, not the implementation
|
|
24
|
+
- **Done when:** one line, testable
|
|
25
|
+
- **Order:** what this waits on / what it unblocks (mirrors the frontmatter)
|
|
26
|
+
- **Progress:**
|
|
27
|
+
- [x] chunk 1 — <what landed> `<short sha>` <YYYY-MM-DD>
|
|
28
|
+
- [ ] chunk 2 — <what is next>
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Goal (why)
|
|
33
|
+
1–3 sentences — if a reader can't answer "so what" after reading = not clear yet
|
|
34
|
+
|
|
35
|
+
## 2. Scope (do / don't do)
|
|
36
|
+
**Do:** 3–7 bullets, outcomes not tasks
|
|
37
|
+
**Don't do:** 2–5 bullets + 1-line reason per item
|
|
38
|
+
|
|
39
|
+
## 3. Done criteria (how we know it's finished)
|
|
40
|
+
3–6 bullets — testable (tests pass / command runs / user can reproduce)
|
|
41
|
+
Never write bare "done" — must be measurable
|
|
42
|
+
|
|
43
|
+
## 4. Constraints / Hard rules (must not violate)
|
|
44
|
+
3–8 bullets — violations that break things (not "good practices")
|
|
45
|
+
|
|
46
|
+
## 5. Risks & Escape hatches (if it fails)
|
|
47
|
+
Table with 3–5 rows: risk | likelihood | impact | escape hatch
|
|
48
|
+
|
|
49
|
+
## 6. Steps (what in which order)
|
|
50
|
+
1. **<Step 1>** — has a clear deliverable
|
|
51
|
+
2. **<Step 2>** — ...
|
|
52
|
+
Each step must be verifiable before moving to the next
|
|
53
|
+
|
|
54
|
+
## 7. Examples (make it concrete)
|
|
55
|
+
bash examples: before / after — **link into spec/, don't paste it.**
|
|
56
|
+
If Source spec exists, this section stays ≤5 lines (1-2 examples or a pointer);
|
|
57
|
+
anything longer belongs in the spec file, not here.
|
|
58
|
+
|
|
59
|
+
## 8. References
|
|
60
|
+
- link back to related files
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Two rules that keep the file cheap to read:**
|
|
64
|
+
- **The TL;DR is 15 lines, hard cap.** It is the only part that changes while the work is in
|
|
65
|
+
flight (tick a box, stamp a sha); everything below it is the agreement and changes only when
|
|
66
|
+
scope changes. That contract is what lets a reader trust the first 40 lines instead of pulling
|
|
67
|
+
100KB into context.
|
|
68
|
+
- **`plan_list` counts the checkboxes in the first `##` section only** — whatever that section is
|
|
69
|
+
called, so the tally works in any language, and a step list deeper in the file stays detail
|
|
70
|
+
instead of becoming status.
|
|
71
|
+
|
|
72
|
+
**Where files live:** `.fapony/plan/` live · `.fapony/done/` shipped (a sibling, so archiving is a
|
|
73
|
+
plain `git mv` that keeps every relative link working) · `.fapony/spec/` every spec, never archived.
|
|
74
|
+
See [examples/](../examples/) for the whole layout with one file per shape.
|
|
75
|
+
|
|
76
|
+
**Language:** frontmatter keys and values are English always (they are an enum a tool reads);
|
|
77
|
+
headings stay as this template has them; everything else is written in whatever language the dev
|
|
78
|
+
reads, because the plan is for them.
|
|
79
|
+
|
|
80
|
+
**4 iron rules:**
|
|
81
|
+
- Sections 1–4 are mandatory — if missing = plan is immature, agent must not execute
|
|
82
|
+
- Section 6 each step must be verifiable — if you can't tell it passed = not clear yet
|
|
83
|
+
- Section 8 must link back — prevents drift and gives context on reopen
|
|
84
|
+
- **Plan is "what/why/order", spec is "how in detail"** — API shapes, schemas,
|
|
85
|
+
wireframes, edge-case tables go in [spec/](../spec/), the plan only links to
|
|
86
|
+
them. A plan that keeps growing past ~200 lines is spec content leaking in,
|
|
87
|
+
not a plan getting more thorough — split it out.
|
|
88
|
+
|
|
89
|
+
Use [templates/SPEC.md](SPEC.md) for the spec file itself — it links back to
|
|
90
|
+
every plan that uses it, so the relationship reads both ways.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Spec Core — template for every spec file
|
|
2
|
+
|
|
3
|
+
> Use with [PLAN.md](PLAN.md). A spec holds the **detail** a plan should only
|
|
4
|
+
> link to: API/data shapes, schemas, wireframes, edge cases, examples. Put it
|
|
5
|
+
> in `.fapony/spec/<feature>.md`.
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# SPEC-<feature>.md — <short name>
|
|
9
|
+
|
|
10
|
+
> **Used by:** [PLAN-<feature>.md](../plan/PLAN-<feature>.md)
|
|
11
|
+
> (add every plan that references this spec — keeps the link bidirectional so
|
|
12
|
+
> either file leads you to the other, and `fapony plan-mv` finds this file
|
|
13
|
+
> when it scans inbound links.)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Shape (data / API / schema)
|
|
18
|
+
Concrete types, request/response bodies, DB columns — whatever the code needs.
|
|
19
|
+
|
|
20
|
+
## Edge cases
|
|
21
|
+
Table or bullets: input → expected behavior.
|
|
22
|
+
|
|
23
|
+
## Examples
|
|
24
|
+
Before / after, request / response, sample payloads — as long as it needs to be.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Rule:** a plan's section 7 (Examples) links here instead of pasting content.
|
|
28
|
+
If a plan keeps growing, the fix is usually "move it into the spec", not
|
|
29
|
+
"trim the plan" — the detail is still needed, just not in the file an agent
|
|
30
|
+
re-reads every round.
|