fapony 0.3.4 → 0.3.6
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 +13 -6
- package/package.json +4 -5
- package/skill/move-to-done/SKILL.md +13 -3
- package/skill/plan-with-pony/SKILL.md +6 -2
- package/src/adapters/cli.ts +12 -7
- package/src/adapters/hooks/bug-markers.ts +50 -0
- package/src/adapters/hooks/compute-hint-impact.ts +22 -5
- package/src/adapters/hooks/context-data.ts +16 -10
- package/src/adapters/hooks/index.ts +6 -0
- package/src/adapters/hooks/read-hint.ts +17 -1
- package/src/adapters/hooks/session-start.ts +17 -6
- package/src/adapters/hooks/stop.ts +132 -6
- package/src/adapters/mcp/tools/index.ts +6 -1
- package/src/adapters/mcp/tools/mem.ts +40 -121
- package/src/commands.ts +180 -0
- package/src/core/debt-format.ts +2 -2
- package/src/core/since.ts +26 -0
- package/src/debt/cli.ts +20 -6
- package/src/debt/load.ts +136 -2
- package/src/digest/collect.ts +1 -19
- package/src/hook.ts +4 -0
- package/src/mem/commands/plan.ts +399 -13
- package/src/mem/commands/read.ts +181 -36
- package/src/mem/commands/write.ts +26 -31
- package/src/mem/engine.ts +249 -0
- package/src/mem/index.ts +69 -5
- package/src/memory.ts +2 -1
- package/src/seed/plan-seed.ts +118 -28
- package/src/seed/review-seed.ts +34 -10
- package/src/update.ts +1 -1
- package/src/test.ts +0 -2
package/src/mem/index.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// Moved from templates/mem/mem.ts (2026-09-19) as part of PLAN-agent-one-call chunk 1.
|
|
4
4
|
// Now an export function called by fapony.ts, not a standalone script.
|
|
5
5
|
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { levenshtein } from "../commands.js";
|
|
6
9
|
import { cmdPlanCheck, cmdPlanSweep } from "./commands/plan.js";
|
|
7
10
|
import { cmdDone, cmdFind, cmdKickoff, cmdStale } from "./commands/read.js";
|
|
8
11
|
import { cmdRotate } from "./commands/rotate.js";
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
cmdRelease,
|
|
16
19
|
cmdSynced,
|
|
17
20
|
} from "./commands/write.js";
|
|
21
|
+
import { doneDir, planDir } from "./store.js";
|
|
18
22
|
|
|
19
23
|
const MEM_HELP = `usage: fapony mem [--mem-dir <path>] <sub> [args]
|
|
20
24
|
|
|
@@ -23,7 +27,8 @@ subcommands:
|
|
|
23
27
|
kickoff [<plan.md>] [--pick <n>] open a session + a next-up list
|
|
24
28
|
add <kind> "<text>" --files f1,f2 [spec.md]
|
|
25
29
|
close <id> "<msg>" close a bug
|
|
26
|
-
find "<text>"
|
|
30
|
+
find ["<text>"] [--kind a,b] [--files f1,f2] [--since <N>d|YYYY-MM-DD] [--limit n]
|
|
31
|
+
substring-search every row (archives included)
|
|
27
32
|
done | stale views
|
|
28
33
|
claim <id> | release <id> | synced bookkeeping
|
|
29
34
|
plan-sweep [<plan.md> [--apply]] move shipped plans + fix links
|
|
@@ -53,9 +58,13 @@ example: fapony mem add decision "chose X because Y" --files src/a.ts,src/b.ts`,
|
|
|
53
58
|
tombstone a bug so it stops showing as open work
|
|
54
59
|
--stdin read the message from stdin
|
|
55
60
|
example: fapony mem close mt14 "fixed in a2c6beb"`,
|
|
56
|
-
find: `usage: fapony mem find "<text>"
|
|
57
|
-
substring search over every row's text/spec/ref —
|
|
58
|
-
|
|
61
|
+
find: `usage: fapony mem find ["<text>"] [--kind a,b] [--files f1,f2] [--since <N>d|YYYY-MM-DD] [--limit n]
|
|
62
|
+
substring search over every row's text/spec/ref (rotated archives included) — bookkeeping kinds (close/synced/claim/release) hidden unless --kind names them
|
|
63
|
+
--kind a,b include only these kinds (overrides the bookkeeping default)
|
|
64
|
+
--files f1,f2 rows about these paths (stored files[] first, text/spec/ref fallback)
|
|
65
|
+
--since 7d only rows at or after this time (<N>d or YYYY-MM-DD)
|
|
66
|
+
--limit n max rows returned (default 20, newest first)
|
|
67
|
+
example: fapony mem find "usage-web" --kind bug,decision --limit 5`,
|
|
59
68
|
done: `usage: fapony mem done
|
|
60
69
|
closed rows with their tombstone message
|
|
61
70
|
example: fapony mem done`,
|
|
@@ -86,6 +95,42 @@ reads the Stop-hook payload on stdin — installed by fapony, not run by hand
|
|
|
86
95
|
example: (installed hook only)`,
|
|
87
96
|
};
|
|
88
97
|
|
|
98
|
+
// Subcommand names for top-level did-you-mean (src/commands.ts) — derived
|
|
99
|
+
// from HELP keys so the list cannot drift from the dispatch below.
|
|
100
|
+
export const MEM_SUBCOMMANDS: string[] = Object.keys(HELP);
|
|
101
|
+
|
|
102
|
+
// Chunk 7 (PLAN-seed-and-surface, bug muc85pml): `fapony mem now` used to fall
|
|
103
|
+
// through to kickoff and print an overview ("N entries") instead of an error —
|
|
104
|
+
// a typo that looks like success. Three rules, in order:
|
|
105
|
+
// 1. a known subcommand → dispatch (unchanged)
|
|
106
|
+
// 2. looks like a plan path (.md suffix, a slash, or a file with that name
|
|
107
|
+
// in planDir/doneDir) → kickoff — this is `fapony mem PLAN-x.md`, which
|
|
108
|
+
// must keep working without the `kickoff` word
|
|
109
|
+
// 3. anything else → error, exit 1 (a typo is never a kickoff)
|
|
110
|
+
//
|
|
111
|
+
// `exists` is injectable so tests hit this directly without a store on disk —
|
|
112
|
+
// production passes nothing and reads planDir/doneDir live.
|
|
113
|
+
export function classifyMemArg(
|
|
114
|
+
arg: string,
|
|
115
|
+
exists?: (name: string) => boolean,
|
|
116
|
+
): "subcommand" | "plan" | "unknown" {
|
|
117
|
+
// hasOwn, not `in` or indexing — prototype props ("toString") are not
|
|
118
|
+
// subcommands.
|
|
119
|
+
if (Object.hasOwn(HELP, arg)) return "subcommand";
|
|
120
|
+
if (arg.endsWith(".md") || arg.includes("/")) return "plan";
|
|
121
|
+
const has =
|
|
122
|
+
exists ??
|
|
123
|
+
((name: string): boolean => {
|
|
124
|
+
const base = basename(name);
|
|
125
|
+
const candidates = [name, base, `${name}.md`, `${base}.md`];
|
|
126
|
+
return candidates.some(
|
|
127
|
+
(c) => existsSync(join(planDir, c)) || existsSync(join(doneDir, c)),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
if (has(arg)) return "plan";
|
|
131
|
+
return "unknown";
|
|
132
|
+
}
|
|
133
|
+
|
|
89
134
|
export async function cmdMem(a: string[], memDir?: string): Promise<void> {
|
|
90
135
|
const [cmd, ...rest] = a;
|
|
91
136
|
|
|
@@ -128,8 +173,27 @@ export async function cmdMem(a: string[], memDir?: string): Promise<void> {
|
|
|
128
173
|
cmdPlanCheck(rest);
|
|
129
174
|
} else if (cmd === "rotate") {
|
|
130
175
|
cmdRotate(rest);
|
|
131
|
-
} else {
|
|
176
|
+
} else if (cmd === undefined) {
|
|
132
177
|
// bare `fapony mem` → kickoff (ranked session overview)
|
|
133
178
|
cmdKickoff(rest);
|
|
179
|
+
} else if (classifyMemArg(cmd) === "plan") {
|
|
180
|
+
// `fapony mem PLAN-x.md` — the plan arg rides along (the old else-branch
|
|
181
|
+
// dropped it and printed the no-arg overview instead).
|
|
182
|
+
cmdKickoff([cmd, ...rest]);
|
|
183
|
+
} else {
|
|
184
|
+
console.error(`fapony mem: unknown subcommand "${cmd}"`);
|
|
185
|
+
const scored = MEM_SUBCOMMANDS.map((s) => ({
|
|
186
|
+
s,
|
|
187
|
+
d: levenshtein(cmd, s),
|
|
188
|
+
}))
|
|
189
|
+
.filter((x) => x.d <= 2)
|
|
190
|
+
.sort((x, y) => x.d - y.d || (x.s < y.s ? -1 : 1))
|
|
191
|
+
.slice(0, 3)
|
|
192
|
+
.map((x) => `"fapony mem ${x.s}"`);
|
|
193
|
+
if (scored.length > 0) {
|
|
194
|
+
console.error(`did you mean ${scored.join(" or ")}?`);
|
|
195
|
+
}
|
|
196
|
+
console.error(`subcommands: ${MEM_SUBCOMMANDS.join(" ")}`);
|
|
197
|
+
process.exit(1);
|
|
134
198
|
}
|
|
135
199
|
}
|
package/src/memory.ts
CHANGED
|
@@ -74,6 +74,7 @@ export function claimMemory(
|
|
|
74
74
|
config: Config,
|
|
75
75
|
worktree: string,
|
|
76
76
|
memId: string,
|
|
77
|
+
timeoutMs = 15_000,
|
|
77
78
|
): boolean {
|
|
78
79
|
const mem = resolveMemoryConfig(config, worktree);
|
|
79
80
|
if (!mem) return false;
|
|
@@ -83,7 +84,7 @@ export function claimMemory(
|
|
|
83
84
|
execSync(cmd.join(" "), {
|
|
84
85
|
cwd: worktree,
|
|
85
86
|
stdio: ["pipe", "pipe", "pipe"],
|
|
86
|
-
timeout:
|
|
87
|
+
timeout: timeoutMs,
|
|
87
88
|
});
|
|
88
89
|
return true;
|
|
89
90
|
} catch {
|
package/src/seed/plan-seed.ts
CHANGED
|
@@ -35,7 +35,6 @@ import {
|
|
|
35
35
|
} from "node:fs";
|
|
36
36
|
import { basename, join, relative, resolve, sep } from "node:path";
|
|
37
37
|
import { collectSourceFiles, isSkippedDir, SCAN_EXTS } from "../analyze.js";
|
|
38
|
-
import { computeModelFit } from "../context/projectHealth.js";
|
|
39
38
|
import {
|
|
40
39
|
CONFIG_FILENAME,
|
|
41
40
|
type Config,
|
|
@@ -46,7 +45,6 @@ import {
|
|
|
46
45
|
} from "../core/config.js";
|
|
47
46
|
import { extractExports } from "../map.js";
|
|
48
47
|
import { readRecentMemDecisions } from "../memory.js";
|
|
49
|
-
import { getStatsData } from "../stats/data.js";
|
|
50
48
|
import { capLines, execGit, SIG_MAX } from "./primitives.js";
|
|
51
49
|
|
|
52
50
|
// One chunk = one module's signatures — past ~40 lines a module is its own
|
|
@@ -60,6 +58,14 @@ const SCOPE_WARN_FILES = 300;
|
|
|
60
58
|
// Shipped plans/specs that already touched this scope. Capped low on purpose:
|
|
61
59
|
// this is a "go read that first" pointer, not a bibliography.
|
|
62
60
|
const MAX_PRIOR_ART = 5;
|
|
61
|
+
// Chunk 4 (PLAN-seed-and-surface): the PLAN names what is already in scope —
|
|
62
|
+
// one line per scope file with its export names. SPEC-only seeds never gave
|
|
63
|
+
// PLAN-only readers this pointer, so agents re-derived what export-lines.ts
|
|
64
|
+
// already knew. Capped low: a pointer, not a signature dump.
|
|
65
|
+
const MAX_EXISTING_IN_SCOPE = 15;
|
|
66
|
+
// The PLAN ≤ ~60 contract (§3.1) predates this block — the block yields to it,
|
|
67
|
+
// never grows it. Effective block cap = min(block cap, remaining budget).
|
|
68
|
+
const MAX_PLAN_LINES = 60;
|
|
63
69
|
// Anchor-safe slug: lowercase, non-alphanumerics → dash.
|
|
64
70
|
const slug = (s: string): string =>
|
|
65
71
|
s
|
|
@@ -149,7 +155,84 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
|
|
|
149
155
|
return shown.join("\n");
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
//
|
|
158
|
+
// Chunk 5 (PLAN-seed-and-surface): stdout ends with the plans that already
|
|
159
|
+
// exist — the seed that lands next to a shipped decision without knowing it
|
|
160
|
+
// is the expensive mistake (§8 prior art guards the file, this guards the
|
|
161
|
+
// glance). Active plans first (the ones a new seed must not duplicate),
|
|
162
|
+
// then shipped; capped like every other list here.
|
|
163
|
+
const MAX_PLAN_LIST = 10;
|
|
164
|
+
|
|
165
|
+
function listExistingPlans(
|
|
166
|
+
cwd: string,
|
|
167
|
+
config: Config,
|
|
168
|
+
exclude: string,
|
|
169
|
+
): string[] {
|
|
170
|
+
const items: string[] = [];
|
|
171
|
+
for (const [dir, where] of [
|
|
172
|
+
[planDir(), "plan"],
|
|
173
|
+
[doneDir(config), "done"],
|
|
174
|
+
] as const) {
|
|
175
|
+
let names: string[];
|
|
176
|
+
try {
|
|
177
|
+
names = readdirSync(join(cwd, dir))
|
|
178
|
+
.filter((n) => n.endsWith(".md"))
|
|
179
|
+
.sort();
|
|
180
|
+
} catch {
|
|
181
|
+
continue; // dir missing — nothing seeded yet
|
|
182
|
+
}
|
|
183
|
+
for (const n of names) {
|
|
184
|
+
if (n === exclude) continue; // the file just written, not "existing"
|
|
185
|
+
items.push(`- ${n} (${where})`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (items.length === 0) return ["- (none yet)"];
|
|
189
|
+
return capLines(items, MAX_PLAN_LIST, "plans");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// --- Context (fapony): mem decisions + existing in scope ---
|
|
193
|
+
//
|
|
194
|
+
// No ledger-ranking line here (PLAN-seed-and-surface chunk 6): the ledger is
|
|
195
|
+
// frozen and Positioning rule 2 forbids cross-model ranking claims, so a
|
|
196
|
+
// seeded pointer at it teaches the reader to cite what cannot be cited.
|
|
197
|
+
// computeModelFit() itself stays — `fapony stats` reads it.
|
|
198
|
+
|
|
199
|
+
// One line per scope file naming its exports — `src/debt/scan.ts —
|
|
200
|
+
// scanDebt() · DebtHit`. Files with no exports (or unreadable) are skipped:
|
|
201
|
+
// a pointer lists what is there, not what is not. Sorted for determinism.
|
|
202
|
+
function renderExistingInScope(
|
|
203
|
+
roots: string[],
|
|
204
|
+
cwd: string,
|
|
205
|
+
scoped: boolean,
|
|
206
|
+
cap: number,
|
|
207
|
+
): string[] {
|
|
208
|
+
// No --scope means every file in the repo matches — a list of everything
|
|
209
|
+
// points at nothing (§8 prior art goes quiet for the same reason).
|
|
210
|
+
if (!scoped)
|
|
211
|
+
return ["- _(no --scope — re-seed with --scope <dir> to list exports)_"];
|
|
212
|
+
const abs: string[] = [];
|
|
213
|
+
for (const r of roots) for (const f of scopeSourceFiles(r)) abs.push(f);
|
|
214
|
+
const rels = [...new Set(abs.map((f) => relative(cwd, f) || "."))].sort();
|
|
215
|
+
const lines: string[] = [];
|
|
216
|
+
for (const rel of rels) {
|
|
217
|
+
let source: string;
|
|
218
|
+
try {
|
|
219
|
+
source = readFileSync(join(cwd, rel), "utf-8");
|
|
220
|
+
} catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const scan = extractExports(source);
|
|
224
|
+
if (scan.error || scan.symbols.length === 0) continue;
|
|
225
|
+
// Re-export-only files scan as one `*` per line — dedupe to a single `*`.
|
|
226
|
+
const names = [
|
|
227
|
+
...new Set(
|
|
228
|
+
scan.symbols.map((s) => (s.kind === "fn" ? `${s.name}()` : s.name)),
|
|
229
|
+
),
|
|
230
|
+
];
|
|
231
|
+
lines.push(`- ${rel} — ${names.join(" · ")}`);
|
|
232
|
+
}
|
|
233
|
+
if (lines.length === 0) return ["- _(no exports in scope)_"];
|
|
234
|
+
return capLines(lines, cap, "files in scope (narrow with --scope <path>)");
|
|
235
|
+
}
|
|
153
236
|
|
|
154
237
|
function renderContextFapony(worktree: string): string {
|
|
155
238
|
const lines: string[] = [];
|
|
@@ -164,17 +247,6 @@ function renderContextFapony(worktree: string): string {
|
|
|
164
247
|
.join(" · ")}`
|
|
165
248
|
: "- Decisions on record (mem): _(none — no mem log or empty)_",
|
|
166
249
|
);
|
|
167
|
-
const fits = computeModelFit(getStatsData().byRegime, worktree);
|
|
168
|
-
lines.push(
|
|
169
|
-
fits.length > 0
|
|
170
|
-
? `- Model fit (ledger, min N=5): ${fits
|
|
171
|
-
.map(
|
|
172
|
-
(f) =>
|
|
173
|
-
`${f.regime} → ${f.model} (N=${f.gates}, fail ${(f.failRate * 100).toFixed(0)}%)`,
|
|
174
|
-
)
|
|
175
|
-
.join(" · ")}`
|
|
176
|
-
: "- Model fit: _(not enough graded history yet)_",
|
|
177
|
-
);
|
|
178
250
|
return lines.join("\n");
|
|
179
251
|
}
|
|
180
252
|
|
|
@@ -184,6 +256,7 @@ function planTemplate(
|
|
|
184
256
|
name: string,
|
|
185
257
|
priorArt: string,
|
|
186
258
|
contextFapony: string,
|
|
259
|
+
existingScope: string[],
|
|
187
260
|
specLink: string | null,
|
|
188
261
|
planRel: string,
|
|
189
262
|
): string {
|
|
@@ -204,6 +277,8 @@ status: active
|
|
|
204
277
|
|
|
205
278
|
## Context (fapony)
|
|
206
279
|
${contextFapony}
|
|
280
|
+
### Existing in scope
|
|
281
|
+
${existingScope.join("\n")}
|
|
207
282
|
|
|
208
283
|
## 1. Goal (why)
|
|
209
284
|
_(agent fills in)_
|
|
@@ -221,19 +296,11 @@ _(agent fills in)_
|
|
|
221
296
|
_(agent fills in)_
|
|
222
297
|
|
|
223
298
|
## 6. Steps (what in which order)
|
|
224
|
-
One step = one chunk = one session: finish it, close it, **stop** — starting the
|
|
225
|
-
next step in the same session is what rule 9 forbids.
|
|
299
|
+
One step = one chunk = one session: finish it, close it, **stop** — starting the next step in the same session is what rule 9 forbids.
|
|
226
300
|
|
|
227
301
|
1. _(agent fills in — each step must be verifiable)_
|
|
228
302
|
|
|
229
|
-
**Closing a step:** tick
|
|
230
|
-
files only · then hand off:
|
|
231
|
-
|
|
232
|
-
\`\`\`bash
|
|
233
|
-
fapony mem add note "<what chunk N+1 must know>" --files <f1,f2> ${planRel}
|
|
234
|
-
\`\`\`
|
|
235
|
-
|
|
236
|
-
Next session opens with \`kickoff ${planRel}\` (or \`kickoff ${basename(planRel)}\` — kickoff resolves by filename too, so no need to retype the path).
|
|
303
|
+
**Closing a step:** tick TL;DR with sha · \`git commit\` files only · \`fapony mem add note "<what chunk N+1 must know>" --files <f1,f2> ${planRel}\` · next opens with \`kickoff ${planRel}\` (or \`kickoff PLAN-${name}.md\` — kickoff resolves by filename too).
|
|
237
304
|
|
|
238
305
|
## 7. Examples
|
|
239
306
|
${
|
|
@@ -566,6 +633,14 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
566
633
|
|
|
567
634
|
const priorArt = renderPriorArt(cwd, config, roots);
|
|
568
635
|
const contextFapony = renderContextFapony(worktree);
|
|
636
|
+
const scoped = requested.length > 0;
|
|
637
|
+
// First pass at the block cap — the total-cap check below may shrink it.
|
|
638
|
+
let existingScope = renderExistingInScope(
|
|
639
|
+
roots,
|
|
640
|
+
cwd,
|
|
641
|
+
scoped,
|
|
642
|
+
MAX_EXISTING_IN_SCOPE,
|
|
643
|
+
);
|
|
569
644
|
|
|
570
645
|
let specLink: string | null = null;
|
|
571
646
|
if (withSpec) {
|
|
@@ -593,15 +668,30 @@ export function cmdPlanSeed(args: string[]): void {
|
|
|
593
668
|
}
|
|
594
669
|
|
|
595
670
|
mkdirSync(planDirAbs, { recursive: true });
|
|
596
|
-
|
|
597
|
-
planPath,
|
|
671
|
+
const buildPlan = (existing: string[]): string =>
|
|
598
672
|
planTemplate(
|
|
599
673
|
name,
|
|
600
674
|
priorArt,
|
|
601
675
|
contextFapony,
|
|
676
|
+
existing,
|
|
602
677
|
specLink,
|
|
603
678
|
`${planDir()}/PLAN-${name}.md`,
|
|
604
|
-
)
|
|
605
|
-
);
|
|
679
|
+
);
|
|
680
|
+
let planBody = buildPlan(existingScope);
|
|
681
|
+
// The ≤ ~60 contract predates the §4 block — shrink the block (never the
|
|
682
|
+
// judgment sections) until the file fits. Each item is one line, so cutting
|
|
683
|
+
// `over` items fixes exactly; the re-render recounts the cut honestly.
|
|
684
|
+
const planLines = (b: string): number =>
|
|
685
|
+
b.replace(/\n$/, "").split("\n").length;
|
|
686
|
+
const over = planLines(planBody) - MAX_PLAN_LINES;
|
|
687
|
+
if (over > 0) {
|
|
688
|
+
const budget = Math.max(existingScope.length - over, 1);
|
|
689
|
+
existingScope = renderExistingInScope(roots, cwd, scoped, budget);
|
|
690
|
+
planBody = buildPlan(existingScope);
|
|
691
|
+
}
|
|
692
|
+
writeFileSync(planPath, planBody);
|
|
606
693
|
console.log(`wrote ${planPath}${specLink ? ` + SPEC-${name}.md` : ""}`);
|
|
694
|
+
console.log("Existing plans:");
|
|
695
|
+
for (const l of listExistingPlans(cwd, config, `PLAN-${name}.md`))
|
|
696
|
+
console.log(l);
|
|
607
697
|
}
|
package/src/seed/review-seed.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { Stats } from "node:fs";
|
|
23
23
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
24
|
-
import { join } from "node:path";
|
|
24
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
25
25
|
import {
|
|
26
26
|
buildGraph,
|
|
27
27
|
collectSourceFiles,
|
|
@@ -357,9 +357,35 @@ function shortSha(cwd: string, ref: string): string | null {
|
|
|
357
357
|
return r.ok ? r.output.split("\n")[0] : null;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
// --plan paths arrive in three shapes: absolute, relative to the invocation
|
|
361
|
+
// cwd (e.g. ../../.fapony/plan/X.md from a subdir), or relative to the
|
|
362
|
+
// worktree root (e.g. .fapony/plan/X.md from anywhere). join(worktree, arg)
|
|
363
|
+
// answers only the third — an absolute arg becomes worktree+abs garbage
|
|
364
|
+
// (join, unlike resolve, does not reset on an absolute segment), and a
|
|
365
|
+
// cwd-relative arg with .. escapes above the root. So: absolute as-is,
|
|
366
|
+
// otherwise first-existing-wins between cwd and worktree (bug mucm1own).
|
|
367
|
+
function resolvePlanPath(arg: string, cwd: string, worktree: string): string {
|
|
368
|
+
if (isAbsolute(arg)) {
|
|
369
|
+
if (existsSync(arg)) return arg;
|
|
370
|
+
throw new SeedError(`review-seed: plan file not found: ${arg}`);
|
|
371
|
+
}
|
|
372
|
+
const candidates = [resolve(cwd, arg), join(worktree, arg)];
|
|
373
|
+
for (const p of new Set(candidates)) {
|
|
374
|
+
if (existsSync(p)) return p;
|
|
375
|
+
}
|
|
376
|
+
throw new SeedError(`review-seed: plan file not found: ${arg}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function resolveScope(
|
|
380
|
+
scope: Scope,
|
|
381
|
+
cwd: string,
|
|
382
|
+
worktree: string,
|
|
383
|
+
): ResolvedScope {
|
|
361
384
|
if (scope.kind === "files") {
|
|
362
|
-
const { files, notes, dirExpanded } = expandFilesScope(
|
|
385
|
+
const { files, notes, dirExpanded } = expandFilesScope(
|
|
386
|
+
scope.list,
|
|
387
|
+
worktree,
|
|
388
|
+
);
|
|
363
389
|
return {
|
|
364
390
|
label: dirExpanded ? "--files (dir-expanded)" : "--files (as given)",
|
|
365
391
|
entries: files,
|
|
@@ -367,10 +393,7 @@ function resolveScope(scope: Scope, cwd: string): ResolvedScope {
|
|
|
367
393
|
};
|
|
368
394
|
}
|
|
369
395
|
if (scope.kind === "plan") {
|
|
370
|
-
const planPath =
|
|
371
|
-
if (!existsSync(planPath)) {
|
|
372
|
-
throw new SeedError(`review-seed: plan file not found: ${scope.path}`);
|
|
373
|
-
}
|
|
396
|
+
const planPath = resolvePlanPath(scope.path, cwd, worktree);
|
|
374
397
|
const planFiles = planFrontFiles(readFileSync(planPath, "utf-8"));
|
|
375
398
|
if (planFiles === null) {
|
|
376
399
|
// No files[] to scope from — fall back to the default diff, say so.
|
|
@@ -567,6 +590,7 @@ function findCallers(
|
|
|
567
590
|
function renderLookup(
|
|
568
591
|
flags: LookupFlags,
|
|
569
592
|
scope: Scope,
|
|
593
|
+
cwd: string,
|
|
570
594
|
worktree: string,
|
|
571
595
|
): string {
|
|
572
596
|
const lines: string[] = [];
|
|
@@ -574,7 +598,7 @@ function renderLookup(
|
|
|
574
598
|
|
|
575
599
|
let resolved: ResolvedScope | null = null;
|
|
576
600
|
if (flags.callers) {
|
|
577
|
-
resolved = resolveScope(scope, worktree);
|
|
601
|
+
resolved = resolveScope(scope, cwd, worktree);
|
|
578
602
|
}
|
|
579
603
|
|
|
580
604
|
const hasGraph = (f: string): boolean => {
|
|
@@ -686,10 +710,10 @@ export function renderSeed(args: string[], cwd: string): string {
|
|
|
686
710
|
// the caller asked for one answer, not the review seed around it.
|
|
687
711
|
const lookup = parseLookup(args);
|
|
688
712
|
if (lookup.body.length > 0 || lookup.callers) {
|
|
689
|
-
return renderLookup(lookup, scope, worktree);
|
|
713
|
+
return renderLookup(lookup, scope, cwd, worktree);
|
|
690
714
|
}
|
|
691
715
|
|
|
692
|
-
const resolved = resolveScope(scope, worktree);
|
|
716
|
+
const resolved = resolveScope(scope, cwd, worktree);
|
|
693
717
|
const entries = [...resolved.entries].sort((a, b) =>
|
|
694
718
|
a.path < b.path ? -1 : 1,
|
|
695
719
|
);
|
package/src/update.ts
CHANGED
|
@@ -206,7 +206,7 @@ export async function cmdUpdate(deps: UpdateDeps = {}): Promise<void> {
|
|
|
206
206
|
│ Update complete! │
|
|
207
207
|
│ │
|
|
208
208
|
│ Version: ${newVersion.padEnd(31)}│
|
|
209
|
-
│ Run "
|
|
209
|
+
│ Run "bun run test" to verify. │
|
|
210
210
|
└──────────────────────────────────────────┘
|
|
211
211
|
`);
|
|
212
212
|
}
|
package/src/test.ts
DELETED