fapony 0.2.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -72
- package/fapony.ts +12 -5
- package/package.json +5 -4
- package/skill/define-convention/SKILL.md +77 -0
- package/skill/lookup-before-edit/SKILL.md +48 -0
- package/skill/move-to-done/SKILL.md +19 -30
- package/skill/review-pony/SKILL.md +38 -61
- package/src/analyze.ts +1 -1
- package/src/debt/cli.ts +193 -0
- package/src/debt/format.ts +107 -0
- package/src/debt/index.ts +19 -0
- package/src/debt/load.ts +92 -0
- package/src/debt/promotion.ts +152 -0
- package/src/debt/scan.ts +214 -0
- package/src/debt/types.ts +79 -0
- package/src/detect.ts +92 -0
- package/src/gate.ts +3 -3
- package/src/hook.ts +349 -100
- package/src/init-mem.ts +57 -71
- package/src/init.ts +12 -16
- package/src/install/antigravity.ts +112 -0
- package/src/install/claude.ts +16 -123
- package/src/install/codex.ts +58 -19
- package/src/install/detect.ts +17 -7
- package/src/install/opencode.ts +131 -6
- package/src/install.ts +12 -3
- package/src/lint-baseline.ts +2 -2
- package/src/mcp/primitives.ts +1 -1
- package/src/mcp/tools/index.ts +13 -102
- package/src/mcp/tools/mem.ts +71 -0
- package/src/mcp/transport.ts +8 -94
- package/src/mcp/worktree.ts +1 -1
- package/src/mem/commands/read.ts +231 -141
- package/src/mem/index.ts +4 -13
- package/src/memory.ts +17 -8
- package/src/{plan-seed.ts → seed/plan-seed.ts} +14 -32
- package/src/seed/primitives.ts +66 -0
- package/src/{review-seed.ts → seed/review-seed.ts} +7 -54
- package/src/session/helpers.ts +1 -1
- package/src/session/registry.ts +3 -6
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +1 -1
- package/src/telemetry.ts +1 -1
- package/src/util.ts +61 -0
- package/templates/SPEC.md +8 -1
- package/images/logo.png +0 -0
- package/images/logo.webp +0 -0
- package/images/logo@400.webp +0 -0
- package/images/sample.webp +0 -0
- package/images/summary.webp +0 -0
- package/src/debt.ts +0 -811
- package/src/math.ts +0 -13
- package/src/mcp/tools/usage.ts +0 -211
- package/src/mcp/tools/verdict.ts +0 -161
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/plan-seed.ts — `fapony plan-seed <name> [--spec] [--scope <path>]...`
|
|
1
|
+
// src/seed/plan-seed.ts — `fapony plan-seed <name> [--spec] [--scope <path>]...`
|
|
2
2
|
//
|
|
3
3
|
// Writes PLAN + SPEC straight into planDir/specDir. What it pre-fills is the
|
|
4
4
|
// structure (frontmatter, the 8 sections, prior art, ledger context) — the
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
//
|
|
26
26
|
// Composes existing producers — no new parsing, no new table, no MCP tool.
|
|
27
27
|
|
|
28
|
-
import { execSync } from "node:child_process";
|
|
29
28
|
import {
|
|
30
29
|
existsSync,
|
|
31
30
|
mkdirSync,
|
|
@@ -35,15 +34,16 @@ import {
|
|
|
35
34
|
writeFileSync,
|
|
36
35
|
} from "node:fs";
|
|
37
36
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
38
|
-
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "
|
|
39
|
-
import { computeModelFit } from "
|
|
40
|
-
import { doneDir, planDir, specDir } from "
|
|
41
|
-
import { CONFIG_FILENAME } from "
|
|
42
|
-
import { loadConfig } from "
|
|
43
|
-
import type { Config } from "
|
|
44
|
-
import { extractExports } from "
|
|
45
|
-
import { readRecentMemDecisions } from "
|
|
46
|
-
import { getStatsData } from "
|
|
37
|
+
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "../analyze.js";
|
|
38
|
+
import { computeModelFit } from "../context/projectHealth.js";
|
|
39
|
+
import { doneDir, planDir, specDir } from "../db/getters.js";
|
|
40
|
+
import { CONFIG_FILENAME } from "../db/index.js";
|
|
41
|
+
import { loadConfig } from "../db/load.js";
|
|
42
|
+
import type { Config } from "../db/types.js";
|
|
43
|
+
import { extractExports } from "../map.js";
|
|
44
|
+
import { readRecentMemDecisions } from "../memory.js";
|
|
45
|
+
import { getStatsData } from "../stats/data.js";
|
|
46
|
+
import { capLines, execGit, SIG_MAX } from "./primitives.js";
|
|
47
47
|
|
|
48
48
|
// One chunk = one module's signatures — past ~40 lines a module is its own
|
|
49
49
|
// reading task, and the whole-SPEC cap below does the final trim.
|
|
@@ -56,7 +56,6 @@ const SCOPE_WARN_FILES = 300;
|
|
|
56
56
|
// Shipped plans/specs that already touched this scope. Capped low on purpose:
|
|
57
57
|
// this is a "go read that first" pointer, not a bibliography.
|
|
58
58
|
const MAX_PRIOR_ART = 5;
|
|
59
|
-
const SIG_MAX = 90;
|
|
60
59
|
// Anchor-safe slug: lowercase, non-alphanumerics → dash.
|
|
61
60
|
const slug = (s: string): string =>
|
|
62
61
|
s
|
|
@@ -224,7 +223,7 @@ next step in the same session is what rule 9 forbids.
|
|
|
224
223
|
1. _(agent fills in — each step must be verifiable)_
|
|
225
224
|
|
|
226
225
|
**Closing a step:** tick its TL;DR box with the sha · \`git commit\` this step's
|
|
227
|
-
files only ·
|
|
226
|
+
files only · then hand off:
|
|
228
227
|
|
|
229
228
|
\`\`\`bash
|
|
230
229
|
fapony mem add note "<what chunk N+1 must know>" --files <f1,f2> ${planRel}
|
|
@@ -257,16 +256,6 @@ interface Chunk {
|
|
|
257
256
|
body: string;
|
|
258
257
|
}
|
|
259
258
|
|
|
260
|
-
// review-seed's cap shape: keep the head, always say how much was cut — a
|
|
261
|
-
// silent cut is indistinguishable from "that was everything".
|
|
262
|
-
function capLines(lines: string[], cap: number, what: string): string[] {
|
|
263
|
-
if (lines.length <= cap) return lines;
|
|
264
|
-
const rest = lines.length - (cap - 1);
|
|
265
|
-
const kept = lines.slice(0, cap - 1);
|
|
266
|
-
kept.push(`… +${rest} more ${what}`);
|
|
267
|
-
return kept;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
259
|
function capChunk(c: Chunk): Chunk {
|
|
271
260
|
return {
|
|
272
261
|
...c,
|
|
@@ -530,15 +519,8 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
530
519
|
// state.db uses (git rev-parse --show-toplevel). Running from a subdir
|
|
531
520
|
// would otherwise mismatch: stats return empty, Context (fapony) always
|
|
532
521
|
// prints "(not enough graded history yet)".
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
worktree = execSync("git rev-parse --show-toplevel", {
|
|
536
|
-
encoding: "utf-8",
|
|
537
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
538
|
-
}).trim();
|
|
539
|
-
} catch {
|
|
540
|
-
worktree = cwd;
|
|
541
|
-
}
|
|
522
|
+
const root = execGit("git rev-parse --show-toplevel", cwd);
|
|
523
|
+
const worktree = root.ok ? root.output.split("\n")[0] : cwd;
|
|
542
524
|
|
|
543
525
|
// Scope: explicit paths win; default is the cwd. Resolved absolutes,
|
|
544
526
|
// deduped — the same path twice is one scope. Nested roots are pruned:
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/seed/primitives.ts — shared helpers for plan-seed + review-seed
|
|
2
|
+
//
|
|
3
|
+
// Git execution, output capping, signature formatting — the pieces both
|
|
4
|
+
// commands need. §0 rule: add-only — never remove or rename exported symbols.
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
// --- Error class ---
|
|
9
|
+
|
|
10
|
+
export class SeedError extends Error {}
|
|
11
|
+
|
|
12
|
+
// --- Git helpers ---
|
|
13
|
+
|
|
14
|
+
export function execGit(
|
|
15
|
+
cmd: string,
|
|
16
|
+
cwd: string,
|
|
17
|
+
): { ok: boolean; output: string; error?: string } {
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync(cmd, {
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: "utf-8",
|
|
22
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
23
|
+
timeout: 15_000,
|
|
24
|
+
});
|
|
25
|
+
return { ok: true, output: output.trim() };
|
|
26
|
+
} catch (e: unknown) {
|
|
27
|
+
const err = e as { stderr?: string; message?: string };
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
output: "",
|
|
31
|
+
error: (err.stderr ?? err.message ?? "").trim(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function gitOk(r: { ok: boolean; error?: string }, cmd: string): void {
|
|
37
|
+
if (!r.ok)
|
|
38
|
+
throw new SeedError(`git failed: ${cmd}\n${r.error ?? "unknown error"}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Values interpolated into a git command line must be plain refs/paths —
|
|
42
|
+
// blocks shell metacharacters before execSync ever sees them.
|
|
43
|
+
const GIT_VALUE_RE = /^[A-Za-z0-9._/{}^~+-]+$/;
|
|
44
|
+
|
|
45
|
+
export function gitValue(kind: string, value: string): string {
|
|
46
|
+
if (!GIT_VALUE_RE.test(value)) {
|
|
47
|
+
throw new SeedError(`invalid ${kind}: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- Output capping ---
|
|
53
|
+
|
|
54
|
+
// Keep the head, always say how much was cut — a silent cut is
|
|
55
|
+
// indistinguishable from "that was everything".
|
|
56
|
+
export function capLines(lines: string[], cap: number, what: string): string[] {
|
|
57
|
+
if (lines.length <= cap) return lines;
|
|
58
|
+
const rest = lines.length - (cap - 1);
|
|
59
|
+
const kept = lines.slice(0, cap - 1);
|
|
60
|
+
kept.push(`… +${rest} more ${what}`);
|
|
61
|
+
return kept;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- Signature formatting ---
|
|
65
|
+
|
|
66
|
+
export const SIG_MAX = 90;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/review-seed.ts — `fapony review-seed [--staged|--commit <sha>|--range <a...b>|--files f1,f2,dir|--plan <PLAN.md>]`
|
|
1
|
+
// src/seed/review-seed.ts — `fapony review-seed [--staged|--commit <sha>|--range <a...b>|--files f1,f2,dir|--plan <PLAN.md>]`
|
|
2
2
|
//
|
|
3
3
|
// Seeds a code review with the deterministic facts of the scope the agent
|
|
4
4
|
// asked about: which files changed (per the exact git expression, echoed),
|
|
@@ -12,14 +12,13 @@
|
|
|
12
12
|
// cite rule 5 "never write into a target worktree", dropped 2026-09-17 because
|
|
13
13
|
// four commands broke it; being read-only was always a property of this
|
|
14
14
|
// command, never of that rule. Facts only: nothing here says broken/fixed
|
|
15
|
-
// — judgment lives in the reviewer and
|
|
15
|
+
// — judgment lives in the reviewer and mem log, never in
|
|
16
16
|
// this output. Deterministic: same input, same bytes, no LLM.
|
|
17
17
|
//
|
|
18
18
|
// Composes existing producers — buildGraph (analyze.ts) for importers/untested,
|
|
19
19
|
// extractExports (map.ts) for signatures. One flag = one declared git call;
|
|
20
20
|
// no magic parsing.
|
|
21
21
|
|
|
22
|
-
import { execSync } from "node:child_process";
|
|
23
22
|
import type { Stats } from "node:fs";
|
|
24
23
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
25
24
|
import { join } from "node:path";
|
|
@@ -30,9 +29,10 @@ import {
|
|
|
30
29
|
isTestedThroughBarrels,
|
|
31
30
|
isTestFile,
|
|
32
31
|
SCAN_EXTS,
|
|
33
|
-
} from "
|
|
34
|
-
import { extractBody, extractExports } from "
|
|
35
|
-
import { assertSafe } from "
|
|
32
|
+
} from "../analyze.js";
|
|
33
|
+
import { extractBody, extractExports } from "../map.js";
|
|
34
|
+
import { assertSafe } from "../safety.js";
|
|
35
|
+
import { execGit, gitOk, gitValue, SeedError, SIG_MAX } from "./primitives.js";
|
|
36
36
|
|
|
37
37
|
const WRAP_WIDTH = 88;
|
|
38
38
|
// The changed list is the review's scope boundary, not context: a file hidden
|
|
@@ -58,7 +58,6 @@ const LOOKUP_OUTPUT_CAP = 120;
|
|
|
58
58
|
// files is already past "this component area" into "the whole tree" — cut
|
|
59
59
|
// there and say so, a folder-shaped wall is not an answer either.
|
|
60
60
|
// Signature text cap per symbol (same trim as map.ts's file view).
|
|
61
|
-
const SIG_MAX = 90;
|
|
62
61
|
const DISCLAIMER =
|
|
63
62
|
"static graph only — seed is where to enter, not what is verified";
|
|
64
63
|
const USAGE =
|
|
@@ -70,52 +69,6 @@ const MAX_BODY_LINES = 80;
|
|
|
70
69
|
const MAX_CALLER_FILES = 12;
|
|
71
70
|
const MAX_CALLER_HITS = 20;
|
|
72
71
|
|
|
73
|
-
export class SeedError extends Error {}
|
|
74
|
-
|
|
75
|
-
// --- Git helpers (same shape as collect.ts execGitSafe) ---
|
|
76
|
-
|
|
77
|
-
function execGit(
|
|
78
|
-
cmd: string,
|
|
79
|
-
cwd: string,
|
|
80
|
-
): { ok: boolean; output: string; error?: string } {
|
|
81
|
-
try {
|
|
82
|
-
const output = execSync(cmd, {
|
|
83
|
-
cwd,
|
|
84
|
-
encoding: "utf-8",
|
|
85
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
86
|
-
timeout: 15_000,
|
|
87
|
-
});
|
|
88
|
-
return { ok: true, output: output.trim() };
|
|
89
|
-
} catch (e: unknown) {
|
|
90
|
-
const err = e as { stderr?: string; message?: string };
|
|
91
|
-
return {
|
|
92
|
-
ok: false,
|
|
93
|
-
output: "",
|
|
94
|
-
error: (err.stderr ?? err.message ?? "").trim(),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// A scope's primary git call failing must never masquerade as "nothing
|
|
100
|
-
// changed" — surface it. merge-base failures are tolerated (label fallback).
|
|
101
|
-
function gitOk(r: { ok: boolean; error?: string }, cmd: string): void {
|
|
102
|
-
if (!r.ok)
|
|
103
|
-
throw new SeedError(
|
|
104
|
-
`review-seed: git failed: ${cmd}\n${r.error ?? "unknown error"}`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Values interpolated into a git command line must be plain refs/paths —
|
|
109
|
-
// blocks shell metacharacters before execSync ever sees them.
|
|
110
|
-
const GIT_VALUE_RE = /^[A-Za-z0-9._/{}^~+-]+$/;
|
|
111
|
-
|
|
112
|
-
function gitValue(kind: string, value: string): string {
|
|
113
|
-
if (!GIT_VALUE_RE.test(value)) {
|
|
114
|
-
throw new SeedError(`review-seed: invalid ${kind}: ${value}`);
|
|
115
|
-
}
|
|
116
|
-
return value;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
72
|
// --- Scope flags: exactly one source of scope ---
|
|
120
73
|
|
|
121
74
|
type Scope =
|
|
@@ -233,7 +186,7 @@ function parseScope(args: string[]): Scope {
|
|
|
233
186
|
|
|
234
187
|
// --- Scope resolution: one flag = one declared git call ---
|
|
235
188
|
|
|
236
|
-
interface FileEntry {
|
|
189
|
+
export interface FileEntry {
|
|
237
190
|
path: string;
|
|
238
191
|
ins: number | null;
|
|
239
192
|
del: number | null;
|
package/src/session/helpers.ts
CHANGED
|
@@ -270,7 +270,7 @@ export function readDetailFromDb(
|
|
|
270
270
|
.all(...filter.params) as DetailPerSessionToolRow[];
|
|
271
271
|
|
|
272
272
|
// Context bytes per tool — summed `state.output` length. Same signal the
|
|
273
|
-
// Claude Code/Codex readers derive from tool_result blocks, so
|
|
273
|
+
// Claude Code/Codex readers derive from tool_result blocks, so usage-scan
|
|
274
274
|
// stops reporting zero bytes for the SQLite clients.
|
|
275
275
|
const bytesRows = db
|
|
276
276
|
.prepare(
|
package/src/session/registry.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Adding a client: write its reader (src/session/<name>.ts, same
|
|
4
4
|
// PassiveUsageResult shape as an existing one — SQLite or JSONL, whatever the
|
|
5
|
-
// client actually stores), then add one entry below. usage-scan
|
|
6
|
-
//
|
|
5
|
+
// client actually stores), then add one entry below. usage-scan iterates
|
|
6
|
+
// this list; nothing else needs any other change.
|
|
7
7
|
//
|
|
8
8
|
// This does not remove the real work of a new client (reverse-engineering
|
|
9
9
|
// its storage format) — it only removes the "wire it into 2 call sites"
|
|
@@ -16,12 +16,10 @@ import type { PassiveUsageReader } from "./types.js";
|
|
|
16
16
|
import { readZcodeUsage } from "./zcode.js";
|
|
17
17
|
|
|
18
18
|
export interface ClientAdapter {
|
|
19
|
-
/** cache `client` field and
|
|
19
|
+
/** usage-cache.jsonl `client` field and usage-scan label default. */
|
|
20
20
|
key: string;
|
|
21
21
|
/** usage-scan progress label — defaults to `key`. */
|
|
22
22
|
scanLabel?: string;
|
|
23
|
-
/** fapony_usage text-report section label — defaults to `key`. */
|
|
24
|
-
reportLabel?: string;
|
|
25
23
|
/** The one client whose totals lead the report as the unlabeled top-level summary. */
|
|
26
24
|
primary?: boolean;
|
|
27
25
|
read: PassiveUsageReader;
|
|
@@ -38,7 +36,6 @@ export const CLIENTS: ClientAdapter[] = [
|
|
|
38
36
|
{
|
|
39
37
|
key: "claude_code",
|
|
40
38
|
scanLabel: "claude-code",
|
|
41
|
-
reportLabel: "claude code",
|
|
42
39
|
read: readClaudeCodeUsage,
|
|
43
40
|
},
|
|
44
41
|
{ key: "codex", read: readCodexUsage },
|
package/src/setup.ts
CHANGED
|
@@ -229,7 +229,7 @@ export async function cmdSetup(deps: SetupDeps = {}): Promise<void> {
|
|
|
229
229
|
│ fapony install --platform claude │
|
|
230
230
|
│ │
|
|
231
231
|
│ 2. Ask your agent: │
|
|
232
|
-
│ "
|
|
232
|
+
│ "Find past decisions with mem_find" │
|
|
233
233
|
│ │
|
|
234
234
|
│ 3. Read a run's report: │
|
|
235
235
|
│ fapony report <run-id> │
|
package/src/stats/data.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { join } from "node:path";
|
|
|
5
5
|
import { type Event, openDb, PLAN_DIR, type Run } from "../db/index.js";
|
|
6
6
|
import { loadConfig } from "../db/load.js";
|
|
7
7
|
import { enrichGateWindows } from "../gates.js";
|
|
8
|
-
import { avg, minutesBetween } from "../math.js";
|
|
9
8
|
import { REASON_CODES, REGIME_CODES } from "../mcp/types.js";
|
|
10
9
|
import {
|
|
11
10
|
isPassFamily,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
readPassiveUsage,
|
|
21
20
|
readZcodeUsage,
|
|
22
21
|
} from "../session/index.js";
|
|
22
|
+
import { avg, minutesBetween } from "../util.js";
|
|
23
23
|
|
|
24
24
|
// Walks events per run in order and pairs up spawn→route (executor time)
|
|
25
25
|
// and route→gate (review turnaround) per round, since one run row can span
|
package/src/telemetry.ts
CHANGED
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
type Run,
|
|
18
18
|
} from "./db/index.js";
|
|
19
19
|
import { enrichGateWindows } from "./gates.js";
|
|
20
|
-
import { avg, minutesBetween } from "./math.js";
|
|
21
20
|
import { readPassiveUsage } from "./session/index.js";
|
|
21
|
+
import { avg, minutesBetween } from "./util.js";
|
|
22
22
|
|
|
23
23
|
// ─── Schema version ────────────────────────────────────────────────────
|
|
24
24
|
|
package/src/util.ts
CHANGED
|
@@ -25,8 +25,69 @@ export function fillPrompt(
|
|
|
25
25
|
return out;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Minutes between two ISO-like timestamps (space separator, UTC assumed). */
|
|
29
|
+
export function minutesBetween(a: string, b: string): number {
|
|
30
|
+
const t0 = new Date(`${a.replace(" ", "T")}Z`).getTime();
|
|
31
|
+
const t1 = new Date(`${b.replace(" ", "T")}Z`).getTime();
|
|
32
|
+
return (t1 - t0) / 60000;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Arithmetic mean of a numeric array. 0 for empty arrays. */
|
|
36
|
+
export function avg(xs: number[]): number {
|
|
37
|
+
return xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
/** True for "y"/"yes" (case-insensitive, trimmed) — the only affirmative answers. */
|
|
29
41
|
export function isAffirmative(answer: string): boolean {
|
|
30
42
|
const normalized = answer.trim().toLowerCase();
|
|
31
43
|
return normalized === "y" || normalized === "yes";
|
|
32
44
|
}
|
|
45
|
+
|
|
46
|
+
// --- Recursive directory walker ---
|
|
47
|
+
|
|
48
|
+
import { readdirSync } from "node:fs";
|
|
49
|
+
import { join } from "node:path";
|
|
50
|
+
|
|
51
|
+
export interface WalkDirOpts {
|
|
52
|
+
/** Max recursion depth (default 4). */
|
|
53
|
+
depth?: number;
|
|
54
|
+
/** Skip node_modules (default true). */
|
|
55
|
+
skipNodeModules?: boolean;
|
|
56
|
+
/** Dot-directories to descend into (default []). Others starting with . are skipped. */
|
|
57
|
+
includeDotDirs?: string[];
|
|
58
|
+
/** Extra filter: return true to collect this dir. Omit to collect all reached dirs. */
|
|
59
|
+
predicate?: (dirPath: string) => boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Recursively walk directories under `root`.
|
|
64
|
+
* Returns absolute paths of dirs for which `predicate` returned true (or all
|
|
65
|
+
* reached dirs when no predicate is given). Skips node_modules by default.
|
|
66
|
+
*/
|
|
67
|
+
export function walkDir(root: string, opts?: WalkDirOpts): string[] {
|
|
68
|
+
const maxDepth = opts?.depth ?? 4;
|
|
69
|
+
const skipNm = opts?.skipNodeModules ?? true;
|
|
70
|
+
const includeDot = new Set(opts?.includeDotDirs ?? []);
|
|
71
|
+
const pred = opts?.predicate;
|
|
72
|
+
const result: string[] = [];
|
|
73
|
+
|
|
74
|
+
const walk = (dir: string, depth: number): void => {
|
|
75
|
+
if (depth > maxDepth) return;
|
|
76
|
+
if (!pred || pred(dir)) result.push(dir);
|
|
77
|
+
try {
|
|
78
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
79
|
+
if (!entry.isDirectory()) continue;
|
|
80
|
+
if (entry.isSymbolicLink()) continue;
|
|
81
|
+
if (skipNm && entry.name === "node_modules") continue;
|
|
82
|
+
if (entry.name.startsWith(".") && !includeDot.has(entry.name)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
walk(join(dir, entry.name), depth + 1);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// ignore unreadable dirs
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
walk(root, 0);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
package/templates/SPEC.md
CHANGED
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## Non-goals
|
|
18
|
+
What this explicitly does **not** do, one line of why each. Widening scope is an
|
|
19
|
+
agent's default; a spec that only says what to build never stopped anything.
|
|
20
|
+
|
|
17
21
|
## Shape (data / API / schema)
|
|
18
22
|
Concrete types, request/response bodies, DB columns — whatever the code needs.
|
|
19
23
|
|
|
20
24
|
## Edge cases
|
|
21
|
-
|
|
25
|
+
Three columns: input → expected → **verify** (a command that actually runs).
|
|
26
|
+
An edge case with no check after it is an opinion, not a requirement — and a
|
|
27
|
+
verify built from a substring of the prose is not a verify (real case: `grep "sed"`
|
|
28
|
+
matched "superseded", so the check passed while the work was missing).
|
|
22
29
|
|
|
23
30
|
## Examples
|
|
24
31
|
Before / after, request / response, sample payloads — as long as it needs to be.
|
package/images/logo.png
DELETED
|
Binary file
|
package/images/logo.webp
DELETED
|
Binary file
|
package/images/logo@400.webp
DELETED
|
Binary file
|
package/images/sample.webp
DELETED
|
Binary file
|
package/images/summary.webp
DELETED
|
Binary file
|