fapony 0.3.0 → 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/README.md +87 -65
- package/fapony.ts +3 -116
- package/package.json +6 -5
- package/skill/move-to-done/SKILL.md +22 -32
- package/skill/review-pony/SKILL.md +27 -58
- 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 +3 -3
- 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/index.ts +0 -74
- package/src/{mcp → adapters/mcp}/tools/mem.ts +3 -3
- package/src/{mcp → adapters/mcp}/tools/report.ts +4 -4
- package/src/{mcp → adapters/mcp}/transport.ts +4 -39
- package/src/adapters/mcp/types.ts +18 -0
- package/src/{mcp → adapters/mcp}/worktree.ts +2 -2
- package/src/analyze.ts +2 -2
- 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 +5 -5
- package/src/gates.ts +1 -1
- package/src/hook.ts +69 -1337
- package/src/init-mem.ts +58 -72
- package/src/init.ts +13 -17
- package/src/install/antigravity.ts +112 -0
- package/src/install/claude.ts +19 -123
- package/src/install/detect.ts +17 -7
- package/src/install/opencode.ts +167 -26
- package/src/install.ts +23 -7
- package/src/lint-baseline.ts +1 -2
- package/src/map.ts +29 -8
- package/src/mem/commands/plan.ts +70 -42
- package/src/mem/commands/read.ts +219 -146
- package/src/mem/index.ts +4 -13
- 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/{plan-seed.ts → seed/plan-seed.ts} +18 -32
- package/src/seed/primitives.ts +60 -0
- package/src/{review-seed.ts → seed/review-seed.ts} +7 -54
- package/src/session/types.ts +14 -128
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +4 -4
- package/src/telemetry.ts +3 -3
- 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 -32
- package/src/web/html.ts +2 -33
- 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/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/math.ts +0 -13
- package/src/mcp/tools/verdict.ts +0 -161
- package/src/mcp/types.ts +0 -54
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/commands/read.ts
CHANGED
|
@@ -3,13 +3,28 @@
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import { baselinePath, readEvidenceLintCmd } from "../../lint-baseline.js";
|
|
6
|
-
import { doneLines, fmtClose, fmtRow
|
|
6
|
+
import { doneLines, fmtClose, fmtRow } from "../render.js";
|
|
7
7
|
import { claimsOf, openRows, staleReport } from "../selectors.js";
|
|
8
8
|
import type { CloseRow, WorkRow } from "../store.js";
|
|
9
9
|
import { allRows, app, memCmd, planDir, root, rows } from "../store.js";
|
|
10
10
|
import { planSweepCmd, shippedNotMoved } from "./plan.js";
|
|
11
11
|
import { THRESHOLD } from "./rotate.js";
|
|
12
12
|
|
|
13
|
+
/** Files changed on this branch vs dev — empty set when dev is missing or diff fails. */
|
|
14
|
+
function getBranchDiffFiles(cwd: string): Set<string> {
|
|
15
|
+
try {
|
|
16
|
+
const p = Bun.spawnSync(["git", "diff", "--name-only", "dev...HEAD"], {
|
|
17
|
+
cwd,
|
|
18
|
+
stdout: "pipe",
|
|
19
|
+
stderr: "pipe",
|
|
20
|
+
});
|
|
21
|
+
if (p.exitCode !== 0) return new Set();
|
|
22
|
+
return new Set(p.stdout.toString().trim().split("\n").filter(Boolean));
|
|
23
|
+
} catch {
|
|
24
|
+
return new Set();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
13
28
|
const rotateLine = (n: number) =>
|
|
14
29
|
n >= THRESHOLD
|
|
15
30
|
? `\n## 🗜 log ${n} rows (≥ ${THRESHOLD}) — run: ${memCmd} rotate --apply`
|
|
@@ -24,26 +39,20 @@ const planSweepLine = () => {
|
|
|
24
39
|
: "";
|
|
25
40
|
};
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
stale.map((l) => `- ${l}`).join("\n"),
|
|
42
|
-
);
|
|
43
|
-
const sweep = planSweepLine();
|
|
44
|
-
if (sweep) console.log(sweep);
|
|
45
|
-
const rotate = rotateLine(all.length);
|
|
46
|
-
if (rotate) console.log(rotate);
|
|
42
|
+
// Row-list caps: no row-list section exceeds 10 (same budget as ## recent,
|
|
43
|
+
// which takes doneLines(all, 10)). Per-row text is cut at a word boundary —
|
|
44
|
+
// a kickoff line points at the row (mem find has the full text), it must not
|
|
45
|
+
// reprint it.
|
|
46
|
+
const BUGS_LIMIT = 10;
|
|
47
|
+
const BRANCH_LIMIT = 10;
|
|
48
|
+
const RECENT_OPEN_LIMIT = 10;
|
|
49
|
+
const RECENT_OPEN_TEXT = 160;
|
|
50
|
+
|
|
51
|
+
const shortText = (text: string, max = RECENT_OPEN_TEXT): string => {
|
|
52
|
+
if (text.length <= max) return text;
|
|
53
|
+
const cut = text.slice(0, max);
|
|
54
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
55
|
+
return `${lastSpace > max / 2 ? cut.slice(0, lastSpace) : cut}…`;
|
|
47
56
|
};
|
|
48
57
|
|
|
49
58
|
export const cmdDone = () => {
|
|
@@ -235,118 +244,12 @@ export const cmdKickoff = (a: string[]) => {
|
|
|
235
244
|
}
|
|
236
245
|
}
|
|
237
246
|
|
|
238
|
-
// ---
|
|
239
|
-
|
|
240
|
-
if (!arg && !planFile) {
|
|
241
|
-
// no args = now + a "recent" section = the last 10 closes
|
|
242
|
-
console.log(`# ${app} — ${all.length} entries`);
|
|
243
|
-
printOpenRows(all, { showHold: true });
|
|
244
|
-
console.log(`\n## recent\n${doneLines(all, 10).join("\n")}`);
|
|
245
|
-
const stale = staleReport(all);
|
|
246
|
-
if (stale.length)
|
|
247
|
-
console.log(
|
|
248
|
-
`\n## ⚠ stale (${stale.length})\n` +
|
|
249
|
-
stale.map((l) => `- ${l}`).join("\n"),
|
|
250
|
-
);
|
|
251
|
-
const sweep = planSweepLine();
|
|
252
|
-
if (sweep) console.log(sweep);
|
|
253
|
-
const rotate = rotateLine(all.length);
|
|
254
|
-
if (rotate) console.log(rotate);
|
|
255
|
-
} else if (resolvedSpec) {
|
|
256
|
-
// spec.md = a brief for that spec
|
|
257
|
-
const open = openRows(all);
|
|
258
|
-
const specRows = open.filter((r) => r.spec === resolvedSpec);
|
|
259
|
-
const specDecisions = all
|
|
260
|
-
.filter(
|
|
261
|
-
(r): r is WorkRow => r.kind === "decision" && r.spec === resolvedSpec,
|
|
262
|
-
)
|
|
263
|
-
.slice(-5);
|
|
264
|
-
const specCloses = all
|
|
265
|
-
.filter((r): r is CloseRow => r.kind === "close")
|
|
266
|
-
.filter((r) => byId.get(r.ref)?.spec === resolvedSpec)
|
|
267
|
-
.slice(-3);
|
|
268
|
-
|
|
269
|
-
console.log(`# ${resolvedSpec} — ${specRows.length} open rows`);
|
|
270
|
-
if (specRows.length) {
|
|
271
|
-
console.log(`\n## open\n${specRows.map((r) => fmtRow(r)).join("\n")}`);
|
|
272
|
-
}
|
|
273
|
-
if (specDecisions.length) {
|
|
274
|
-
console.log(
|
|
275
|
-
`\n## decisions\n` +
|
|
276
|
-
specDecisions
|
|
277
|
-
.map((r) => `- ${r.ts.slice(0, 10)} ${r.text}`)
|
|
278
|
-
.join("\n"),
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
if (specCloses.length) {
|
|
282
|
-
console.log(
|
|
283
|
-
`\n## closes\n${specCloses.map((c) => fmtClose(c, byId)).join("\n")}`,
|
|
284
|
-
);
|
|
285
|
-
}
|
|
286
|
-
if (!specRows.length && !specDecisions.length && !specCloses.length) {
|
|
287
|
-
console.log("(no entries for this spec)");
|
|
288
|
-
}
|
|
289
|
-
} else if (target) {
|
|
290
|
-
// id = a brief for that task
|
|
291
|
-
const claims = claimsOf(all);
|
|
292
|
-
const claimInfo = claims.has(arg)
|
|
293
|
-
? `\nClaimed by: ${claims.get(arg)?.agent} (${claims.get(arg)?.ts.slice(0, 10)})`
|
|
294
|
-
: "";
|
|
295
|
-
|
|
296
|
-
const spec = target.spec;
|
|
297
|
-
const open = spec
|
|
298
|
-
? openRows(all).filter((r) => r.spec === spec && r.id !== arg)
|
|
299
|
-
: [];
|
|
300
|
-
const specDecisions = spec
|
|
301
|
-
? all
|
|
302
|
-
.filter((r): r is WorkRow => r.kind === "decision" && r.spec === spec)
|
|
303
|
-
.slice(-5)
|
|
304
|
-
: [];
|
|
305
|
-
const specNotes = spec
|
|
306
|
-
? all
|
|
307
|
-
.filter((r): r is WorkRow => r.kind === "note" && r.spec === spec)
|
|
308
|
-
.slice(-5)
|
|
309
|
-
: [];
|
|
310
|
-
const specCloses = spec
|
|
311
|
-
? all
|
|
312
|
-
.filter((r): r is CloseRow => r.kind === "close")
|
|
313
|
-
.filter((r) => byId.get(r.ref)?.spec === spec)
|
|
314
|
-
.slice(-3)
|
|
315
|
-
: [];
|
|
316
|
-
|
|
317
|
-
console.log(
|
|
318
|
-
`# [${target.id}] ${target.kind} — ${target.text}${target.spec ? ` → ${target.spec}` : ""}${claimInfo}`,
|
|
319
|
-
);
|
|
320
|
-
if (open.length) {
|
|
321
|
-
console.log(
|
|
322
|
-
`\n## open (same spec)\n` +
|
|
323
|
-
open.map((r) => fmtRow(r, claims)).join("\n"),
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
if (specDecisions.length || specNotes.length) {
|
|
327
|
-
console.log(`\n## decisions & notes (spec)\n`);
|
|
328
|
-
for (const r of [...specDecisions, ...specNotes]) {
|
|
329
|
-
console.log(`- ${r.ts.slice(0, 10)} ${r.kind} ${r.text}`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (specCloses.length) {
|
|
333
|
-
console.log(
|
|
334
|
-
`\n## closes (spec)\n` +
|
|
335
|
-
specCloses.map((c) => fmtClose(c, byId)).join("\n"),
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
} else if (arg) {
|
|
339
|
-
console.error(`no id "${arg}" in the log`);
|
|
340
|
-
process.exit(1);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// --- next up section (chunk 3) ---
|
|
247
|
+
// --- suggestions (generated early so the no-args path can print next up
|
|
248
|
+
// before the recency tier, keeping it inside the 4KB cap) ---
|
|
344
249
|
|
|
345
|
-
// A suggestion may carry a fully-runnable command. `manual` marks a command
|
|
346
|
-
// that is a template with a placeholder (bug close) — show it, never auto-run it.
|
|
347
250
|
type Suggestion = { text: string; run?: string; manual?: boolean };
|
|
348
251
|
const suggestions: Suggestion[] = [];
|
|
349
|
-
const GROUP_CAP = 3;
|
|
252
|
+
const GROUP_CAP = 3;
|
|
350
253
|
|
|
351
254
|
const truncate = (s: string, n = 80) =>
|
|
352
255
|
s.length > n ? `${s.slice(0, n - 1).trimEnd()}…` : s;
|
|
@@ -358,7 +261,6 @@ export const cmdKickoff = (a: string[]) => {
|
|
|
358
261
|
for (const file of files) {
|
|
359
262
|
if (planLines.length >= GROUP_CAP) break;
|
|
360
263
|
const fullPath = join(planDir, file);
|
|
361
|
-
// Skip the plan we already showed
|
|
362
264
|
if (planFile && fullPath === planFile) continue;
|
|
363
265
|
if (hasHighPriority(fullPath)) {
|
|
364
266
|
const title = readPlanTitle(fullPath);
|
|
@@ -369,19 +271,16 @@ export const cmdKickoff = (a: string[]) => {
|
|
|
369
271
|
}
|
|
370
272
|
}
|
|
371
273
|
}
|
|
372
|
-
// Only the first unchecked chunk (SPEC §3) — the body of the plan is the plan's job.
|
|
373
|
-
// There is no per-chunk command, so this line is context, not a runnable item.
|
|
374
274
|
if (planLines.length < GROUP_CAP && planCheckboxes.length) {
|
|
375
275
|
const planName = planFile ? basename(planFile) : "plan";
|
|
376
276
|
planLines.push({ text: `${planCheckboxes[0]} (${planName})` });
|
|
377
277
|
}
|
|
378
278
|
suggestions.push(...planLines);
|
|
379
279
|
|
|
380
|
-
// group 2 — open bugs, oldest first
|
|
381
|
-
|
|
382
|
-
const claims = claimsOf(all);
|
|
280
|
+
// group 2 — open bugs, oldest first
|
|
281
|
+
const suggClaims = claimsOf(all);
|
|
383
282
|
const openBugs = openRows(all)
|
|
384
|
-
.filter((r) => r.kind === "bug" && !
|
|
283
|
+
.filter((r) => r.kind === "bug" && !suggClaims.has(r.id))
|
|
385
284
|
.slice(0, GROUP_CAP);
|
|
386
285
|
for (const b of openBugs) {
|
|
387
286
|
suggestions.push({
|
|
@@ -407,11 +306,7 @@ export const cmdKickoff = (a: string[]) => {
|
|
|
407
306
|
});
|
|
408
307
|
}
|
|
409
308
|
|
|
410
|
-
// group 4 — the lint baseline
|
|
411
|
-
// already answered by `lint-baseline`, but only if it was captured before
|
|
412
|
-
// the work started — and nobody remembers at session open, which is what
|
|
413
|
-
// kickoff is. Only offered when the repo declared a lint command; without
|
|
414
|
-
// one the capture would just fail.
|
|
309
|
+
// group 4 — the lint baseline
|
|
415
310
|
try {
|
|
416
311
|
if (readEvidenceLintCmd(root) && !existsSync(baselinePath(root))) {
|
|
417
312
|
suggestions.push({
|
|
@@ -423,15 +318,14 @@ export const cmdKickoff = (a: string[]) => {
|
|
|
423
318
|
// state dir unreadable — the baseline line is a convenience, never a gate
|
|
424
319
|
}
|
|
425
320
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
321
|
+
/** Print ## next up and handle --pick. */
|
|
322
|
+
const printNextUp = () => {
|
|
323
|
+
if (!suggestions.length) return;
|
|
429
324
|
console.log(`\n## next up`);
|
|
430
325
|
for (let i = 0; i < suggestions.length; i++) {
|
|
431
326
|
console.log(` [${i + 1}] ${suggestions[i].text}`);
|
|
432
327
|
if (suggestions[i].run) console.log(` → ${suggestions[i].run}`);
|
|
433
328
|
}
|
|
434
|
-
|
|
435
329
|
if (pickIdx !== null) {
|
|
436
330
|
if (pickIdx > suggestions.length) {
|
|
437
331
|
console.error(
|
|
@@ -461,5 +355,184 @@ export const cmdKickoff = (a: string[]) => {
|
|
|
461
355
|
process.exit(1);
|
|
462
356
|
}
|
|
463
357
|
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// --- main output ---
|
|
361
|
+
|
|
362
|
+
// Deferred output for the no-args path — recent closes, plan sweep, rotate
|
|
363
|
+
let kickoffExtras: (() => void) | null = null;
|
|
364
|
+
|
|
365
|
+
if (!arg && !planFile) {
|
|
366
|
+
// no args = ranked open rows + next up + recent closes
|
|
367
|
+
console.log(`# ${app} — ${all.length} entries`);
|
|
368
|
+
|
|
369
|
+
const open = openRows(all);
|
|
370
|
+
const claims = claimsOf(all);
|
|
371
|
+
const diffFiles = getBranchDiffFiles(root);
|
|
372
|
+
|
|
373
|
+
// Tier 1: unclaimed bugs (always first, no matter how old) — capped: a
|
|
374
|
+
// backlog of unclosable bugs must not push the rest of the ranking out.
|
|
375
|
+
const bugs = open.filter((r) => r.kind === "bug" && !claims.has(r.id));
|
|
376
|
+
if (bugs.length) {
|
|
377
|
+
console.log(`\n## bugs`);
|
|
378
|
+
for (const r of bugs.slice(0, BUGS_LIMIT)) console.log(fmtRow(r));
|
|
379
|
+
if (bugs.length > BUGS_LIMIT)
|
|
380
|
+
console.log(
|
|
381
|
+
`… +${bugs.length - BUGS_LIMIT} more unclosed bugs — \`fapony mem find <word>\` for the rest`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// next up BEFORE the branch/recency tiers so no row-list section can
|
|
386
|
+
// push it out — order is by actionability, never by "this tier is short".
|
|
387
|
+
printNextUp();
|
|
388
|
+
|
|
389
|
+
// Tier 2: rows whose files[] overlap with git diff --name-only dev...HEAD
|
|
390
|
+
// — capped like every other row-list section (≤10 + overflow, pointer
|
|
391
|
+
// lines via shortText): the branch diff is unbounded and mem clusters on
|
|
392
|
+
// files every real branch touches, so uncapped this reprints the log.
|
|
393
|
+
const bugIds = new Set(bugs.map((r) => r.id));
|
|
394
|
+
const diffMatched = open.filter((r) => {
|
|
395
|
+
if (bugIds.has(r.id)) return false;
|
|
396
|
+
if (diffFiles.size === 0) return false;
|
|
397
|
+
return (r.files ?? []).some((f) => diffFiles.has(f));
|
|
398
|
+
});
|
|
399
|
+
if (diffMatched.length) {
|
|
400
|
+
console.log(`\n## on this branch`);
|
|
401
|
+
for (const r of diffMatched.slice(0, BRANCH_LIMIT))
|
|
402
|
+
console.log(fmtRow({ ...r, text: shortText(r.text) }, claims));
|
|
403
|
+
if (diffMatched.length > BRANCH_LIMIT)
|
|
404
|
+
console.log(
|
|
405
|
+
`… +${diffMatched.length - BRANCH_LIMIT} more — \`fapony mem find <word>\` for the rest`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Tier 3: rest by recency (newest first) — tie-breaker only, capped like
|
|
410
|
+
// ## recent (doneLines takes 10): decision/note can never close, so an
|
|
411
|
+
// uncapped tier reprints the whole log. Lines are pointers, not prose —
|
|
412
|
+
// cut at a word boundary and point at mem find for the rest.
|
|
413
|
+
const shown = new Set([...bugIds, ...diffMatched.map((r) => r.id)]);
|
|
414
|
+
const rest = open
|
|
415
|
+
.filter((r) => !shown.has(r.id))
|
|
416
|
+
.sort((a, b) => b.ts.localeCompare(a.ts));
|
|
417
|
+
if (rest.length) {
|
|
418
|
+
console.log(`\n## recent open`);
|
|
419
|
+
for (const r of rest.slice(0, RECENT_OPEN_LIMIT))
|
|
420
|
+
console.log(fmtRow({ ...r, text: shortText(r.text) }, claims));
|
|
421
|
+
if (rest.length > RECENT_OPEN_LIMIT)
|
|
422
|
+
console.log(
|
|
423
|
+
`… +${rest.length - RECENT_OPEN_LIMIT} more — \`fapony mem find <word>\` for the rest`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// recent closes, plan sweep, rotate — after next up
|
|
428
|
+
kickoffExtras = () => {
|
|
429
|
+
console.log(`\n## recent\n${doneLines(all, 10).join("\n")}`);
|
|
430
|
+
const sweep = planSweepLine();
|
|
431
|
+
if (sweep) console.log(sweep);
|
|
432
|
+
const rotate = rotateLine(all.length);
|
|
433
|
+
if (rotate) console.log(rotate);
|
|
434
|
+
};
|
|
435
|
+
} else if (resolvedSpec) {
|
|
436
|
+
// spec.md = a brief for that spec
|
|
437
|
+
const open = openRows(all);
|
|
438
|
+
const specRows = open.filter((r) => r.spec === resolvedSpec);
|
|
439
|
+
const specDecisions = all
|
|
440
|
+
.filter(
|
|
441
|
+
(r): r is WorkRow => r.kind === "decision" && r.spec === resolvedSpec,
|
|
442
|
+
)
|
|
443
|
+
.slice(-5);
|
|
444
|
+
const specCloses = all
|
|
445
|
+
.filter((r): r is CloseRow => r.kind === "close")
|
|
446
|
+
.filter((r) => byId.get(r.ref)?.spec === resolvedSpec)
|
|
447
|
+
.slice(-3);
|
|
448
|
+
|
|
449
|
+
console.log(`# ${resolvedSpec} — ${specRows.length} open rows`);
|
|
450
|
+
if (specRows.length) {
|
|
451
|
+
console.log(`\n## open\n${specRows.map((r) => fmtRow(r)).join("\n")}`);
|
|
452
|
+
}
|
|
453
|
+
if (specDecisions.length) {
|
|
454
|
+
console.log(
|
|
455
|
+
`\n## decisions\n` +
|
|
456
|
+
specDecisions
|
|
457
|
+
.map((r) => `- ${r.ts.slice(0, 10)} ${r.text}`)
|
|
458
|
+
.join("\n"),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (specCloses.length) {
|
|
462
|
+
console.log(
|
|
463
|
+
`\n## closes\n${specCloses.map((c) => fmtClose(c, byId)).join("\n")}`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (!specRows.length && !specDecisions.length && !specCloses.length) {
|
|
467
|
+
console.log("(no entries for this spec)");
|
|
468
|
+
}
|
|
469
|
+
} else if (target) {
|
|
470
|
+
// id = a brief for that task
|
|
471
|
+
const claims = claimsOf(all);
|
|
472
|
+
const claimInfo = claims.has(arg)
|
|
473
|
+
? `\nClaimed by: ${claims.get(arg)?.agent} (${claims.get(arg)?.ts.slice(0, 10)})`
|
|
474
|
+
: "";
|
|
475
|
+
|
|
476
|
+
const spec = target.spec;
|
|
477
|
+
const open = spec
|
|
478
|
+
? openRows(all).filter((r) => r.spec === spec && r.id !== arg)
|
|
479
|
+
: [];
|
|
480
|
+
const specDecisions = spec
|
|
481
|
+
? all
|
|
482
|
+
.filter((r): r is WorkRow => r.kind === "decision" && r.spec === spec)
|
|
483
|
+
.slice(-5)
|
|
484
|
+
: [];
|
|
485
|
+
const specNotes = spec
|
|
486
|
+
? all
|
|
487
|
+
.filter((r): r is WorkRow => r.kind === "note" && r.spec === spec)
|
|
488
|
+
.slice(-5)
|
|
489
|
+
: [];
|
|
490
|
+
const specCloses = spec
|
|
491
|
+
? all
|
|
492
|
+
.filter((r): r is CloseRow => r.kind === "close")
|
|
493
|
+
.filter((r) => byId.get(r.ref)?.spec === spec)
|
|
494
|
+
.slice(-3)
|
|
495
|
+
: [];
|
|
496
|
+
|
|
497
|
+
console.log(
|
|
498
|
+
`# [${target.id}] ${target.kind} — ${target.text}${target.spec ? ` → ${target.spec}` : ""}${claimInfo}`,
|
|
499
|
+
);
|
|
500
|
+
if (open.length) {
|
|
501
|
+
console.log(
|
|
502
|
+
`\n## open (same spec)\n` +
|
|
503
|
+
open.map((r) => fmtRow(r, claims)).join("\n"),
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
if (specDecisions.length || specNotes.length) {
|
|
507
|
+
console.log(`\n## decisions & notes (spec)\n`);
|
|
508
|
+
for (const r of [...specDecisions, ...specNotes]) {
|
|
509
|
+
console.log(`- ${r.ts.slice(0, 10)} ${r.kind} ${r.text}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (specCloses.length) {
|
|
513
|
+
console.log(
|
|
514
|
+
`\n## closes (spec)\n` +
|
|
515
|
+
specCloses.map((c) => fmtClose(c, byId)).join("\n"),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
} else if (planFile) {
|
|
519
|
+
const title = readPlanTitle(planFile);
|
|
520
|
+
console.log(`# ${title || basename(planFile)} — plan`);
|
|
521
|
+
if (planCheckboxes.length) {
|
|
522
|
+
console.log(
|
|
523
|
+
`\n## unchecked\n${planCheckboxes.map((c) => `- [ ] ${c}`).join("\n")}`,
|
|
524
|
+
);
|
|
525
|
+
} else {
|
|
526
|
+
console.log(`\n(all chunks checked — ready to ship or archive)`);
|
|
527
|
+
}
|
|
528
|
+
} else if (arg) {
|
|
529
|
+
console.error(`no id "${arg}" in the log`);
|
|
530
|
+
process.exit(1);
|
|
464
531
|
}
|
|
532
|
+
|
|
533
|
+
// For non-no-args paths, print next up at the end
|
|
534
|
+
if (arg || planFile) printNextUp();
|
|
535
|
+
|
|
536
|
+
// Deferred output from no-args path (recent closes, plan sweep, rotate)
|
|
537
|
+
kickoffExtras?.();
|
|
465
538
|
};
|