fapony 0.3.3 → 0.3.4
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/fapony.ts +3 -116
- package/package.json +2 -2
- package/skill/move-to-done/SKILL.md +3 -2
- package/src/adapters/cli.ts +123 -0
- package/src/adapters/hooks/compute-hint-impact.ts +107 -0
- package/src/adapters/hooks/context-data.ts +102 -0
- package/src/adapters/hooks/edit-hint.ts +195 -0
- package/src/adapters/hooks/git-autonomy.ts +178 -0
- package/src/adapters/hooks/index.ts +79 -0
- package/src/adapters/hooks/mv-guard.ts +52 -0
- package/src/adapters/hooks/read-hint.ts +383 -0
- package/src/adapters/hooks/session-start.ts +101 -0
- package/src/adapters/hooks/stop.ts +302 -0
- package/src/{mcp → adapters/mcp}/evidence.ts +2 -2
- package/src/{mcp → adapters/mcp}/primitives.ts +2 -2
- package/src/{mcp → adapters/mcp}/tools/check.ts +1 -1
- package/src/{mcp → adapters/mcp}/tools/collect.ts +1 -1
- package/src/{mcp → adapters/mcp}/tools/mem.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
- package/src/adapters/mcp/types.ts +18 -0
- package/src/{mcp → adapters/mcp}/worktree.ts +1 -1
- package/src/analyze.ts +1 -1
- package/src/conventions-seed.ts +1 -1
- package/src/core/config.ts +199 -0
- package/src/core/debt-format.ts +107 -0
- package/src/core/debt-types.ts +79 -0
- package/src/core/defaults.ts +8 -0
- package/src/core/enums.ts +34 -0
- package/src/core/format.ts +33 -0
- package/src/core/hint-log.ts +68 -0
- package/src/core/hook-helpers.ts +31 -0
- package/src/core/mem-log.ts +357 -0
- package/src/core/parse.ts +71 -0
- package/src/core/pricing.ts +217 -0
- package/src/core/safety.ts +18 -0
- package/src/core/types.ts +142 -0
- package/src/core/util.ts +105 -0
- package/src/db/store.ts +2 -2
- package/src/debt/cli.ts +1 -1
- package/src/debt/format.ts +2 -107
- package/src/debt/load.ts +1 -1
- package/src/debt/promotion.ts +1 -1
- package/src/debt/types.ts +14 -79
- package/src/digest/collect.ts +4 -3
- package/src/gate.ts +2 -2
- package/src/gates.ts +1 -1
- package/src/hook.ts +69 -1424
- package/src/init-mem.ts +1 -1
- package/src/init.ts +1 -1
- package/src/install/claude.ts +17 -0
- package/src/install/opencode.ts +114 -0
- package/src/install.ts +12 -5
- package/src/lint-baseline.ts +1 -2
- package/src/map.ts +29 -8
- package/src/mem/commands/plan.ts +70 -42
- package/src/mem/store.ts +5 -1
- package/src/memory.ts +24 -387
- package/src/parse.ts +9 -71
- package/src/price/fetch.ts +4 -16
- package/src/price/resolve.ts +12 -213
- package/src/report/cli.ts +3 -3
- package/src/safety.ts +2 -18
- package/src/seed/plan-seed.ts +8 -4
- package/src/seed/primitives.ts +1 -7
- package/src/session/types.ts +14 -128
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +3 -3
- package/src/telemetry.ts +2 -2
- package/src/usage/cache.ts +1 -2
- package/src/usage/cli.ts +1 -1
- package/src/usage/scan.ts +2 -1
- package/src/util.ts +10 -93
- package/src/web/html.ts +2 -33
- package/src/db/defaults.ts +0 -34
- package/src/db/getters.ts +0 -35
- package/src/db/index.ts +0 -7
- package/src/db/load.ts +0 -57
- package/src/db/types.ts +0 -77
- package/src/mcp/types.ts +0 -54
- /package/src/{mcp → adapters/mcp}/tools/index.ts +0 -0
- /package/src/{mcp → adapters/mcp}/transport.ts +0 -0
package/src/init-mem.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { DEFAULT_MEM_DIR, FAPONY_DIR, loadConfig } from "./
|
|
9
|
+
import { DEFAULT_MEM_DIR, FAPONY_DIR, loadConfig } from "./core/config.js";
|
|
10
10
|
import { walkDir } from "./util.js";
|
|
11
11
|
|
|
12
12
|
export function cmdInitMem(args: string[]): void {
|
package/src/init.ts
CHANGED
package/src/install/claude.ts
CHANGED
|
@@ -232,6 +232,7 @@ function installClaudeHooks(dryRun: boolean, deps: InstallDeps): void {
|
|
|
232
232
|
installStopHook(dryRun, deps);
|
|
233
233
|
installReadHintHook(dryRun, deps);
|
|
234
234
|
installEditHintHook(dryRun, deps);
|
|
235
|
+
installMvGuardHook(dryRun, deps);
|
|
235
236
|
installSessionStartHook(dryRun, deps);
|
|
236
237
|
}
|
|
237
238
|
|
|
@@ -276,6 +277,22 @@ function installEditHintHook(dryRun: boolean, deps: InstallDeps): void {
|
|
|
276
277
|
});
|
|
277
278
|
}
|
|
278
279
|
|
|
280
|
+
/**
|
|
281
|
+
* PreToolUse hook on Bash: denies a raw `git mv` of a plan file into a done/
|
|
282
|
+
* directory, pointing at `fapony mem plan-sweep --apply` instead — that
|
|
283
|
+
* command does the link rewrite a plain `git mv` skips. The one fapony hook
|
|
284
|
+
* that blocks besides Stop; matcher "Bash" keeps the spawn off every other
|
|
285
|
+
* tool call.
|
|
286
|
+
*/
|
|
287
|
+
function installMvGuardHook(dryRun: boolean, deps: InstallDeps): void {
|
|
288
|
+
ensureClaudeHook(dryRun, deps, {
|
|
289
|
+
event: "PreToolUse",
|
|
290
|
+
matcher: "Bash",
|
|
291
|
+
subcommand: "hook-mv-guard",
|
|
292
|
+
label: "plan-mv guard",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
279
296
|
/**
|
|
280
297
|
* SessionStart hook: injects `fapony mem kickoff` as context when the repo has
|
|
281
298
|
* a mem log, and stays silent when it does not. Context only — SessionStart
|
package/src/install/opencode.ts
CHANGED
|
@@ -81,9 +81,15 @@ function isConfigured(mcp: Record<string, unknown> | undefined): boolean {
|
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export interface OpencodeInstallOpts {
|
|
85
|
+
/** Opt-in only: write the git-autonomy rewrite plugin. Never default. */
|
|
86
|
+
gitAutonomy?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
84
89
|
export function cmdInstallOpencode(
|
|
85
90
|
dryRun: boolean,
|
|
86
91
|
deps: InstallDeps = {},
|
|
92
|
+
opts: OpencodeInstallOpts = {},
|
|
87
93
|
): void {
|
|
88
94
|
const exitFn = deps.exit ?? defaultExit;
|
|
89
95
|
const getHome = deps.homedir ?? homedir;
|
|
@@ -115,6 +121,7 @@ export function cmdInstallOpencode(
|
|
|
115
121
|
installCommitHintPlugin(dryRun, getHome);
|
|
116
122
|
installEditHintPlugin(dryRun, getHome);
|
|
117
123
|
installSessionStartPlugin(dryRun, getHome);
|
|
124
|
+
if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
|
|
118
125
|
return;
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -140,6 +147,7 @@ export function cmdInstallOpencode(
|
|
|
140
147
|
installCommitHintPlugin(dryRun, getHome);
|
|
141
148
|
installEditHintPlugin(dryRun, getHome);
|
|
142
149
|
installSessionStartPlugin(dryRun, getHome);
|
|
150
|
+
if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
|
|
143
151
|
return;
|
|
144
152
|
}
|
|
145
153
|
|
|
@@ -163,6 +171,7 @@ export function cmdInstallOpencode(
|
|
|
163
171
|
installCommitHintPlugin(dryRun, getHome);
|
|
164
172
|
installEditHintPlugin(dryRun, getHome);
|
|
165
173
|
installSessionStartPlugin(dryRun, getHome);
|
|
174
|
+
if (opts.gitAutonomy) installGitAutonomyPlugin(dryRun, getHome);
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
/**
|
|
@@ -608,3 +617,108 @@ function installSessionStartPlugin(
|
|
|
608
617
|
` session start: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
609
618
|
);
|
|
610
619
|
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* OpenCode plugin — git-autonomy rewrites, the parity of the personal
|
|
623
|
+
* `git-autonomy.ts` plugin. Two hooks because the policy lives on two
|
|
624
|
+
* surfaces the other never sees:
|
|
625
|
+
* `experimental.chat.system.transform` (system prompt) + `tool.definition`
|
|
626
|
+
* (bash tool description).
|
|
627
|
+
*
|
|
628
|
+
* OPT-IN ONLY — written only when `fapony install` gets `--git-autonomy`.
|
|
629
|
+
* Never on the default path, never via `--all` unless the flag rides along.
|
|
630
|
+
* The policy is an opinion (commit-as-you-go), not a utility, and the file
|
|
631
|
+
* name is `fapony-git-autonomy.ts`: the user's own `git-autonomy.ts` is a
|
|
632
|
+
* different file and is never touched — delete it by hand after verifying.
|
|
633
|
+
*
|
|
634
|
+
* Logic lives in src/adapters/hooks/git-autonomy.ts
|
|
635
|
+
* (rewriteGitAutonomySystem / rewriteGitAutonomyTool) — the plugin imports
|
|
636
|
+
* it from the install root, so `git pull` updates every client. No baked
|
|
637
|
+
* regex in the generated body: a baked copy would rot silently when upstream
|
|
638
|
+
* rewords the policy, the same failure this module's gitAutonomyStatus
|
|
639
|
+
* exists to name. Never throws: a rewrite must not break a session start
|
|
640
|
+
* or hide a tool.
|
|
641
|
+
*/
|
|
642
|
+
export function gitAutonomyPluginSource(root: string): string {
|
|
643
|
+
const hookModule = JSON.stringify(join(root, "src", "hook.ts"));
|
|
644
|
+
return `// fapony git-autonomy — rewrites opencode's ask-first git policy to
|
|
645
|
+
// commit-as-you-go on both surfaces that carry it (system prompt +
|
|
646
|
+
// bash tool description), and cuts the tone few-shot examples.
|
|
647
|
+
// OPT-IN: written only by \`fapony install --platform opencode --git-autonomy\`.
|
|
648
|
+
// Generated by \`fapony install\` — logic lives in src/adapters/hooks/git-autonomy.ts.
|
|
649
|
+
import { rewriteGitAutonomySystem, rewriteGitAutonomyTool } from ${hookModule};
|
|
650
|
+
|
|
651
|
+
export const FaponyGitAutonomy = async () => ({
|
|
652
|
+
"experimental.chat.system.transform": async (
|
|
653
|
+
_input: unknown,
|
|
654
|
+
output: { system: string[] },
|
|
655
|
+
) => {
|
|
656
|
+
try {
|
|
657
|
+
for (let i = 0; i < output.system.length; i++) {
|
|
658
|
+
output.system[i] = rewriteGitAutonomySystem(output.system[i]).text;
|
|
659
|
+
}
|
|
660
|
+
} catch {
|
|
661
|
+
// a rewrite must never break a session start
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
"tool.definition": async (
|
|
665
|
+
input: { toolID: string },
|
|
666
|
+
output: { description: string; parameters: unknown },
|
|
667
|
+
) => {
|
|
668
|
+
try {
|
|
669
|
+
const r = rewriteGitAutonomyTool(input.toolID, output.description);
|
|
670
|
+
output.description = r.description;
|
|
671
|
+
} catch {
|
|
672
|
+
// a rewrite must never hide a tool
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
`;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function installGitAutonomyPlugin(
|
|
680
|
+
dryRun: boolean,
|
|
681
|
+
getHome: () => string,
|
|
682
|
+
): void {
|
|
683
|
+
const pluginsDir = join(getHome(), ".config", "opencode", "plugins");
|
|
684
|
+
const pluginPath = join(pluginsDir, "fapony-git-autonomy.ts");
|
|
685
|
+
if (existsSync(pluginPath)) {
|
|
686
|
+
let current = "";
|
|
687
|
+
try {
|
|
688
|
+
current = readFileSync(pluginPath, "utf-8");
|
|
689
|
+
} catch {
|
|
690
|
+
current = "";
|
|
691
|
+
}
|
|
692
|
+
if (current.includes("FaponyGitAutonomy")) {
|
|
693
|
+
// Ours — and generated, so never overwritten. Same stale-check shape
|
|
694
|
+
// as the session-start plugin: an older generation must say so instead
|
|
695
|
+
// of "no change", or a fixed rewrite never reaches the client.
|
|
696
|
+
if (current !== gitAutonomyPluginSource(INSTALL_ROOT)) {
|
|
697
|
+
console.error(
|
|
698
|
+
` git-autonomy: stale fapony plugin — delete ${pluginPath} and re-run install --git-autonomy`,
|
|
699
|
+
);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
console.error(` git-autonomy: already installed — no change`);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
console.error(
|
|
706
|
+
` git-autonomy: ${pluginPath} exists but isn't fapony's — not overwriting.`,
|
|
707
|
+
);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (!dryRun) {
|
|
711
|
+
try {
|
|
712
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
713
|
+
writeFileSync(pluginPath, gitAutonomyPluginSource(INSTALL_ROOT), "utf-8");
|
|
714
|
+
} catch (e) {
|
|
715
|
+
console.error(
|
|
716
|
+
` git-autonomy: failed to write — ${(e as Error).message}`,
|
|
717
|
+
);
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
console.error(
|
|
722
|
+
` git-autonomy: ${dryRun ? "would write" : "wrote"} ${pluginPath}`,
|
|
723
|
+
);
|
|
724
|
+
}
|
package/src/install.ts
CHANGED
|
@@ -37,6 +37,8 @@ export {
|
|
|
37
37
|
cmdInstallOpencode,
|
|
38
38
|
commitHintPluginSource,
|
|
39
39
|
editHintPluginSource,
|
|
40
|
+
gitAutonomyPluginSource,
|
|
41
|
+
type OpencodeInstallOpts,
|
|
40
42
|
readHintPluginSource,
|
|
41
43
|
} from "./install/opencode.js";
|
|
42
44
|
export {
|
|
@@ -58,6 +60,10 @@ export async function cmdInstall(
|
|
|
58
60
|
const platform = args.find((a) => !a.startsWith("--"));
|
|
59
61
|
const dryRun = args.includes("--dry-run");
|
|
60
62
|
const installAll = args.includes("--all");
|
|
63
|
+
// Opt-in only: the git-autonomy rewrite is an opinion (commit-as-you-go),
|
|
64
|
+
// not a utility — never installed unless this flag rides along, including
|
|
65
|
+
// via --all. Opencode-only; other platforms ignore it.
|
|
66
|
+
const gitAutonomy = args.includes("--git-autonomy");
|
|
61
67
|
|
|
62
68
|
// --- explicit platform: original behavior, unchanged ---
|
|
63
69
|
if (platform === "antigravity") {
|
|
@@ -81,12 +87,12 @@ export async function cmdInstall(
|
|
|
81
87
|
return;
|
|
82
88
|
}
|
|
83
89
|
if (platform === "opencode") {
|
|
84
|
-
cmdInstallOpencode(dryRun, deps);
|
|
90
|
+
cmdInstallOpencode(dryRun, deps, { gitAutonomy });
|
|
85
91
|
return;
|
|
86
92
|
}
|
|
87
93
|
if (platform !== undefined) {
|
|
88
94
|
console.error(
|
|
89
|
-
`usage: fapony install --platform antigravity|opencode|claude|cursor|zcode|codex [--dry-run]`,
|
|
95
|
+
`usage: fapony install --platform antigravity|opencode|claude|cursor|zcode|codex [--dry-run] [--git-autonomy]`,
|
|
90
96
|
);
|
|
91
97
|
console.error(
|
|
92
98
|
` supported platforms: antigravity, opencode, claude, cursor, zcode, codex`,
|
|
@@ -125,7 +131,7 @@ export async function cmdInstall(
|
|
|
125
131
|
if (installAll) {
|
|
126
132
|
console.error();
|
|
127
133
|
for (const client of found) {
|
|
128
|
-
installPlatform(client.platform, dryRun, deps);
|
|
134
|
+
installPlatform(client.platform, dryRun, deps, { gitAutonomy });
|
|
129
135
|
}
|
|
130
136
|
return;
|
|
131
137
|
}
|
|
@@ -154,7 +160,7 @@ export async function cmdInstall(
|
|
|
154
160
|
for (const client of found) {
|
|
155
161
|
const answer = await askFn(`install into ${client.platform}?`, "Y");
|
|
156
162
|
if (isAffirmative(answer)) {
|
|
157
|
-
installPlatform(client.platform, dryRun, deps);
|
|
163
|
+
installPlatform(client.platform, dryRun, deps, { gitAutonomy });
|
|
158
164
|
}
|
|
159
165
|
}
|
|
160
166
|
} finally {
|
|
@@ -166,6 +172,7 @@ function installPlatform(
|
|
|
166
172
|
platform: string,
|
|
167
173
|
dryRun: boolean,
|
|
168
174
|
deps: InstallDeps,
|
|
175
|
+
opts: { gitAutonomy?: boolean } = {},
|
|
169
176
|
): void {
|
|
170
177
|
switch (platform) {
|
|
171
178
|
case "antigravity":
|
|
@@ -178,7 +185,7 @@ function installPlatform(
|
|
|
178
185
|
cmdInstallCursor(dryRun, deps);
|
|
179
186
|
break;
|
|
180
187
|
case "opencode":
|
|
181
|
-
cmdInstallOpencode(dryRun, deps);
|
|
188
|
+
cmdInstallOpencode(dryRun, deps, opts);
|
|
182
189
|
break;
|
|
183
190
|
case "zcode":
|
|
184
191
|
cmdInstallZcode(dryRun, deps);
|
package/src/lint-baseline.ts
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
import { execSync } from "node:child_process";
|
|
15
15
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { join, relative } from "node:path";
|
|
17
|
-
import { DEFAULT_EVIDENCE_FILE } from "./
|
|
18
|
-
import { faponyDir } from "./db/load.js";
|
|
17
|
+
import { DEFAULT_EVIDENCE_FILE, faponyDir } from "./core/config.js";
|
|
19
18
|
import { assertSafe } from "./safety.js";
|
|
20
19
|
|
|
21
20
|
const USAGE =
|
package/src/map.ts
CHANGED
|
@@ -41,19 +41,36 @@ export interface ExportScan {
|
|
|
41
41
|
error: string | null;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// --- Parse gate
|
|
44
|
+
// --- Parse gate (injectable: default is Bun.Transpiler, no Node fallback —
|
|
45
|
+
// Bun-only is a repo constraint. Pass a custom scanner to extractExports()
|
|
46
|
+
// to reuse this module outside Bun or in tests.) ---
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
export interface ExportScanner {
|
|
49
|
+
scan(source: string): { exports: string[] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createBunScanner(): ExportScanner {
|
|
53
|
+
const transpiler = new Bun.Transpiler({ loader: "tsx" });
|
|
54
|
+
return {
|
|
55
|
+
scan: (source) => transpiler.scan(source) as { exports: string[] },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let defaultScanner: ExportScanner | null = null;
|
|
60
|
+
|
|
61
|
+
function getDefaultScanner(): ExportScanner {
|
|
62
|
+
if (!defaultScanner) defaultScanner = createBunScanner();
|
|
63
|
+
return defaultScanner;
|
|
64
|
+
}
|
|
47
65
|
|
|
48
66
|
interface ScanResult {
|
|
49
67
|
error: string | null;
|
|
50
68
|
exports: string[];
|
|
51
69
|
}
|
|
52
70
|
|
|
53
|
-
function scanSource(source: string): ScanResult {
|
|
71
|
+
function scanSource(source: string, scanner: ExportScanner): ScanResult {
|
|
54
72
|
try {
|
|
55
|
-
|
|
56
|
-
const scanned = transpiler.scan(source) as { exports: string[] };
|
|
73
|
+
const scanned = scanner.scan(source);
|
|
57
74
|
return { error: null, exports: scanned.exports };
|
|
58
75
|
} catch (e) {
|
|
59
76
|
return {
|
|
@@ -150,8 +167,12 @@ function braceNames(
|
|
|
150
167
|
|
|
151
168
|
const VAR_DECL_RE = /^export\s+(?:const|let|var)\b/;
|
|
152
169
|
|
|
153
|
-
export function extractExports(
|
|
154
|
-
|
|
170
|
+
export function extractExports(
|
|
171
|
+
source: string,
|
|
172
|
+
scanner?: ExportScanner,
|
|
173
|
+
): ExportScan {
|
|
174
|
+
const s = scanner ?? getDefaultScanner();
|
|
175
|
+
const scanned = scanSource(source, s);
|
|
155
176
|
if (scanned.error) return { symbols: [], error: scanned.error };
|
|
156
177
|
|
|
157
178
|
// Ambient declarations (`export declare ...`) are invisible to scan(), so
|
|
@@ -161,7 +182,7 @@ export function extractExports(source: string): ExportScan {
|
|
|
161
182
|
/^([ \t]*)export\s+declare\s+/gm,
|
|
162
183
|
"$1export ",
|
|
163
184
|
);
|
|
164
|
-
const strippedScan = scanSource(stripped);
|
|
185
|
+
const strippedScan = scanSource(stripped, s);
|
|
165
186
|
const valid = new Set([
|
|
166
187
|
...scanned.exports,
|
|
167
188
|
...(strippedScan.error ? [] : strippedScan.exports),
|
package/src/mem/commands/plan.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// rationale: moving by hand = chasing relative links yourself (in the file + files that link to it) → the step gets skipped often
|
|
3
3
|
// no arg = report only (safe, shows every kickoff/stale run)
|
|
4
4
|
// <file.md> = check a single file, is it ready to move
|
|
5
|
-
// <file.md> --apply = git mv + fix markdown links inside the file + fix inbound links from
|
|
6
|
-
// + warn about plain-text mentions (detect-only, no auto-fix)
|
|
5
|
+
// <file.md> --apply = git mv + fix markdown links inside the file + fix inbound links from every .md under .fapony/
|
|
6
|
+
// (plan/ + done/ + spec/) + warn about plain-text mentions (detect-only, no auto-fix)
|
|
7
|
+
// + warn about tracked files outside .fapony/ that mention the filename (detect-only)
|
|
7
8
|
|
|
8
9
|
import {
|
|
9
10
|
existsSync,
|
|
@@ -12,10 +13,20 @@ import {
|
|
|
12
13
|
readFileSync,
|
|
13
14
|
writeFileSync,
|
|
14
15
|
} from "node:fs";
|
|
15
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
16
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
16
17
|
|
|
17
18
|
import { openRows } from "../selectors.js";
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
doneDir,
|
|
21
|
+
memCmd,
|
|
22
|
+
nextId,
|
|
23
|
+
planBase,
|
|
24
|
+
planDir,
|
|
25
|
+
put,
|
|
26
|
+
rel,
|
|
27
|
+
root,
|
|
28
|
+
rows,
|
|
29
|
+
} from "../store.js";
|
|
19
30
|
|
|
20
31
|
const SHIPPED = /^>\s*✅/m;
|
|
21
32
|
const FRONT = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
@@ -52,6 +63,11 @@ const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
52
63
|
// the file moved dir (same content) — an old markdown link meant the old path relative to oldDir, must re-relativize via newDir
|
|
53
64
|
// resolve from newDir (where the file is now) — if the target also moved to the same dir → plain filename,
|
|
54
65
|
// if the target stayed in oldDir → ../target (both correct)
|
|
66
|
+
//
|
|
67
|
+
// Runs on EVERY move, including the standard sibling layout (plan/ → done/ beside
|
|
68
|
+
// it): same depth is not the same directory, so a sibling link like
|
|
69
|
+
// [other](PLAN-other.md) breaks unless rewritten to ../plan/PLAN-other.md.
|
|
70
|
+
// Skipping this pass left exactly that dangling (mtjn3ldk).
|
|
55
71
|
// pass 1 only — fix only [text](target) markdown links, do not touch plain text
|
|
56
72
|
export const rewriteMovedFileLinks = (
|
|
57
73
|
file: string,
|
|
@@ -65,11 +81,17 @@ export const rewriteMovedFileLinks = (
|
|
|
65
81
|
const [target, anchor] = t.split("#");
|
|
66
82
|
if (!target || /^(https?:|mailto:|\/)/.test(target)) return m;
|
|
67
83
|
// resolve from where the file IS now (newDir); if target doesn't exist
|
|
68
|
-
// there, fall back to oldDir (target stayed in original location)
|
|
84
|
+
// there, fall back to oldDir (target stayed in original location).
|
|
85
|
+
// If it exists in neither, the link is already broken — leave it for
|
|
86
|
+
// plan-check instead of fabricating a new broken path.
|
|
69
87
|
const inNew = resolve(newDir, target);
|
|
70
|
-
const
|
|
88
|
+
const inOld = resolve(oldDir, target);
|
|
89
|
+
const abs = existsSync(inNew) ? inNew : existsSync(inOld) ? inOld : null;
|
|
90
|
+
if (!abs) return m;
|
|
91
|
+
const next = `](${relative(newDir, abs) || "."}${anchor ? `#${anchor}` : ""})`;
|
|
92
|
+
if (next === m) return m;
|
|
71
93
|
n++;
|
|
72
|
-
return
|
|
94
|
+
return next;
|
|
73
95
|
});
|
|
74
96
|
|
|
75
97
|
if (n) writeFileSync(file, out);
|
|
@@ -172,11 +194,20 @@ export const cmdPlanSweep = (a: string[]) => {
|
|
|
172
194
|
return;
|
|
173
195
|
}
|
|
174
196
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
197
|
+
// target arrives as a bare name (PLAN-x.md, the SKILL form) or a repo-relative
|
|
198
|
+
// path (.fapony/plan/PLAN-x.md, the example form) — both must resolve, so try
|
|
199
|
+
// planDir first, then the repo root, then the bare basename as a last resort.
|
|
200
|
+
const src = [
|
|
201
|
+
join(dir, target),
|
|
202
|
+
join(root, target),
|
|
203
|
+
join(dir, basename(target)),
|
|
204
|
+
].find((p) => existsSync(p));
|
|
205
|
+
if (!src) {
|
|
206
|
+
console.error(`${target} not found (looked in ${rel(dir)}/ and repo root)`);
|
|
178
207
|
process.exit(1);
|
|
179
208
|
}
|
|
209
|
+
const name = basename(src);
|
|
210
|
+
const srcDir = dirname(src);
|
|
180
211
|
const shipped = hasShippedHeader(src);
|
|
181
212
|
if (!apply) {
|
|
182
213
|
console.log(
|
|
@@ -191,21 +222,21 @@ export const cmdPlanSweep = (a: string[]) => {
|
|
|
191
222
|
// but then run --apply directly and skip everything → risky when an agent ships automatically with no human check, so hard block
|
|
192
223
|
if (!shipped && !process.env.MEM_FORCE) {
|
|
193
224
|
console.error(
|
|
194
|
-
`${
|
|
225
|
+
`${name}: no ✅ shipped header at the top — refusing to move (MEM_FORCE=1 to override)`,
|
|
195
226
|
);
|
|
196
227
|
process.exit(1);
|
|
197
228
|
}
|
|
198
|
-
const openSpec =
|
|
229
|
+
const openSpec = rel(src);
|
|
199
230
|
const openN = openRows(rows()).filter((r) => r.spec === openSpec);
|
|
200
231
|
if (openN.length && !process.env.MEM_FORCE) {
|
|
201
232
|
console.error(
|
|
202
|
-
`${
|
|
233
|
+
`${name}: still has ${openN.length} open row(s) (next/bug/hold/decision/note) — close them or move the spec first (MEM_FORCE=1 to override):\n` +
|
|
203
234
|
openN.map((r) => ` [${r.id}] ${r.kind} ${r.text}`).join("\n"),
|
|
204
235
|
);
|
|
205
236
|
process.exit(1);
|
|
206
237
|
}
|
|
207
238
|
|
|
208
|
-
const dst = join(doneDir,
|
|
239
|
+
const dst = join(doneDir, name);
|
|
209
240
|
if (existsSync(dst)) {
|
|
210
241
|
console.error(`${rel(dst)} already exists`);
|
|
211
242
|
process.exit(1);
|
|
@@ -221,14 +252,17 @@ export const cmdPlanSweep = (a: string[]) => {
|
|
|
221
252
|
process.exit(1);
|
|
222
253
|
}
|
|
223
254
|
|
|
224
|
-
//
|
|
225
|
-
// the
|
|
226
|
-
|
|
227
|
-
const ownLinks =
|
|
255
|
+
// own links always need re-relativizing — the file changed directory even in
|
|
256
|
+
// the sibling layout (plan/ → done/ beside it), so sibling links dangle
|
|
257
|
+
// unless rewritten (see rewriteMovedFileLinks).
|
|
258
|
+
const ownLinks = rewriteMovedFileLinks(dst, srcDir, doneDir);
|
|
228
259
|
|
|
260
|
+
// inbound: every .md under .fapony/ can link here — other active plans,
|
|
261
|
+
// shipped plans in done/ (they reference each other), and specs. Scanning
|
|
262
|
+
// plan/ only left done/+spec/ links dangling (mtl15q4y).
|
|
229
263
|
let inbound = 0;
|
|
230
264
|
let inboundFiles = 0;
|
|
231
|
-
for (const f of mdFiles(
|
|
265
|
+
for (const f of mdFiles(planBase)) {
|
|
232
266
|
if (f === dst) continue;
|
|
233
267
|
const n = rewriteMarkdownLinks(f, src, dst);
|
|
234
268
|
if (n) {
|
|
@@ -238,13 +272,9 @@ export const cmdPlanSweep = (a: string[]) => {
|
|
|
238
272
|
}
|
|
239
273
|
|
|
240
274
|
console.log(`moved ${rel(src)} → ${rel(dst)}`);
|
|
275
|
+
console.log(`links rewritten inside the file: ${ownLinks}`);
|
|
241
276
|
console.log(
|
|
242
|
-
|
|
243
|
-
? `links rewritten inside the file: ${ownLinks}`
|
|
244
|
-
: `links rewritten inside the file: 0 (same depth, existing links still resolve)`,
|
|
245
|
-
);
|
|
246
|
-
console.log(
|
|
247
|
-
`inbound links rewritten: ${inbound} in ${inboundFiles} file(s) (scanned ${rel(dir)}/** only)`,
|
|
277
|
+
`inbound links rewritten: ${inbound} in ${inboundFiles} file(s) (scanned ${rel(planBase)}/**)`,
|
|
248
278
|
);
|
|
249
279
|
|
|
250
280
|
// log decision — record ship event (reuse existing kind, no new schema)
|
|
@@ -252,7 +282,7 @@ export const cmdPlanSweep = (a: string[]) => {
|
|
|
252
282
|
put({
|
|
253
283
|
id: nextId(rows()),
|
|
254
284
|
kind: "decision",
|
|
255
|
-
text: `${
|
|
285
|
+
text: `${name} shipped → ${rel(dst)}`,
|
|
256
286
|
spec: doneSpec,
|
|
257
287
|
});
|
|
258
288
|
// ponytail: this decision *is* the move itself, nothing to write back into the spec — do not mark synced
|
|
@@ -262,35 +292,33 @@ export const cmdPlanSweep = (a: string[]) => {
|
|
|
262
292
|
// plain-text mention detection (detect-only, no auto-fix)
|
|
263
293
|
let plainTextTotal = 0;
|
|
264
294
|
const plainTextFiles: string[] = [];
|
|
265
|
-
for (const f of mdFiles(
|
|
266
|
-
|
|
295
|
+
for (const f of mdFiles(planBase)) {
|
|
296
|
+
if (f === dst) continue;
|
|
297
|
+
const n = countPlainTextMentions(f, name);
|
|
267
298
|
if (n) {
|
|
268
299
|
plainTextTotal += n;
|
|
269
|
-
plainTextFiles.push(f.replace(`${
|
|
300
|
+
plainTextFiles.push(f.replace(`${planBase}/`, ""));
|
|
270
301
|
}
|
|
271
302
|
}
|
|
272
303
|
if (plainTextTotal > 0) {
|
|
273
304
|
console.log(
|
|
274
|
-
`⚠ ${plainTextTotal} plain-text mention(s) in ${plainTextFiles.length} file(s) under ${rel(
|
|
305
|
+
`⚠ ${plainTextTotal} plain-text mention(s) in ${plainTextFiles.length} file(s) under ${rel(planBase)}/ — grep and update the paths yourself:\n${plainTextFiles.join("\n")}`,
|
|
275
306
|
);
|
|
276
307
|
}
|
|
277
308
|
|
|
278
|
-
// files outside
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
rel(dirname(planDir)),
|
|
287
|
-
`:!${rel(dir)}`,
|
|
288
|
-
])
|
|
309
|
+
// files outside .fapony/ that mention the filename — detect-only.
|
|
310
|
+
// Everything under planBase/ was auto-fixed above, so what remains is repo
|
|
311
|
+
// docs and prose (README, docs/, CLAUDE.md) with hand-written paths.
|
|
312
|
+
// cwd: root — the pathspecs are root-relative no matter where fapony runs from.
|
|
313
|
+
const grep = Bun.spawnSync(
|
|
314
|
+
["git", "grep", "-l", name, "--", ".", `:!${rel(planBase)}`],
|
|
315
|
+
{ cwd: root },
|
|
316
|
+
)
|
|
289
317
|
.stdout.toString()
|
|
290
318
|
.trim();
|
|
291
319
|
if (grep)
|
|
292
320
|
console.log(
|
|
293
|
-
`⚠ files outside ${rel(
|
|
321
|
+
`⚠ files outside ${rel(planBase)}/ still mention "${name}" — check them yourself (not auto-fixed):\n${grep}`,
|
|
294
322
|
);
|
|
295
323
|
};
|
|
296
324
|
|
package/src/mem/store.ts
CHANGED
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
} from "node:fs";
|
|
16
16
|
import { hostname, networkInterfaces } from "node:os";
|
|
17
17
|
import { basename, dirname, join, relative } from "node:path";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
CONFIG_FILENAME,
|
|
20
|
+
DEFAULT_MEM_DIR,
|
|
21
|
+
FAPONY_DIR,
|
|
22
|
+
} from "../core/config.js";
|
|
19
23
|
import { whereMemDir } from "../memory.js";
|
|
20
24
|
|
|
21
25
|
// --- types ---
|