@tekmidian/pai 0.35.2 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{auto-route-DVM3U2ZY.mjs → auto-route-Byf8ENXj.mjs} +4 -4
- package/dist/{auto-route-DVM3U2ZY.mjs.map → auto-route-Byf8ENXj.mjs.map} +1 -1
- package/dist/{checkpoint-block-D3rm4dAJ.mjs → checkpoint-block-DKYxCkBL.mjs} +7 -6
- package/dist/checkpoint-block-DKYxCkBL.mjs.map +1 -0
- package/dist/cli/index.mjs +25 -17
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/probe2.mjs +2 -0
- package/dist/cli/program.d.mts.map +1 -1
- package/dist/cli/program.mjs +16 -267
- package/dist/{clusters-CzGxefB7.mjs → clusters-wZgTCYCB.mjs} +2 -2
- package/dist/{clusters-CzGxefB7.mjs.map → clusters-wZgTCYCB.mjs.map} +1 -1
- package/dist/{config-BSkVcvfq.mjs → config-B64vFg14.mjs} +3 -12
- package/dist/{config-BSkVcvfq.mjs.map → config-B64vFg14.mjs.map} +1 -1
- package/dist/config-C_ErGddD.mjs +3 -0
- package/dist/daemon/index.mjs +19 -19
- package/dist/{daemon-B8L2f5vy.mjs → daemon--N2JFnUs.mjs} +33 -38
- package/dist/daemon--N2JFnUs.mjs.map +1 -0
- package/dist/daemon-DJEFqV84.mjs +20 -0
- package/dist/daemon-mcp/index.mjs +2 -2
- package/dist/{db-BtuN768f.mjs → db-Ca5qfsMC.mjs} +2 -4
- package/dist/{db-BtuN768f.mjs.map → db-Ca5qfsMC.mjs.map} +1 -1
- package/dist/db-O-cyAPfS.mjs +3 -0
- package/dist/db-XEwJbuGO.mjs +3 -0
- package/dist/{db-CYmBWcjh.mjs → db-a1ixZQjr.mjs} +2 -4
- package/dist/{db-CYmBWcjh.mjs.map → db-a1ixZQjr.mjs.map} +1 -1
- package/dist/{detect-Bf2z-oKB.mjs → detect-CdaA48EI.mjs} +1 -1
- package/dist/{detect-Bf2z-oKB.mjs.map → detect-CdaA48EI.mjs.map} +1 -1
- package/dist/{detector-BU-bsDXs.mjs → detector--Gg5JRN5.mjs} +3 -5
- package/dist/{detector-BU-bsDXs.mjs.map → detector--Gg5JRN5.mjs.map} +1 -1
- package/dist/detector-DGAk1iBR.mjs +5 -0
- package/dist/embeddings-CEBGrzwu.mjs +3 -0
- package/dist/{embeddings-Bn86ssxR.mjs → embeddings-DOLZnT1X.mjs} +2 -12
- package/dist/{embeddings-Bn86ssxR.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
- package/dist/{factory-vPTPoBR7.mjs → factory-Bsp7xOpO.mjs} +9 -12
- package/dist/{factory-vPTPoBR7.mjs.map → factory-Bsp7xOpO.mjs.map} +1 -1
- package/dist/factory-CrokPMk2.mjs +3 -0
- package/dist/{helpers-crDEr6S2.mjs → helpers-IjZkXBhj.mjs} +1 -1
- package/dist/{helpers-crDEr6S2.mjs.map → helpers-IjZkXBhj.mjs.map} +1 -1
- package/dist/hooks/context-compression-hook.mjs +209 -70
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/index.mjs +10 -10
- package/dist/{indexer-backend-Bg7VDpGt.mjs → indexer-backend-nQZuEx6N.mjs} +3 -3
- package/dist/{indexer-backend-Bg7VDpGt.mjs.map → indexer-backend-nQZuEx6N.mjs.map} +1 -1
- package/dist/{ipc-client-aVKVERjJ.mjs → ipc-client-BmypMNYk.mjs} +13 -7
- package/dist/ipc-client-BmypMNYk.mjs.map +1 -0
- package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
- package/dist/{latent-ideas-BL9m2HF9.mjs → latent-ideas-Bn6A5-5P.mjs} +4 -4
- package/dist/{latent-ideas-BL9m2HF9.mjs.map → latent-ideas-Bn6A5-5P.mjs.map} +1 -1
- package/dist/{link-boost-QFLrJwD6.mjs → link-boost-fYjUnxCN.mjs} +1 -1
- package/dist/{link-boost-QFLrJwD6.mjs.map → link-boost-fYjUnxCN.mjs.map} +1 -1
- package/dist/{main-resolver-CNSqU8wo.mjs → main-resolver-BAbhKpeX.mjs} +12 -14
- package/dist/main-resolver-BAbhKpeX.mjs.map +1 -0
- package/dist/main-resolver-Dxh444GO.mjs +4 -0
- package/dist/{migrate-fLD6rAdO.mjs → migrate-Cjzeefn9.mjs} +2 -2
- package/dist/{migrate-fLD6rAdO.mjs.map → migrate-Cjzeefn9.mjs.map} +1 -1
- package/dist/{neighborhood-BX89_nty.mjs → neighborhood-DpaEM991.mjs} +2 -2
- package/dist/{neighborhood-BX89_nty.mjs.map → neighborhood-DpaEM991.mjs.map} +1 -1
- package/dist/{note-context-d1wT_-GA.mjs → note-context-DrcY4cWm.mjs} +1 -1
- package/dist/{note-context-d1wT_-GA.mjs.map → note-context-DrcY4cWm.mjs.map} +1 -1
- package/dist/{pai-marker-B20KqhA8.mjs → pai-marker-CHtbJMwJ.mjs} +1 -1
- package/dist/{pai-marker-B20KqhA8.mjs.map → pai-marker-CHtbJMwJ.mjs.map} +1 -1
- package/dist/{postgres-CI3FjGq0.mjs → postgres-BVme6qX0.mjs} +2 -2
- package/dist/{postgres-CI3FjGq0.mjs.map → postgres-BVme6qX0.mjs.map} +1 -1
- package/dist/{pick-DatgS3Nk.mjs → program-BnMNFb4O.mjs} +1171 -223
- package/dist/program-BnMNFb4O.mjs.map +1 -0
- package/dist/query-feedback-BBMBp96K.mjs +3 -0
- package/dist/{query-feedback-D4U56Hz6.mjs → query-feedback-C1T6kS18.mjs} +2 -4
- package/dist/{query-feedback-D4U56Hz6.mjs.map → query-feedback-C1T6kS18.mjs.map} +1 -1
- package/dist/reranker-CwTCNsgA.mjs +3 -0
- package/dist/{reranker-CMNZcfVx.mjs → reranker-xPm04PXx.mjs} +2 -8
- package/dist/{reranker-CMNZcfVx.mjs.map → reranker-xPm04PXx.mjs.map} +1 -1
- package/dist/router-BMkOb62X.mjs +3 -0
- package/dist/{router-i9S19Usg.mjs → router-CsDm7HvK.mjs} +2 -4
- package/dist/{router-i9S19Usg.mjs.map → router-CsDm7HvK.mjs.map} +1 -1
- package/dist/{runtime-paths-B0P1TvUr.mjs → runtime-paths-rni52zHX.mjs} +1 -1
- package/dist/{runtime-paths-B0P1TvUr.mjs.map → runtime-paths-rni52zHX.mjs.map} +1 -1
- package/dist/search-CfPpJAWQ.mjs +4 -0
- package/dist/{search-C32zQ0V0.mjs → search-Rpk1cSBC.mjs} +4 -15
- package/dist/{search-C32zQ0V0.mjs.map → search-Rpk1cSBC.mjs.map} +1 -1
- package/dist/{sources-BDwN0B8i.mjs → sources-D8ZdNfvK.mjs} +2 -2
- package/dist/{sources-BDwN0B8i.mjs.map → sources-D8ZdNfvK.mjs.map} +1 -1
- package/dist/{sqlite-C6FHnMkn.mjs → sqlite-D1IaR8Am.mjs} +3 -3
- package/dist/{sqlite-C6FHnMkn.mjs.map → sqlite-D1IaR8Am.mjs.map} +1 -1
- package/dist/state-WaXhLr6R.mjs +70 -0
- package/dist/{state-DTvy-jRB.mjs.map → state-WaXhLr6R.mjs.map} +1 -1
- package/dist/state-qtmrBWCm.mjs +3 -0
- package/dist/{stop-words-BaMEGVeY.mjs → stop-words-Hfu8u22w.mjs} +1 -1
- package/dist/{stop-words-BaMEGVeY.mjs.map → stop-words-Hfu8u22w.mjs.map} +1 -1
- package/dist/{sync--BoxBBok.mjs → sync-BWbe8JTg.mjs} +3 -3
- package/dist/{sync--BoxBBok.mjs.map → sync-BWbe8JTg.mjs.map} +1 -1
- package/dist/{themes-BObEGMWn.mjs → themes-XPkj_bfP.mjs} +3 -3
- package/dist/{themes-BObEGMWn.mjs.map → themes-XPkj_bfP.mjs.map} +1 -1
- package/dist/tools-DEt6YPfc.mjs +5 -0
- package/dist/{tools-C1lCHerL.mjs → tools-ceiy7ANX.mjs} +28 -65
- package/dist/tools-ceiy7ANX.mjs.map +1 -0
- package/dist/{trace-h23JCcFD.mjs → trace-DfyGmMG_.mjs} +1 -1
- package/dist/{trace-h23JCcFD.mjs.map → trace-DfyGmMG_.mjs.map} +1 -1
- package/dist/{utils-BAxjW3j8.mjs → utils-9Err2RBW.mjs} +2 -22
- package/dist/{utils-BAxjW3j8.mjs.map → utils-9Err2RBW.mjs.map} +1 -1
- package/dist/utils-DhMex3Ox.mjs +3 -0
- package/dist/{vault-indexer-CUF9edbW.mjs → vault-indexer-CFvlPUMB.mjs} +2 -2
- package/dist/{vault-indexer-CUF9edbW.mjs.map → vault-indexer-CFvlPUMB.mjs.map} +1 -1
- package/dist/{work-queue-worker-BcDGAcF3.mjs → work-queue-worker-228XjABm.mjs} +234 -14
- package/dist/work-queue-worker-228XjABm.mjs.map +1 -0
- package/dist/work-queue-worker-LRA9Fj9z.mjs +11 -0
- package/dist/{zettelkasten-W-h8G2is.mjs → zettelkasten-CvjmMghT.mjs} +4 -4
- package/dist/{zettelkasten-W-h8G2is.mjs.map → zettelkasten-CvjmMghT.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.test.ts +515 -0
- package/src/hooks/ts/lib/context-fill.ts +585 -0
- package/src/hooks/ts/lib/context-handover-cache.ts +46 -0
- package/src/hooks/ts/lib/transcript-text.test.ts +125 -0
- package/src/hooks/ts/lib/transcript-text.ts +71 -0
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +112 -30
- package/statusline-command.sh +16 -0
- package/dist/checkpoint-block-D3rm4dAJ.mjs.map +0 -1
- package/dist/daemon-B8L2f5vy.mjs.map +0 -1
- package/dist/ipc-client-aVKVERjJ.mjs.map +0 -1
- package/dist/main-resolver-CNSqU8wo.mjs.map +0 -1
- package/dist/pick-DatgS3Nk.mjs.map +0 -1
- package/dist/rolldown-runtime-95iHPtFO.mjs +0 -18
- package/dist/state-DTvy-jRB.mjs +0 -102
- package/dist/tools-C1lCHerL.mjs.map +0 -1
- package/dist/work-queue-worker-BcDGAcF3.mjs.map +0 -1
- /package/dist/{indexer-AEcT8wHf.mjs → indexer-D7MvSQPY.mjs} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as readPaiMarker } from "./pai-marker-
|
|
2
|
-
import { t as detectProject } from "./detect-
|
|
1
|
+
import { r as readPaiMarker } from "./pai-marker-CHtbJMwJ.mjs";
|
|
2
|
+
import { t as detectProject } from "./detect-CdaA48EI.mjs";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
|
|
@@ -26,7 +26,7 @@ async function autoRoute(registryDb, federation, cwd, context) {
|
|
|
26
26
|
const markerResult = findMarkerUpward(registryDb, target);
|
|
27
27
|
if (markerResult) return markerResult;
|
|
28
28
|
if (context && context.trim().length > 0) {
|
|
29
|
-
const { detectTopicShift } = await import("./detector-
|
|
29
|
+
const { detectTopicShift } = await import("./detector-DGAk1iBR.mjs");
|
|
30
30
|
const topicResult = await detectTopicShift(registryDb, federation, {
|
|
31
31
|
context,
|
|
32
32
|
threshold: .5
|
|
@@ -83,4 +83,4 @@ function formatAutoRouteJson(result) {
|
|
|
83
83
|
|
|
84
84
|
//#endregion
|
|
85
85
|
export { autoRoute, formatAutoRouteJson };
|
|
86
|
-
//# sourceMappingURL=auto-route-
|
|
86
|
+
//# sourceMappingURL=auto-route-Byf8ENXj.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-route-
|
|
1
|
+
{"version":3,"file":"auto-route-Byf8ENXj.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb Open PAI registry database\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = detectProject(registryDb, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = findMarkerUpward(registryDb, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(registryDb, federation, {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n });\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(topicResult.suggestedProject) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n registryDb: Database,\n startDir: string\n): AutoRouteResult | null {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(marker.slug) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { _ as warn, c as ok, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-
|
|
2
|
-
import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-
|
|
3
|
-
import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-
|
|
4
|
-
import { i as kgAdd, o as kgInvalidate, r as upsertKgEntity, s as kgQuery } from "./kg-entity-
|
|
1
|
+
import { _ as warn, c as ok, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-9Err2RBW.mjs";
|
|
2
|
+
import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-Cjzeefn9.mjs";
|
|
3
|
+
import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-CHtbJMwJ.mjs";
|
|
4
|
+
import { i as kgAdd, o as kgInvalidate, r as upsertKgEntity, s as kgQuery } from "./kg-entity-DbOMPdF9.mjs";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { basename, join, resolve } from "node:path";
|
|
@@ -520,7 +520,8 @@ function cmdScan(db, opts = {}) {
|
|
|
520
520
|
result = performScan(db);
|
|
521
521
|
} catch (e) {
|
|
522
522
|
console.error(err(String(e)));
|
|
523
|
-
process.
|
|
523
|
+
process.exitCode = 1;
|
|
524
|
+
return;
|
|
524
525
|
}
|
|
525
526
|
if (!opts.quick) {
|
|
526
527
|
console.log(ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`));
|
|
@@ -1406,4 +1407,4 @@ function readBodyFile(path) {
|
|
|
1406
1407
|
|
|
1407
1408
|
//#endregion
|
|
1408
1409
|
export { transcriptFiles as _, readBodyFile as a, loadScanConfig as c, saveScanConfig as d, upsertProject as f, scanTranscriptFolders as g, findMovedProjects as h, findNotesDir as i, performScan as l, claudeProjectsDir as m, applyContinue as n, extractAndStoreTriples as o, upsertSession as p, findLatestNote as r, cmdScan as s, appendCheckpointToNote as t, resolveHome as u };
|
|
1409
|
-
//# sourceMappingURL=checkpoint-block-
|
|
1410
|
+
//# sourceMappingURL=checkpoint-block-DKYxCkBL.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint-block-DKYxCkBL.mjs","names":[],"sources":["../src/registry/moved.ts","../src/cli/commands/registry/utils.ts","../src/cli/commands/registry/scan.ts","../src/daemon/templates/triple-extraction-prompt.ts","../src/memory/kg-extraction.ts","../src/session/checkpoint-block.ts"],"sourcesContent":["/**\n * moved.ts — reconnect a project to transcripts that moved out from under it.\n *\n * The registry records an `encoded_dir`: the name Claude Code gave the folder\n * holding a project's transcripts. It is written once, when the project is\n * added, and nothing updates it when the project moves. So a project that has\n * been relocated points at a directory that is empty or gone, and every lookup\n * — checkpoints, handovers, session digests — quietly returns nothing.\n *\n * `resolveTranscriptDir` already re-derives the name from the project's current\n * root path, which fixes the case where the encoding is stale but the path is\n * right. It cannot fix the case where the ENCODING RULE itself does not\n * reproduce the folder name: iCloud paths with `~` in them, emoji segments, or\n * a folder Claude Code created under a path the project no longer has.\n *\n * This module answers that case by asking the transcripts instead of guessing.\n * Every transcript entry records the `cwd` it was written in, so the mapping\n * from a project root to its transcript folder is a fact sitting on disk rather\n * than something to be inferred from a naming convention. Reading it is slower\n * than a string transform and it is right, which is the correct trade for a\n * repair that runs on demand.\n */\n\nimport { existsSync, readdirSync, statSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/** Where Claude Code keeps per-project transcript folders. */\nexport function claudeProjectsDir(): string {\n return join(homedir(), \".claude\", \"projects\");\n}\n\n/**\n * Every transcript file for one project folder — live and archived.\n *\n * The archived half matters: `session-stop` moves all but the newest transcript\n * into `sessions/`, so a folder whose work is finished has an empty top level\n * and everything underneath. Counting only the top level reports a busy project\n * as unused, which is how an earlier audit of this same problem overstated the\n * breakage by a factor of three.\n */\nexport function transcriptFiles(projectDir: string): string[] {\n const out: string[] = [];\n for (const dir of [projectDir, join(projectDir, \"sessions\")]) {\n if (!existsSync(dir)) continue;\n try {\n for (const entry of readdirSync(dir)) {\n if (entry.endsWith(\".jsonl\")) out.push(join(dir, entry));\n }\n } catch {\n /* unreadable — treat as empty rather than failing the scan */\n }\n }\n return out;\n}\n\n/**\n * The working directory a transcript was recorded in.\n *\n * Reads only the head of the file. `cwd` is present on the first entry and does\n * not change within a session, so scanning further would cost time to learn\n * nothing. Returns null rather than throwing: a truncated or half-written\n * transcript is normal for a session that is still running.\n */\nexport function cwdOfTranscript(file: string, maxBytes = 64 * 1024): string | null {\n let head: string;\n try {\n const buf = readFileSync(file);\n head = buf.subarray(0, maxBytes).toString(\"utf-8\");\n } catch {\n return null;\n }\n\n for (const line of head.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n const cwd = (JSON.parse(line) as { cwd?: string }).cwd;\n if (cwd) return cwd;\n } catch {\n // A partial final line is expected when the head is cut mid-entry.\n continue;\n }\n }\n return null;\n}\n\nexport interface TranscriptFolder {\n /** Folder name under ~/.claude/projects. */\n name: string;\n /** Number of transcripts, live plus archived. */\n count: number;\n /** Newest transcript mtime, for choosing between candidates. */\n newest: number;\n /**\n * How many sampled transcripts in this folder began in the cwd being looked\n * up, and how many began somewhere else.\n *\n * One folder routinely holds transcripts for more than one directory: a\n * session started in a subdirectory can land in the parent's folder. So a\n * project appearing in a folder is not evidence the folder is ITS folder.\n * Measured 2026-08-02 — `apps/youdrill` held 5 transcripts for itself and 3\n * for `apps/youdrill/app`, and reconnecting `app` there would have attached a\n * subdirectory to its parent's history.\n */\n matching: number;\n total: number;\n}\n\n/**\n * Map every working directory seen on disk to the folders that hold its\n * transcripts.\n *\n * A directory can legitimately map to more than one folder — Claude Code's\n * encoding is lossy, so two different paths can collide, and a project moved\n * and moved back leaves both. Callers pick; this reports.\n */\nexport function scanTranscriptFolders(\n projectsDir = claudeProjectsDir(),\n sampleFilesPerFolder = 8\n): Map<string, TranscriptFolder[]> {\n const byCwd = new Map<string, TranscriptFolder[]>();\n if (!existsSync(projectsDir)) return byCwd;\n\n let entries: string[];\n try {\n entries = readdirSync(projectsDir);\n } catch {\n return byCwd;\n }\n\n for (const name of entries) {\n const dir = join(projectsDir, name);\n try {\n if (!statSync(dir).isDirectory()) continue;\n } catch {\n continue;\n }\n\n const files = transcriptFiles(dir);\n if (files.length === 0) continue;\n\n // Newest first: a moved project's most recent transcripts carry its current\n // path, and older ones may still carry the old one.\n const mtimes = new Map<string, number>();\n for (const f of files) {\n try {\n mtimes.set(f, statSync(f).mtimeMs);\n } catch {\n mtimes.set(f, 0);\n }\n }\n const ordered = files.sort((a, b) => (mtimes.get(b) ?? 0) - (mtimes.get(a) ?? 0));\n const newest = mtimes.get(ordered[0]) ?? 0;\n\n // Count how many sampled transcripts began in each cwd, so a caller can\n // tell \"this folder is that project's\" from \"that project appears in this\n // folder\" — which are very different claims.\n const tally = new Map<string, number>();\n let sampled = 0;\n for (const f of ordered.slice(0, sampleFilesPerFolder)) {\n const cwd = cwdOfTranscript(f);\n if (!cwd) continue;\n sampled++;\n tally.set(cwd, (tally.get(cwd) ?? 0) + 1);\n }\n\n for (const [cwd, matching] of tally) {\n const record: TranscriptFolder = {\n name,\n count: files.length,\n newest,\n matching,\n total: sampled,\n };\n const list = byCwd.get(cwd);\n if (list) list.push(record);\n else byCwd.set(cwd, [record]);\n }\n }\n\n return byCwd;\n}\n\nexport interface MovedProject {\n id: number;\n slug: string;\n rootPath: string;\n /** What the registry currently claims. Null when it has never been set. */\n storedDir: string | null;\n /** The folder that actually holds this project's transcripts. */\n correctDir: string;\n transcripts: number;\n /** Sessions the registry believes exist — how much is currently unreachable. */\n sessions: number;\n}\n\nexport interface RegistryProjectRow {\n id: number;\n slug: string;\n root_path: string;\n encoded_dir: string | null;\n sessions: number;\n}\n\n/**\n * Which projects point at the wrong transcript folder, and where they belong.\n *\n * A project is only reported when its stored folder yields nothing AND its root\n * path is recorded as a `cwd` somewhere else. Both halves matter: without the\n * first this would rewrite entries that work, and without the second it would\n * have nothing better to offer than the value already there.\n *\n * `resolvesNow` is supplied by the caller so this module does not have to\n * duplicate the resolver's fallback logic — a project the shipped resolver\n * already handles is not broken and must not be \"repaired\".\n */\nexport function findMovedProjects(\n rows: RegistryProjectRow[],\n byCwd: Map<string, TranscriptFolder[]>,\n resolvesNow: (row: RegistryProjectRow) => boolean\n): MovedProject[] {\n const out: MovedProject[] = [];\n\n for (const row of rows) {\n if (resolvesNow(row)) continue;\n\n const candidates = byCwd.get(row.root_path);\n if (!candidates || candidates.length === 0) continue;\n\n // Only a folder this project DOMINATES. A folder where most transcripts\n // began somewhere else belongs to that somewhere else, and attaching a\n // subdirectory to its parent's folder would hand it a history that is\n // mostly not its own — then the no-session-id fallback, which takes the\n // newest transcript in the folder, would read a sibling's work as this\n // project's. A missed repair costs nothing; a wrong one is silent and\n // wrong in the direction of confidently reporting someone else's data.\n const owned = candidates.filter((c) => c.matching * 2 > c.total);\n if (owned.length === 0) continue;\n\n // Most transcripts wins, newest breaks a tie: the folder with the most\n // history is the one worth reconnecting to, and recency separates a live\n // folder from an abandoned duplicate.\n const best = [...owned].sort((a, b) => b.count - a.count || b.newest - a.newest)[0];\n\n if (best.name === row.encoded_dir) continue;\n\n out.push({\n id: row.id,\n slug: row.slug,\n rootPath: row.root_path,\n storedDir: row.encoded_dir,\n correctDir: best.name,\n transcripts: best.count,\n sessions: row.sessions,\n });\n }\n\n // Most sessions first: those are the projects where the breakage costs most.\n return out.sort((a, b) => b.sessions - a.sessions || b.transcripts - a.transcripts);\n}\n","/** Shared database helpers for registry command operations. */\n\nimport type { Database } from \"better-sqlite3\";\nimport { now } from \"../../utils.js\";\nimport { basename, join } from \"node:path\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\n\n/**\n * Would writing this encoded_dir replace a working one with a broken one?\n *\n * `encodeDir` is lossy: `~` and emoji do not survive it, so a value derived\n * from a project's path can name a folder Claude Code never created. Every\n * caller here derives its value that way, and this function is the single\n * point they all pass through.\n *\n * `pai registry reconnect` repairs those by reading the cwd recorded inside the\n * transcripts. Without this check the next scan silently reverts the repair —\n * and it reverts it in the most confusing possible way, because a read taken\n * straight after the repair still shows it. Observed twice on 2026-08-02:\n * three projects reconnected, verified, and rediscovered as broken within the\n * hour, with the repair command reporting success both times.\n *\n * Permits the write when the new value resolves to transcripts, or when the\n * existing one resolves to nothing — i.e. whenever it cannot make things worse.\n */\nfunction worthWriting(db: Database, projectId: number, encodedDir: string): boolean {\n if (transcriptFiles(join(claudeProjectsDir(), encodedDir)).length > 0) return true;\n\n const row = db\n .prepare(\"SELECT encoded_dir FROM projects WHERE id = ?\")\n .get(projectId) as { encoded_dir: string | null } | undefined;\n\n const current = row?.encoded_dir;\n if (!current) return true;\n\n return transcriptFiles(join(claudeProjectsDir(), current)).length === 0;\n}\n\n/**\n * Upsert a project row. Returns { id, isNew }.\n *\n * Matching priority:\n * 1. root_path — most reliable; handles slug collisions\n * 2. encoded_dir — Claude project dirs are canonical\n * 3. Insert with suffix-deduplication on slug collision\n *\n * display_name is set to basename(rootPath) on INSERT so that the unified\n * listing always shows a human-readable name rather than the kebab-case slug.\n */\nexport function upsertProject(\n db: Database,\n slug: string,\n rootPath: string,\n encodedDir: string\n): { id: number; isNew: boolean } {\n const ts = now();\n\n const byPath = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (byPath) {\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (\n (!encodedOwner || encodedOwner.id === byPath.id) &&\n worthWriting(db, byPath.id, encodedDir)\n ) {\n db.prepare(\n \"UPDATE projects SET encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(encodedDir, ts, byPath.id);\n }\n return { id: byPath.id, isNew: false };\n }\n\n const byEncoded = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(encodedDir) as { id: number } | undefined;\n\n if (byEncoded) {\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(rootPath) as { id: number } | undefined;\n\n if (!pathOwner || pathOwner.id === byEncoded.id) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(rootPath, ts, byEncoded.id);\n }\n return { id: byEncoded.id, isNew: false };\n }\n\n // Insert — deduplicate slug with numeric suffix if needed.\n let finalSlug = slug;\n let attempt = 0;\n while (true) {\n const conflict = db\n .prepare(\"SELECT id FROM projects WHERE slug = ?\")\n .get(finalSlug) as { id: number } | undefined;\n if (!conflict) break;\n attempt++;\n finalSlug = `${slug}-${attempt}`;\n }\n\n // Use basename(rootPath) as the human display name. If rootPath is empty or\n // just \"/\" fall back to the slug so we always have something non-empty.\n const displayName = basename(rootPath) || finalSlug;\n\n const result = db\n .prepare(\n `INSERT OR IGNORE INTO projects\n (slug, display_name, root_path, encoded_dir, type, status, created_at, updated_at)\n VALUES (?, ?, ?, ?, 'local', 'active', ?, ?)`\n )\n .run(finalSlug, displayName, rootPath, encodedDir, ts, ts);\n\n if (result.changes === 0) {\n const fallback =\n (db.prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\").get(encodedDir) as { id: number } | undefined) ??\n (db.prepare(\"SELECT id FROM projects WHERE root_path = ?\").get(rootPath) as { id: number } | undefined);\n\n if (fallback) {\n return { id: fallback.id, isNew: false };\n }\n\n throw new Error(\n `upsertProject: INSERT OR IGNORE was suppressed but no matching row found ` +\n `for root_path=${rootPath} encoded_dir=${encodedDir}`\n );\n }\n\n return { id: result.lastInsertRowid as number, isNew: true };\n}\n\n/** Upsert a session note. Returns true if newly inserted. */\nexport function upsertSession(\n db: Database,\n projectId: number,\n number: number,\n date: string,\n slug: string,\n title: string,\n filename: string\n): boolean {\n const existing = db\n .prepare(\"SELECT id FROM sessions WHERE project_id = ? AND number = ?\")\n .get(projectId, number);\n\n if (existing) return false;\n\n const ts = now();\n db.prepare(\n `INSERT INTO sessions\n (project_id, number, date, slug, title, filename, status, created_at)\n VALUES (?, ?, ?, ?, ?, ?, 'completed', ?)`\n ).run(projectId, number, date, slug, title, filename, ts);\n\n return true;\n}\n","/** Registry scan command: walk ~/.claude/projects/ and populate the registry. */\n\nimport { existsSync, readdirSync, statSync, readFileSync, writeFileSync, mkdirSync } from \"node:fs\";\nimport { realpathSync } from \"node:fs\";\nimport { join, basename, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { ok, warn, err, dim, bold } from \"../../utils.js\";\nimport { encodeDir } from \"../../utils.js\";\nimport { decodeEncodedDir, slugify, parseSessionFilename, buildEncodedDirMap } from \"../../../registry/migrate.js\";\nimport { ensurePaiMarker, discoverPaiMarkers } from \"../../../registry/pai-marker.js\";\nimport { transcriptFiles, claudeProjectsDir } from \"../../../registry/moved.js\";\nimport { upsertProject, upsertSession } from \"./utils.js\";\nimport type { Database } from \"better-sqlite3\";\n\n// ---------------------------------------------------------------------------\n// clc session.json fallback map\n// ---------------------------------------------------------------------------\n\n/**\n * Build a reverse map from encoded-dir → real path using the clc session\n * registry (~/.claude/session.json).\n *\n * The clc registry stores { sessions: [{ directory, ... }, ...] }. For each\n * entry with a resolvable directory, we encode the realpath and add it to\n * the map. This lets the scanner recover project paths that are not in\n * Claude's session-registry.json but were registered by clc.\n */\nfunction buildClcDirMap(): Map<string, string> {\n const map = new Map<string, string>();\n const sessionFile = join(homedir(), \".claude\", \"session.json\");\n if (!existsSync(sessionFile)) return map;\n\n try {\n const raw = readFileSync(sessionFile, \"utf8\");\n const parsed = JSON.parse(raw) as { sessions?: Array<{ directory?: string }> };\n for (const entry of parsed.sessions ?? []) {\n const dir = entry.directory;\n if (!dir) continue;\n try {\n const real = realpathSync(dir);\n if (!existsSync(real)) continue;\n const encoded = encodeDir(real);\n map.set(encoded, real);\n } catch {\n // Unresolvable path — skip\n }\n }\n } catch {\n // Unparseable — return empty map\n }\n return map;\n}\n\n// ---------------------------------------------------------------------------\n// Config helpers\n// ---------------------------------------------------------------------------\n\nconst CLAUDE_PROJECTS_DIR = join(homedir(), \".claude\", \"projects\");\nconst PAI_CONFIG_DIR = join(homedir(), \".pai\");\nconst PAI_CONFIG_FILE = join(PAI_CONFIG_DIR, \"config.json\");\n\ninterface PaiConfig {\n scan_dirs: string[];\n}\n\nexport function loadScanConfig(): PaiConfig {\n if (!existsSync(PAI_CONFIG_FILE)) return { scan_dirs: [] };\n try {\n return JSON.parse(readFileSync(PAI_CONFIG_FILE, \"utf8\")) as PaiConfig;\n } catch {\n return { scan_dirs: [] };\n }\n}\n\nexport function saveScanConfig(config: PaiConfig): void {\n mkdirSync(PAI_CONFIG_DIR, { recursive: true });\n writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + \"\\n\", \"utf8\");\n}\n\n/**\n * Resolve a path to its canonical form, falling back to the input.\n *\n * Every path that becomes a `projects.root_path` must go through this. That\n * column is UNIQUE, so two spellings of one directory create two projects and\n * split session history between them. The spellings are not hypothetical: a\n * configured scan_dir of `~/dev/ai` (where `~/dev` is a symlink) makes every\n * project under it register under the symlinked path, while a session started\n * from the resolved path registers a rival row.\n */\nexport function canonicalPath(p: string): string {\n try {\n return realpathSync(p);\n } catch {\n return p;\n }\n}\n\nexport function resolveHome(p: string): string {\n if (p.startsWith(\"~/\")) return join(homedir(), p.slice(2));\n return resolve(p);\n}\n\n// ---------------------------------------------------------------------------\n// File discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Recursively find all .md files in a directory, including YYYY/MM subdirectories.\n * Returns filenames (basename only).\n */\nexport function findNoteFiles(dir: string): string[] {\n const results: string[] = [];\n if (!existsSync(dir)) return results;\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.isFile() && entry.name.endsWith(\".md\")) {\n results.push(entry.name);\n } else if (entry.isDirectory() && /^\\d{4}$/.test(entry.name)) {\n const yearDir = join(dir, entry.name);\n for (const monthEntry of readdirSync(yearDir, { withFileTypes: true })) {\n if (monthEntry.isDirectory() && /^\\d{2}$/.test(monthEntry.name)) {\n const monthDir = join(yearDir, monthEntry.name);\n for (const noteEntry of readdirSync(monthDir, { withFileTypes: true })) {\n if (noteEntry.isFile() && noteEntry.name.endsWith(\".md\")) {\n results.push(noteEntry.name);\n }\n }\n }\n }\n }\n }\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Scan result type\n// ---------------------------------------------------------------------------\n\nexport interface ScanResult {\n projectsScanned: number;\n projectsNew: number;\n projectsUpdated: number;\n sessionsScanned: number;\n sessionsNew: number;\n skipped: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Core scan logic\n// ---------------------------------------------------------------------------\n\nexport function performScan(db: Database): ScanResult {\n const result: ScanResult = {\n projectsScanned: 0,\n projectsNew: 0,\n projectsUpdated: 0,\n sessionsScanned: 0,\n sessionsNew: 0,\n skipped: [],\n };\n\n if (!existsSync(CLAUDE_PROJECTS_DIR)) {\n throw new Error(`Claude projects directory not found: ${CLAUDE_PROJECTS_DIR}`);\n }\n\n const entries = readdirSync(CLAUDE_PROJECTS_DIR).filter((name) => {\n const full = join(CLAUDE_PROJECTS_DIR, name);\n return statSync(full).isDirectory();\n });\n\n const lookupMap = buildEncodedDirMap();\n const clcMap = buildClcDirMap();\n\n for (const encodedDir of entries) {\n let rootPath = decodeEncodedDir(encodedDir, lookupMap);\n\n // If the decoded path doesn't exist, try the clc session.json fallback map.\n // The clc map is keyed by encodeDir(realpathSync(directory)), so paths that\n // were registered by clc but not in session-registry.json can be recovered.\n if (!existsSync(rootPath) && clcMap.has(encodedDir)) {\n const clcPath = clcMap.get(encodedDir)!;\n if (existsSync(clcPath)) {\n rootPath = clcPath;\n }\n }\n\n if (!existsSync(rootPath)) {\n result.skipped.push(`${encodedDir} (decoded: ${rootPath} — path not found on disk)`);\n result.projectsScanned++;\n continue;\n }\n\n // Canonicalize before upserting.\n //\n // ~/.claude/projects/ holds one encoded entry per path *spelling*, so a\n // directory reachable through a symlinked prefix (~/dev -> Cloud/Development)\n // appears twice. Decoding each to its literal path registers two projects\n // for one directory, and session history then splits between them —\n // whichever spelling the shell used gets the session.\n //\n // upsertProject looks up by root_path first, so resolving here makes the\n // second spelling land on the row the first one created instead of\n // creating a rival. Both encoded_dir values stay valid for locating\n // central notes; only the project identity is unified.\n rootPath = canonicalPath(rootPath);\n\n const slug = slugify(basename(rootPath) || encodedDir);\n const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);\n\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try {\n ensurePaiMarker(rootPath, slug);\n } catch {\n // Non-fatal\n }\n\n const claudeNotesDir = join(CLAUDE_PROJECTS_DIR, encodedDir, \"Notes\");\n\n if (existsSync(claudeNotesDir)) {\n const rootNotesDir = join(rootPath, \"Notes\");\n if (claudeNotesDir !== rootNotesDir) {\n db.prepare(\n \"UPDATE projects SET claude_notes_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(claudeNotesDir, Date.now(), id);\n }\n }\n\n if (!existsSync(claudeNotesDir)) continue;\n\n const noteFiles = findNoteFiles(claudeNotesDir);\n\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n\n // Phase 2: Scan project-root Notes/ for all registered active projects\n {\n const activeProjects = db\n .prepare(\"SELECT id, slug, root_path FROM projects WHERE status = 'active'\")\n .all() as { id: number; slug: string; root_path: string }[];\n\n for (const project of activeProjects) {\n const notesDir = join(project.root_path, \"Notes\");\n if (!existsSync(notesDir)) continue;\n\n let files: string[];\n try {\n files = findNoteFiles(notesDir);\n } catch {\n continue;\n }\n\n for (const filename of files) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n\n result.sessionsScanned++;\n const isNewSession = upsertSession(db, project.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename);\n if (isNewSession) result.sessionsNew++;\n }\n }\n }\n\n // Phase 3: Scan extra directories from config\n const config = loadScanConfig();\n if (config.scan_dirs.length) {\n for (const rawDir of config.scan_dirs) {\n const scanDir = resolveHome(rawDir);\n if (!existsSync(scanDir)) {\n result.skipped.push(`${rawDir} (configured scan_dir not found)`);\n continue;\n }\n\n const children = readdirSync(scanDir).filter((name) => {\n if (name.startsWith(\".\")) return false;\n const full = join(scanDir, name);\n try { return statSync(full).isDirectory(); } catch { return false; }\n });\n\n for (const child of children) {\n // Canonicalize: a scan_dir of \"~/dev/ai\" (symlinked prefix) would\n // otherwise register every project under the symlinked spelling and\n // duplicate anything already registered under the resolved one.\n const childPath = canonicalPath(join(scanDir, child));\n const childSlug = slugify(child);\n const childEncoded = encodeDir(childPath);\n\n const existing = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(childPath) as { id: number } | undefined;\n\n if (existing) {\n result.projectsScanned++;\n result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, existing.id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n continue;\n }\n\n const { id, isNew } = upsertProject(db, childSlug, childPath, childEncoded);\n result.projectsScanned++;\n if (isNew) result.projectsNew++;\n else result.projectsUpdated++;\n\n try { ensurePaiMarker(childPath, childSlug); } catch { /* non-fatal */ }\n\n const notesDir = join(childPath, \"Notes\");\n if (existsSync(notesDir)) {\n const noteFiles = readdirSync(notesDir).filter((f) => f.endsWith(\".md\"));\n for (const filename of noteFiles) {\n const parsed = parseSessionFilename(filename);\n if (!parsed) continue;\n result.sessionsScanned++;\n if (upsertSession(db, id, parsed.number, parsed.date, parsed.slug, parsed.title, parsed.filename)) {\n result.sessionsNew++;\n }\n }\n }\n }\n }\n }\n\n // Phase 4: Discover PAI.md markers in scan_dirs\n if (config.scan_dirs.length) {\n const resolvedScanDirs = config.scan_dirs.map(resolveHome).filter(existsSync);\n const markers = discoverPaiMarkers(resolvedScanDirs);\n\n for (const marker of markers) {\n const registeredRow = db\n .prepare(\"SELECT id, root_path, slug, encoded_dir FROM projects WHERE slug = ?\")\n .get(marker.slug) as\n | { id: number; root_path: string; slug: string; encoded_dir: string | null }\n | undefined;\n\n if (!registeredRow) continue;\n\n // The marker was found by walking a scan_dir, so its projectRoot carries\n // whatever spelling that dir used. Canonicalize before comparing —\n // otherwise this \"repair\" step rewrites a correct root_path back to the\n // symlinked form on every scan, undoing any deduplication.\n const markerRoot = canonicalPath(marker.projectRoot);\n\n if (registeredRow.root_path !== markerRoot) {\n const newEncoded = encodeDir(markerRoot);\n const now4 = Date.now();\n\n const encodedOwner = db\n .prepare(\"SELECT id FROM projects WHERE encoded_dir = ?\")\n .get(newEncoded) as { id: number } | undefined;\n const pathOwner = db\n .prepare(\"SELECT id FROM projects WHERE root_path = ?\")\n .get(markerRoot) as { id: number } | undefined;\n\n const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;\n const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;\n\n // Never trade a working encoded_dir for one that resolves to nothing.\n //\n // encodeDir is lossy — `~` and emoji do not survive it — so re-deriving\n // from the path can produce a folder name Claude Code never created.\n // `pai registry reconnect` repairs exactly those by reading the cwd out\n // of the transcripts, and this scan runs every 30 minutes from the\n // daemon: without this check it silently reverts that repair, and the\n // repair looks like it worked because a fresh read right afterwards\n // still shows it. Observed on 2026-08-02 — three projects reconnected,\n // reverted, and rediscovered as broken within the hour.\n const derivedResolves = transcriptFiles(join(claudeProjectsDir(), newEncoded)).length > 0;\n const currentResolves =\n Boolean(registeredRow.encoded_dir) &&\n transcriptFiles(join(claudeProjectsDir(), registeredRow.encoded_dir!)).length > 0;\n const encodedWorthWriting = derivedResolves || !currentResolves;\n\n if (encodedSafe && pathSafe && encodedWorthWriting) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?\"\n ).run(markerRoot, newEncoded, now4, registeredRow.id);\n } else if (pathSafe) {\n db.prepare(\n \"UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?\"\n ).run(markerRoot, now4, registeredRow.id);\n }\n }\n }\n }\n\n // Phase 5: Backfill display_name for rows where it still equals the slug.\n // This converts legacy entries created before the basename-based naming was\n // introduced: \"jobs-beta\" → \"Jobs Beta\" (from basename of root_path).\n {\n const stale = db\n .prepare(\"SELECT id, slug, root_path FROM projects WHERE display_name = slug AND root_path IS NOT NULL AND root_path != ''\")\n .all() as { id: number; slug: string; root_path: string }[];\n\n for (const row of stale) {\n const name = basename(row.root_path);\n if (name && name !== row.slug) {\n db.prepare(\"UPDATE projects SET display_name = ?, updated_at = ? WHERE id = ?\")\n .run(name, Date.now(), row.id);\n }\n }\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// cmdScan\n// ---------------------------------------------------------------------------\n\n/**\n * Run the registry scan CLI command.\n *\n * @param opts.quick When true, skips verbose output (same scan, less noise).\n * The underlying scan is always incremental via upsert —\n * this flag exists for hook/daemon-triggered invocations that\n * want minimal log output.\n */\nexport function cmdScan(db: Database, opts: { quick?: boolean } = {}): void {\n const config = loadScanConfig();\n if (!opts.quick) {\n console.log(dim(\"Scanning ~/.claude/projects/ ...\"));\n if (config.scan_dirs.length) {\n console.log(dim(`Scanning ${config.scan_dirs.length} extra dir(s): ${config.scan_dirs.join(\", \")}`));\n }\n console.log(dim(\"Scanning project-root Notes/ directories ...\"));\n }\n\n let result: ScanResult;\n try {\n result = performScan(db);\n } catch (e) {\n console.error(err(String(e)));\n process.exitCode = 1;\n return;\n }\n\n if (!opts.quick) {\n console.log(\n ok(`Scanned ${bold(String(result.projectsScanned))} projects, ${bold(String(result.sessionsScanned))} session notes.`)\n );\n console.log(dim(` Projects: ${result.projectsNew} new, ${result.projectsUpdated} updated`));\n console.log(dim(` Sessions: ${result.sessionsNew} new`));\n\n if (result.skipped.length) {\n console.log();\n console.log(warn(` ${result.skipped.length} project(s) skipped (path not found on disk):`));\n for (const s of result.skipped.slice(0, 10)) {\n console.log(dim(` ${s}`));\n }\n if (result.skipped.length > 10) {\n console.log(dim(` ... and ${result.skipped.length - 10} more`));\n }\n }\n } else {\n // Quick mode: single compact line (stderr so it doesn't pollute pipe output)\n process.stderr.write(\n `[registry-scan] ${result.projectsScanned} projects, ${result.sessionsScanned} sessions ` +\n `(${result.projectsNew}+${result.sessionsNew} new).\\n`\n );\n }\n}\n","/**\n * triple-extraction-prompt.ts — Prompt template for KG triple extraction.\n *\n * Used by the session-summary-worker to extract structured facts from\n * a completed session summary and store them in the temporal knowledge graph.\n */\n\nexport function buildTripleExtractionPrompt(params: {\n sessionContent: string;\n projectSlug: string;\n gitLog: string;\n}): string {\n return `Extract structured entities and relations from this coding session.\n\nOutput a single JSON object with two arrays: \"entities\" and \"relations\".\n\nEntity types: project | person | concept | tool | file | version | decision | technology | organization\n\nRules:\n- Be SPECIFIC: entity names must be concrete (e.g., \"FSRS\", \"Glidr\", \"the owner\")\n- Use snake_case relation verb phrases (e.g., \"uses_algorithm\", \"decided_to\", \"shipped_version\")\n- Skip opinions, speculation, and \"we should\" statements\n- Skip entities obvious from project metadata unless they have a meaningful relation\n- Maximum 15 relations per session — pick the most important\n- Each entity should have a brief description (1 sentence, what it is in this context)\n- Each relation must reference entity names that appear in the entities array\n\nExample output:\n{\n \"entities\": [\n {\"name\": \"Glidr\", \"type\": \"project\", \"description\": \"Flashcard app using FSRS spaced repetition\"},\n {\"name\": \"FSRS\", \"type\": \"concept\", \"description\": \"Free Spaced Repetition Scheduler algorithm\"},\n {\"name\": \"the owner\", \"type\": \"person\", \"description\": \"Developer of Glidr and Quassl\"},\n {\"name\": \"Quassl\", \"type\": \"project\", \"description\": \"iOS app being rewritten in Flutter\"},\n {\"name\": \"Flutter\", \"type\": \"technology\", \"description\": \"Cross-platform mobile framework\"}\n ],\n \"relations\": [\n {\"source\": \"Glidr\", \"relation\": \"uses_algorithm\", \"target\": \"FSRS\"},\n {\"source\": \"Glidr\", \"relation\": \"shipped_version\", \"target\": \"1.0.5\"},\n {\"source\": \"the owner\", \"relation\": \"decided_to_rewrite\", \"target\": \"Quassl\"},\n {\"source\": \"Quassl\", \"relation\": \"migrating_to\", \"target\": \"Flutter\"}\n ]\n}\n\nPROJECT: ${params.projectSlug}\n\nSESSION CONTENT:\n${params.sessionContent}\n\nGIT COMMITS:\n${params.gitLog}\n\nJSON object (entities + relations):`;\n}\n","/**\n * kg-extraction.ts — Shared KG triple extraction logic.\n *\n * Extracted from session-summary-worker.ts so both the worker and the\n * CLI backfill (`pai kg backfill`) can use the same code path.\n *\n * Provides:\n * - findClaudeBinary() — locate the claude CLI\n * - spawnClaude() — generic prompt -> response runner (strips ANTHROPIC_API_KEY)\n * - extractAndStoreTriples() — run the extractor prompt and persist triples to Postgres\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport type { Pool } from \"pg\";\nimport type { Database } from \"better-sqlite3\";\n\nimport { buildTripleExtractionPrompt } from \"../daemon/templates/triple-extraction-prompt.js\";\nimport { kgAdd, kgQuery, kgInvalidate } from \"./kg.js\";\nimport { upsertKgEntity } from \"./kg-entity.js\";\n\n// ---------------------------------------------------------------------------\n// Claude CLI binary discovery\n// ---------------------------------------------------------------------------\n\n/**\n * Find the `claude` CLI binary. Checks common installation locations first\n * (launchd PATH is minimal so bare \"claude\" often won't resolve).\n */\nexport function findClaudeBinary(): string | null {\n const candidates = [\n join(homedir(), \".local\", \"bin\", \"claude\"),\n join(homedir(), \".claude\", \"local\", \"claude\"),\n \"/usr/local/bin/claude\",\n \"/opt/homebrew/bin/claude\",\n ];\n\n for (const candidate of candidates) {\n try {\n if (existsSync(candidate)) return candidate;\n } catch { /* skip */ }\n }\n return \"claude\";\n}\n\nconst CLAUDE_TIMEOUT_MS: Record<string, number> = {\n haiku: 60_000,\n sonnet: 120_000,\n opus: 300_000,\n};\n\n/**\n * Spawn the claude CLI with a prompt on stdin and return stdout.\n *\n * IMPORTANT: ANTHROPIC_API_KEY is stripped from the spawned environment so\n * the CLI uses the user's Max plan (free) instead of billing the API key.\n */\nexport async function spawnClaude(\n prompt: string,\n model: \"haiku\" | \"sonnet\" | \"opus\" = \"sonnet\"\n): Promise<string | null> {\n const claudeBin = findClaudeBinary();\n if (!claudeBin) {\n process.stderr.write(\"[kg-extraction] claude CLI not found.\\n\");\n return null;\n }\n\n const { spawn } = await import(\"node:child_process\");\n\n return new Promise((resolve) => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const { ANTHROPIC_API_KEY: _drop, ...envWithoutApiKey } = process.env;\n const child = spawn(\n claudeBin,\n [\"--model\", model, \"-p\", \"--no-session-persistence\"],\n { env: envWithoutApiKey, stdio: [\"pipe\", \"pipe\", \"pipe\"] }\n );\n\n let stdout = \"\";\n let stderr = \"\";\n\n child.stdout.on(\"data\", (chunk: Buffer) => { stdout += chunk.toString(); });\n child.stderr.on(\"data\", (chunk: Buffer) => { stderr += chunk.toString(); });\n\n child.on(\"error\", (err: Error) => {\n if (timer) { clearTimeout(timer); timer = null; }\n process.stderr.write(`[kg-extraction] ${model} spawn error: ${err.message}\\n`);\n resolve(null);\n });\n\n child.on(\"close\", (code: number | null) => {\n if (timer) { clearTimeout(timer); timer = null; }\n if (code !== 0) {\n process.stderr.write(\n `[kg-extraction] ${model} exited ${code}: ${stderr.slice(0, 300)}\\n`\n );\n resolve(null);\n } else {\n resolve(stdout.trim() || null);\n }\n });\n\n timer = setTimeout(() => {\n process.stderr.write(`[kg-extraction] ${model} timed out — killing process.\\n`);\n child.kill(\"SIGTERM\");\n resolve(null);\n }, CLAUDE_TIMEOUT_MS[model] ?? 120_000);\n\n child.stdin.write(prompt);\n child.stdin.end();\n });\n}\n\n// ---------------------------------------------------------------------------\n// Triple extraction\n// ---------------------------------------------------------------------------\n\nexport interface ExtractTriplesParams {\n summaryText: string;\n projectSlug: string;\n projectId: number | null;\n sessionId: string;\n gitLog?: string;\n model?: \"haiku\" | \"sonnet\" | \"opus\";\n /** Optional federation SQLite db — when provided, entities are upserted into kg_entities (QW1) */\n federationDb?: Database;\n /** Tenant ID for multi-tenant entity scoping (default: \"default\") */\n tenantId?: string;\n}\n\nexport interface ExtractTriplesResult {\n extracted: number;\n added: number;\n superseded: number;\n}\n\n/**\n * Extract structured KG triples from a session summary and store them in\n * Postgres. Idempotent: if a (subject, predicate) pair already has the same\n * object, no new row is added; if the object differs, the old triple is\n * invalidated (valid_to = NOW()) and a new one is inserted.\n *\n * Best-effort: per-triple errors are caught and logged but never thrown.\n * Returns a small stats object so callers can report progress.\n */\nexport async function extractAndStoreTriples(\n pool: Pool,\n params: ExtractTriplesParams\n): Promise<ExtractTriplesResult> {\n const stats: ExtractTriplesResult = { extracted: 0, added: 0, superseded: 0 };\n\n const prompt = buildTripleExtractionPrompt({\n sessionContent: params.summaryText,\n projectSlug: params.projectSlug,\n gitLog: params.gitLog ?? \"\",\n });\n\n const jsonOutput = await spawnClaude(prompt, params.model ?? \"sonnet\");\n if (!jsonOutput) return stats;\n\n // Strip markdown code fences if Claude wrapped the JSON\n const cleaned = jsonOutput\n .replace(/^```json\\s*/m, \"\")\n .replace(/^```\\s*/m, \"\")\n .replace(/\\s*```$/m, \"\")\n .trim();\n\n // Support both legacy array format and new structured format\n type LegacyTriple = { subject: string; predicate: string; object: string };\n type NewRelation = { source: string; relation: string; target: string };\n type NewEntity = { name: string; type: string; description: string };\n type NewFormat = { entities: NewEntity[]; relations: NewRelation[] };\n\n let triples: Array<LegacyTriple>;\n try {\n const parsed = JSON.parse(cleaned);\n\n if (Array.isArray(parsed)) {\n // Legacy format: [{subject, predicate, object}]\n triples = parsed;\n } else if (parsed && typeof parsed === \"object\" && Array.isArray(parsed.relations)) {\n // New structured format: {entities: [...], relations: [...]}\n const newFmt = parsed as NewFormat;\n\n // QW1: Upsert entities into federation SQLite kg_entities table when db is available\n if (params.federationDb && Array.isArray(newFmt.entities)) {\n const tenantId = params.tenantId ?? \"default\";\n for (const entity of newFmt.entities) {\n if (!entity.name) continue;\n try {\n upsertKgEntity(params.federationDb, {\n name: entity.name,\n type: entity.type ?? \"unknown\",\n description: entity.description,\n tenantId,\n });\n } catch (entityErr) {\n process.stderr.write(`[kg-extraction] entity upsert error (${entity.name}): ${entityErr}\\n`);\n }\n }\n }\n\n triples = newFmt.relations.map((r: NewRelation) => ({\n subject: r.source,\n predicate: r.relation,\n object: r.target,\n }));\n } else {\n process.stderr.write(`[kg-extraction] Unexpected JSON shape — neither array nor {entities,relations}\\n`);\n return stats;\n }\n } catch (e) {\n process.stderr.write(`[kg-extraction] JSON parse failed: ${e}\\n`);\n return stats;\n }\n\n if (!Array.isArray(triples)) return stats;\n stats.extracted = triples.length;\n\n for (const t of triples) {\n if (!t.subject || !t.predicate || !t.object) continue;\n\n try {\n const existing = await kgQuery(pool, {\n subject: t.subject,\n predicate: t.predicate,\n project_id: params.projectId ?? undefined,\n });\n\n // If an identical (subject, predicate, object) is already valid, skip — idempotent\n const alreadyValid = existing.find((e) => e.object === t.object && !e.valid_to);\n if (alreadyValid) continue;\n\n // Invalidate any superseded triple (same subject+predicate, different object)\n const supersedes = existing.find((e) => e.object !== t.object && !e.valid_to);\n if (supersedes) {\n await kgInvalidate(pool, supersedes.id);\n stats.superseded++;\n }\n\n await kgAdd(pool, {\n subject: t.subject,\n predicate: t.predicate,\n object: t.object,\n project_id: params.projectId ?? undefined,\n source_session: params.sessionId,\n confidence: \"EXTRACTED\",\n });\n stats.added++;\n } catch (tripleErr) {\n process.stderr.write(`[kg-extraction] store error (${t.subject}): ${tripleErr}\\n`);\n }\n }\n\n return stats;\n}\n","/**\n * Shared \"## Continue\" checkpoint logic for a project's TODO.md.\n *\n * WHY THIS MODULE EXISTS\n * ----------------------\n * Before this, `pause.ts` and `handover.ts` each carried their own copy of\n * findProjectTodo / stripContinueSection / block-builder. Both wrote the same\n * fixed four-line block, and both stripped any existing ## Continue section\n * unconditionally. The consequence was that a rich, model-authored checkpoint\n * could never survive:\n *\n * 1. `pai pause` had no way to accept a body, so the model printed its\n * checkpoint to the terminal and it was lost.\n * 2. Even if a body had been written by hand, the session-stop hook runs\n * `pai session handover` on every clean exit, which regenerated the\n * generic block and erased it.\n *\n * So there are two jobs here:\n *\n * - AUTHORED writes (`pai pause --body-file`) carry the model's markdown\n * verbatim, wrapped in explicit start/end markers.\n * - AUTO writes (hooks) must never destroy an authored checkpoint belonging\n * to the *same* session. They may replace a stale one left by an earlier\n * session, otherwise TODO.md would show a checkpoint that no longer\n * describes where the work stands.\n *\n * PARSING\n * -------\n * A rich body can legitimately contain `---` rules and `##` headings, which the\n * old heuristic scanner treated as section terminators. Authored blocks are\n * therefore delimited by an explicit HTML-comment pair; the legacy heuristic is\n * kept only as a fallback for blocks written before this change.\n */\n\nimport {\n existsSync,\n readFileSync,\n writeFileSync,\n readdirSync,\n renameSync,\n mkdirSync,\n realpathSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Locations searched for a project TODO.md, in priority order. */\nexport const TODO_LOCATIONS = [\n \"Notes/TODO.md\",\n \".claude/Notes/TODO.md\",\n \"tasks/todo.md\",\n \"TODO.md\",\n];\n\nexport const MARKER_OPEN = \"<!-- pai:checkpoint\";\nexport const MARKER_CLOSE = \"<!-- /pai:checkpoint -->\";\n\n/**\n * Markers for a handover that has been superseded but not thrown away.\n *\n * Deliberately NOT `pai:checkpoint`: `locateContinue` scans for that marker and\n * for the `## Continue` heading, so an archived block written verbatim would be\n * found as the live section and rewritten in place of it. Archived entries are\n * inert by construction — different marker, no heading.\n */\nexport const ARCHIVE_OPEN = \"<!-- pai:archived-handover\";\nexport const ARCHIVE_CLOSE = \"<!-- /pai:archived-handover -->\";\nexport const ARCHIVE_HEADING = \"## Previous handovers\";\n\n/**\n * How many superseded handovers TODO.md keeps.\n *\n * Enough that a handover survives a run of sessions that each end without\n * pausing, which is the sequence that destroyed one on 2026-08-04. Not\n * unbounded: TODO.md is read by a human at the top of every session, and a file\n * that grows without limit stops being read, which is its own kind of loss.\n */\nexport const ARCHIVE_LIMIT = 5;\n\nexport const CONTINUE_HEADING = \"## Continue\";\n\n/** Who wrote the checkpoint currently in TODO.md. */\nexport type Authorship = \"model\" | \"auto\";\n\n/** Result of applying a checkpoint. */\nexport type ApplyAction = \"written\" | \"preserved\" | \"failed\";\n\n// ---------------------------------------------------------------------------\n// TODO.md discovery\n// ---------------------------------------------------------------------------\n\nexport function findProjectTodo(\n rootPath: string\n): { path: string; content: string } | null {\n for (const rel of TODO_LOCATIONS) {\n const full = join(rootPath, rel);\n if (existsSync(full)) {\n try {\n return { path: full, content: readFileSync(full, \"utf8\") };\n } catch {\n // unreadable — try next\n }\n }\n }\n return null;\n}\n\n/**\n * Resolve the TODO.md to write to, creating Notes/ if nothing exists yet.\n * Returns null only when the directory could not be created.\n */\nexport function resolveTodoTarget(\n rootPath: string,\n opts: { create?: boolean } = {}\n): { path: string; content: string } | null {\n const found = findProjectTodo(rootPath);\n if (found) return found;\n\n const notesDir = join(rootPath, \"Notes\");\n if (opts.create !== false) {\n try {\n if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });\n } catch {\n return null;\n }\n }\n return { path: join(notesDir, \"TODO.md\"), content: \"\" };\n}\n\n// ---------------------------------------------------------------------------\n// Marker parsing\n// ---------------------------------------------------------------------------\n\nexport interface CheckpointMeta {\n authored: Authorship;\n /** Session line the checkpoint describes, e.g. \"0003 - 2026-08-01 - Title\". */\n session?: string;\n /** Claude Code session UUID, when known. */\n sessionId?: string;\n ts?: string;\n}\n\n/**\n * Parse a `<!-- pai:checkpoint key=\"value\" ... -->` marker line.\n * Returns null when the line is not a marker.\n */\nexport function parseMarker(line: string): CheckpointMeta | null {\n const trimmed = line.trim();\n if (!trimmed.startsWith(MARKER_OPEN)) return null;\n\n const attrs: Record<string, string> = {};\n for (const m of trimmed.matchAll(/([a-zA-Z][\\w-]*)=\"([^\"]*)\"/g)) {\n attrs[m[1]] = m[2];\n }\n\n return {\n authored: attrs.authored === \"model\" ? \"model\" : \"auto\",\n session: attrs.session || undefined,\n sessionId: attrs[\"session-id\"] || undefined,\n ts: attrs.ts || undefined,\n };\n}\n\nexport interface LocatedContinue {\n /** Index of the \"## Continue\" heading line. */\n startIdx: number;\n /** Exclusive end index, past any trailing `---` separator. */\n endIdx: number;\n meta: CheckpointMeta | null;\n /** Raw lines of the section, heading included. */\n lines: string[];\n}\n\n/**\n * Lines an auto-generated block is made of. Anything else in a section is\n * content somebody put there deliberately.\n *\n * The blockquote marker is optional on every pattern. These lines appear\n * quoted when the block builder emits them as its header, and unquoted when a\n * caller passes the same text as a BODY — which the session-stop hook does: it\n * builds `Working directory: ${cwd}` and hands it over as state. Matching only\n * the quoted form made such a body read as content, which is how a session\n * that did no work at all overwrote a real handover on 2026-08-04. A line that\n * merely restates the generated header is boilerplate wherever it sits.\n */\nconst BOILERPLATE_PATTERNS = [\n /^##\\s+Continue$/,\n /^<!--\\s*\\/?pai:checkpoint/,\n /^>?\\s*\\*\\*Last session:\\*\\*/,\n /^>?\\s*\\*\\*Paused at:\\*\\*/,\n /^>?\\s*Working directory:/,\n /^>?\\s*Resume with:/,\n /^>?\\s*_No checkpoint body was recorded/,\n /^-{3,}$/,\n];\n\n/** A blank line, with or without a blockquote marker. */\nconst BLANK_LINE = /^>?\\s*$/;\n\n/**\n * True when a section contains nothing but generated header lines.\n *\n * This is the guard that stops an auto write from destroying content it did\n * not author. A session that hit the old clobbering bug may have worked around\n * it by hand — writing its state into a subsection underneath the generated\n * header lines, in an unmarked block. Those blocks predate the marker, so\n * authorship cannot be read off them; the only safe signal is whether anything\n * beyond boilerplate is present.\n */\nexport function isBoilerplateOnly(lines: string[]): boolean {\n return lines.every((line) => {\n const t = line.trim();\n return BLANK_LINE.test(t) || BOILERPLATE_PATTERNS.some((re) => re.test(t));\n });\n}\n\n/**\n * True when a checkpoint body says something the generated header does not.\n *\n * \"Has a body\" and \"has something to say\" are not the same question, and the\n * preservation rules care only about the second. An unattended writer that\n * emits a single line restating the working directory has produced a body by\n * any string test and a handover by none.\n */\nexport function hasSubstance(body: string | null | undefined): boolean {\n const text = (body ?? \"\").trim();\n if (!text) return false;\n return !isBoilerplateOnly(text.split(\"\\n\"));\n}\n\n/**\n * Extract the non-boilerplate content of a section, or \"\" if there is none.\n *\n * Interior blank lines are kept. They are not decoration — in Markdown they\n * are what separates a paragraph from the table or list that follows, so\n * dropping them (as this did while it treated every blank line as boilerplate)\n * silently welds a checkpoint body into one unreadable run.\n */\nexport function extractSectionContent(lines: string[]): string {\n const kept: string[] = [];\n for (const line of lines) {\n const t = line.trim();\n if (BOILERPLATE_PATTERNS.some((re) => re.test(t))) continue;\n kept.push(line);\n }\n return trimBlankEdges(collapseBlankRuns(kept)).join(\"\\n\");\n}\n\n/** Drop leading and trailing blank lines, leaving interior spacing alone. */\nfunction trimBlankEdges(lines: string[]): string[] {\n let start = 0;\n let end = lines.length;\n while (start < end && BLANK_LINE.test(lines[start].trim())) start += 1;\n while (end > start && BLANK_LINE.test(lines[end - 1].trim())) end -= 1;\n return lines.slice(start, end);\n}\n\n/**\n * Collapse runs of blank lines to a single blank.\n *\n * Removing a boilerplate line leaves the blank that surrounded it behind, so\n * stripping the header can open a three-line gap in the middle of the body.\n * One blank line is all Markdown needs.\n */\nfunction collapseBlankRuns(lines: string[]): string[] {\n const out: string[] = [];\n let lastWasBlank = false;\n for (const line of lines) {\n const isBlank = BLANK_LINE.test(line.trim());\n if (isBlank && lastWasBlank) continue;\n out.push(line);\n lastWasBlank = isBlank;\n }\n return out;\n}\n\n/**\n * Locate the existing ## Continue section.\n *\n * When the section carries an explicit marker pair, the close marker defines\n * the end — this is what lets a rich body contain `---` and `##` safely.\n * Otherwise the legacy heuristic applies: stop at the first `---` or the next\n * `##` heading.\n */\nexport function locateContinue(content: string): LocatedContinue | null {\n const lines = content.split(\"\\n\");\n const startIdx = lines.findIndex((l) => l.trim() === CONTINUE_HEADING);\n if (startIdx === -1) return null;\n\n // Look for an open marker in the first few lines of the section.\n let meta: CheckpointMeta | null = null;\n let markerIdx = -1;\n for (let i = startIdx + 1; i < Math.min(startIdx + 6, lines.length); i++) {\n const parsed = parseMarker(lines[i]);\n if (parsed) {\n meta = parsed;\n markerIdx = i;\n break;\n }\n // A non-blank, non-marker line means there is no marker for this section.\n if (lines[i].trim() !== \"\") break;\n }\n\n let endIdx = lines.length;\n\n if (markerIdx !== -1) {\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx !== -1) {\n endIdx = closeIdx + 1;\n } else {\n // Malformed (open without close) — fall back to the heuristic so we do\n // not swallow the rest of the file.\n endIdx = heuristicEnd(lines, startIdx);\n }\n } else {\n endIdx = heuristicEnd(lines, startIdx);\n }\n\n // Consume one trailing `---` separator and the blank lines around it.\n let trailingEnd = endIdx;\n while (trailingEnd < lines.length && lines[trailingEnd].trim() === \"\") {\n trailingEnd += 1;\n }\n if (trailingEnd < lines.length && lines[trailingEnd].trim() === \"---\") {\n trailingEnd += 1;\n } else {\n trailingEnd = endIdx;\n }\n\n return {\n startIdx,\n endIdx: trailingEnd,\n meta,\n lines: lines.slice(startIdx, trailingEnd),\n };\n}\n\n/**\n * Legacy scanner for blocks written before checkpoint markers existed.\n *\n * Terminates on a horizontal rule or the next level-1 or level-2 heading. Note\n * the `(?!#)` — `###` is a *subsection* of `## Continue`, not a terminator. The\n * original scanner stopped at any run of `#`, which meant a `### Restored\n * state` subsection fell outside the section entirely and could not be seen,\n * let alone carried forward.\n *\n * `#{1,2}` rather than `##`, because matching only `##` did not stop at an H1\n * and therefore ATE IT. A TODO.md whose `## Continue` block precedes its\n * `# TODO` title had the title absorbed into the section and destroyed on the\n * next regenerate — reported independently by three sessions on 2026-08-03,\n * one of which lost it three times and recovered from git each time.\n *\n * An H1 is a document-level heading. It can never be part of a `## Continue`\n * section, so treating it as a terminator is not a heuristic improvement but a\n * structural fact.\n */\nfunction heuristicEnd(lines: string[], startIdx: number): number {\n for (let i = startIdx + 1; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (\n trimmed === \"---\" ||\n (/^#{1,2}(?!#)/.test(trimmed) && trimmed !== CONTINUE_HEADING)\n ) {\n return i;\n }\n }\n return lines.length;\n}\n\n/** Remove the ## Continue section, returning the remainder of the document. */\nexport function stripContinue(content: string): string {\n const found = locateContinue(content);\n if (!found) return content;\n\n const lines = content.split(\"\\n\");\n const before = lines.slice(0, found.startIdx);\n const after = lines.slice(found.endIdx);\n while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n return [...before, ...after].join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Reading a checkpoint back\n// ---------------------------------------------------------------------------\n\nexport interface ContinueCheckpoint {\n meta: CheckpointMeta | null;\n /** The authored body — the section with generated header lines removed. */\n body: string;\n /** The full section, heading included. */\n raw: string;\n}\n\n/**\n * Read the `## Continue` checkpoint out of a TODO.md.\n *\n * Writing a checkpoint is only half of a handover; something has to deliver it\n * to the next session. This is the read side, used by the SessionStart hook.\n *\n * Returns null when there is no section, or when the section holds nothing but\n * generated header lines — a bodyless block carries no information a new\n * session does not already have, and injecting it would only add noise.\n */\nexport function readContinueCheckpoint(\n content: string\n): ContinueCheckpoint | null {\n const found = locateContinue(content);\n if (!found) return null;\n\n const body = found.meta\n ? extractMarkedBody(found.lines)\n : extractSectionContent(found.lines);\n if (!body) return null;\n\n return { meta: found.meta, body, raw: found.lines.join(\"\\n\") };\n}\n\n/**\n * Extract the body of a marker-delimited block by position rather than by\n * pattern.\n *\n * The layout is fixed: open marker, a contiguous run of `>` header lines, the\n * body, then the close marker. Because the boundaries are known exactly, the\n * body comes back byte-for-byte — including any `---` rules or `##` headings\n * of its own, which a pattern-based filter would mistake for boilerplate and\n * delete. Falls back to the pattern filter if the shape is not as expected.\n */\nfunction extractMarkedBody(lines: string[]): string {\n const markerIdx = lines.findIndex((l) => parseMarker(l) !== null);\n if (markerIdx === -1) return extractSectionContent(lines);\n\n const closeIdx = lines.findIndex(\n (l, i) => i > markerIdx && l.trim() === MARKER_CLOSE\n );\n if (closeIdx === -1) return extractSectionContent(lines);\n\n // Skip the blank lines and the `>` header run that follow the open marker.\n let bodyStart = markerIdx + 1;\n while (bodyStart < closeIdx) {\n const t = lines[bodyStart].trim();\n if (BLANK_LINE.test(t) || t.startsWith(\">\")) {\n bodyStart += 1;\n continue;\n }\n break;\n }\n\n return trimBlankEdges(lines.slice(bodyStart, closeIdx)).join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Block construction\n// ---------------------------------------------------------------------------\n\nexport interface BuildOptions {\n authored: Authorship;\n /** Human-readable session line, or \"Unknown session\". */\n sessionLine: string;\n /** Claude Code session UUID — the `claude --resume` handle. */\n sessionId?: string;\n cwd: string;\n /** Model-authored markdown. Omitted for auto blocks. */\n body?: string;\n /** Overridable for deterministic tests. */\n timestamp?: string;\n}\n\nfunction escapeAttr(value: string): string {\n return value.replace(/\"/g, \"'\");\n}\n\nexport function buildContinueBlock(opts: BuildOptions): string {\n const ts = opts.timestamp ?? new Date().toISOString();\n\n const attrs = [\n `authored=\"${opts.authored}\"`,\n `session=\"${escapeAttr(opts.sessionLine)}\"`,\n opts.sessionId ? `session-id=\"${escapeAttr(opts.sessionId)}\"` : null,\n `ts=\"${ts}\"`,\n ]\n .filter(Boolean)\n .join(\" \");\n\n const header = [\n `> **Last session:** ${opts.sessionLine}`,\n `> **Paused at:** ${ts}`,\n \">\",\n `> Working directory: ${opts.cwd}`,\n ];\n\n if (opts.sessionId) {\n header.push(\">\", `> Resume with: \\`claude --resume ${opts.sessionId}\\``);\n }\n\n const body = (opts.body ?? \"\").trim();\n\n const parts = [\n CONTINUE_HEADING,\n \"\",\n `${MARKER_OPEN} ${attrs} -->`,\n \"\",\n ...header,\n ];\n\n if (body) {\n parts.push(\"\", body);\n } else {\n parts.push(\n \">\",\n \"> _No checkpoint body was recorded — see the latest session note._\"\n );\n }\n\n parts.push(\"\", MARKER_CLOSE, \"\", \"---\", \"\");\n\n return parts.join(\"\\n\");\n}\n\n// ---------------------------------------------------------------------------\n// Apply\n// ---------------------------------------------------------------------------\n\nexport interface ApplyOptions extends BuildOptions {\n /** Project root; TODO.md is resolved beneath it. */\n rootPath: string;\n /** Preview only — nothing is written. */\n dryRun?: boolean;\n}\n\nexport interface ApplyResult {\n action: ApplyAction;\n path: string | null;\n block: string;\n /** Set when action === \"preserved\". */\n preservedMeta?: CheckpointMeta;\n /** Set when unattributed content was carried forward into the new block. */\n carriedForward?: boolean;\n /** Set when a superseded model-authored handover was moved to the archive. */\n archived?: boolean;\n error?: string;\n}\n\n/**\n * Write the ## Continue block, honouring the preservation rules.\n *\n * An AUTO write is unattended — it fires from the session-stop and pre-compact\n * hooks — so it operates under one governing rule: **never destroy content it\n * did not author.** Three cases follow from that:\n *\n * 1. An authored checkpoint for the SAME session is left untouched. The hooks\n * fire after the model has already recorded the real state; overwriting it\n * with metadata is the bug this module exists to fix.\n *\n * \"Same session\" is decided by the Claude session UUID whenever both sides\n * know it, and only falls back to the human-readable session line when one\n * of them does not. The line is derived from the session note filename,\n * and `session-stop.sh` *renames and renumbers that file* — via `session\n * slug --apply` and `session cleanup --execute` — before it reaches the\n * handover step. So the key the hook computes at exit is not the key the\n * model wrote seconds earlier, and a filename-keyed comparison mismatches\n * by construction. Observed live on 2026-08-01: notes renumbered twice\n * within a single session. The UUID is the only identifier that holds\n * still.\n * 2. An authored checkpoint from an EARLIER session is stale — TODO.md would\n * otherwise keep pointing at the wrong session — so it is replaced. Its\n * content is not lost: `pai pause` mirrors every authored body into the\n * session note.\n * 3. An UNMARKED block predates the marker, so authorship cannot be read off\n * it. If it is nothing but generated header lines it is replaced. If it\n * carries anything else, that content was put there deliberately — quite\n * possibly as a hand-rolled workaround for the very clobbering this fixes —\n * and is carried forward into the new block rather than dropped.\n *\n * A MODEL write always replaces: the model is authoring the checkpoint, and a\n * newer one supersedes an older one.\n */\n/**\n * Do an existing checkpoint and an incoming write describe the same session?\n *\n * The UUID is authoritative when both sides carry one: it is assigned by Claude\n * Code and never changes for the life of the session. The session line is a\n * derived, mutable label and is only consulted when there is no UUID to compare\n * — a checkpoint written before `--session-id` was threaded through, or an auto\n * write from a caller that was not given one.\n */\nfunction isSameSession(\n meta: CheckpointMeta,\n opts: Pick<ApplyOptions, \"sessionId\" | \"sessionLine\">\n): boolean {\n if (meta.sessionId && opts.sessionId) {\n return meta.sessionId === opts.sessionId;\n }\n return meta.session === opts.sessionLine;\n}\n\n/**\n * How long a model-authored checkpoint is protected from automated overwriting.\n *\n * Sized for the race, not for the session: the gap between a model writing its\n * checkpoint and a hook firing is seconds to minutes, and an hour covers even a\n * slow checkpoint on a loaded machine. Anything genuinely from an earlier\n * session is hours or days old and still falls through to case 2, which is what\n * keeps TODO.md from freezing on a checkpoint nobody will ever replace.\n */\nconst AUTHORED_GRACE_MS = 60 * 60 * 1000;\n\n/** Was this checkpoint written recently enough to still belong to the run in progress? */\nfunction isRecent(meta: CheckpointMeta, now?: string): boolean {\n if (!meta.ts) return false; // no stamp, no claim — case 2 decides as before\n const then = Date.parse(meta.ts);\n if (Number.isNaN(then)) return false;\n const nowMs = now ? Date.parse(now) : Date.now();\n if (Number.isNaN(nowMs)) return false;\n // Guard the future too: a clock skew that puts the stamp ahead of now must\n // not read as \"very old\" and license an overwrite.\n return nowMs - then < AUTHORED_GRACE_MS;\n}\n\n/**\n * Turn a live checkpoint block into an inert archive entry.\n *\n * Two things have to go: the `## Continue` heading and the `pai:checkpoint`\n * marker. Both are what `locateContinue` looks for, so leaving either in place\n * would let a later write treat the archive as the live section — and the next\n * one after that would then archive the archive. Everything else is kept\n * verbatim, because the body is the whole reason this exists.\n */\nfunction toArchiveEntry(lines: string[], meta: CheckpointMeta | null): string[] {\n const out: string[] = [];\n const label = meta?.session ?? \"Unknown session\";\n const stamp = meta?.ts ? ` — checkpointed ${meta.ts}` : \"\";\n out.push(`${ARCHIVE_OPEN} session=\"${label}\"${meta?.ts ? ` ts=\"${meta.ts}\"` : \"\"} -->`);\n out.push(\"\");\n out.push(`### ${label}${stamp}`);\n out.push(\"\");\n for (const line of lines) {\n if (line.trim() === CONTINUE_HEADING) continue;\n if (line.trim().startsWith(MARKER_OPEN)) continue;\n if (line.trim() === MARKER_CLOSE) continue;\n if (line.trim() === \"---\") continue;\n out.push(line);\n }\n while (out.length > 0 && out[out.length - 1].trim() === \"\") out.pop();\n out.push(\"\");\n out.push(ARCHIVE_CLOSE);\n return out;\n}\n\n/**\n * Put a superseded handover into the archive section, newest first.\n *\n * `rest` is the document with the Continue section already stripped. The\n * archive lives immediately below it so a reader meets the current handover\n * first and the previous ones directly after, rather than hunting for them at\n * the bottom of a file that also holds the project's open work.\n */\nfunction archiveInto(rest: string, entry: string[]): string {\n const lines = rest.split(\"\\n\");\n const headingIdx = lines.findIndex((l) => l.trim() === ARCHIVE_HEADING);\n\n if (headingIdx === -1) {\n return [ARCHIVE_HEADING, \"\", ...entry, \"\", \"---\", \"\", rest.trimStart()].join(\"\\n\");\n }\n\n // Insert directly under the heading, then drop whatever falls past the cap.\n const before = lines.slice(0, headingIdx + 1);\n const after = lines.slice(headingIdx + 1);\n while (after.length > 0 && after[0].trim() === \"\") after.shift();\n\n const merged = [...entry, \"\", ...after];\n const kept: string[] = [];\n let seen = 0;\n for (let i = 0; i < merged.length; i++) {\n if (merged[i].trim().startsWith(ARCHIVE_OPEN)) {\n seen += 1;\n if (seen > ARCHIVE_LIMIT) {\n // Everything from here to the end of THIS entry goes; keep scanning so\n // anything after the archive section (other headings, open work) stays.\n while (i < merged.length && merged[i].trim() !== ARCHIVE_CLOSE) i++;\n continue; // skips the close marker too\n }\n }\n kept.push(merged[i]);\n }\n return [...before, \"\", ...kept].join(\"\\n\");\n}\n\nexport function applyContinue(opts: ApplyOptions): ApplyResult {\n const target = resolveTodoTarget(opts.rootPath, { create: !opts.dryRun });\n if (!target) {\n return {\n action: \"failed\",\n path: null,\n block: buildContinueBlock(opts),\n error: \"Could not resolve or create a TODO.md target\",\n };\n }\n\n const existing = locateContinue(target.content);\n let carriedForward = false;\n let effectiveBody = opts.body;\n let archiveEntry: string[] | null = null;\n\n if (opts.authored === \"auto\" && existing) {\n // Case 1 — authored, same session: hands off.\n if (existing.meta?.authored === \"model\" && isSameSession(existing.meta, opts)) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta,\n };\n }\n\n // Case 1b — the incoming write has nothing to say, and what is already\n // there does. A metadata-only block says \"see the latest session note\", so\n // replacing a real handover with one trades content for a pointer — and the\n // pointer is not always good: observed live on 2026-08-01 in the AIBroker\n // project, where the stop hook's bodyless handover overwrote the autosave's\n // body and named a session note that had never been created, leaving the\n // next session nothing to resume from.\n //\n // Deferring to the session note is only safe when the note demonstrably has\n // the content. This code cannot see that, so it does the one thing that is\n // never wrong: a write with nothing to say does not get to destroy\n // something that does. A stale-but-real handover beats a fresh dead link.\n // Scoped to blocks we marked. An unmarked block falls through to case 3,\n // which salvages its content into the new block rather than freezing the\n // old one — better, because an unmarked block has no session metadata worth\n // preserving and may predate this scheme entirely.\n //\n // The test is SUBSTANCE, not emptiness. It was emptiness until 2026-08-04,\n // when a session that was opened and immediately exited destroyed the\n // previous day's handover for every project it touched. Its stop hook found\n // no work items and no completion message, so it sent the one line it\n // always builds unconditionally — `Working directory: …`, a verbatim copy\n // of a line the header already generates. Non-empty by a string test, so\n // this guard stood down; worthless by any other reading. `hasSubstance`\n // asks the question the comment above always claimed to be asking.\n if (\n existing.meta &&\n !hasSubstance(opts.body) &&\n !isBoilerplateOnly(existing.lines)\n ) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta ?? undefined,\n };\n }\n\n // Case 2b — a model checkpoint written moments ago is THIS session's,\n // whatever the identity comparison says.\n //\n // Case 2 deliberately lets an auto write replace an authored checkpoint\n // from an EARLIER session, and that policy is right: TODO.md would freeze\n // otherwise, and `pai pause` mirrors authored bodies into the session note.\n // The bug is not the policy, it is that identity DEGRADES and healthy\n // sessions fall into it.\n //\n // `sessionId` is optional on `pai pause`. Without it isSameSession compares\n // a display line containing the note TITLE — and pausing renames the note.\n // So a session writes a checkpoint, its note is renamed, the hook fires\n // seconds later, no longer recognises its own work, and classifies it as a\n // stale earlier session. Three sessions reported exactly this on\n // 2026-08-03 from one `pai pause all`; one lost its checkpoint three times\n // and recovered from git each time. Sessions without a clean repo would\n // never have known it happened.\n //\n // Recency is the tiebreaker that identity cannot supply. A model checkpoint\n // minutes old is not a stale predecessor by any reading, so an automated\n // digest does not get to overwrite it. Older ones still fall through to\n // case 2 and are replaced as before, which is what stops TODO.md freezing\n // and keeps this from nesting carried-forward blocks without end.\n if (\n existing.meta?.authored === \"model\" &&\n isRecent(existing.meta, opts.timestamp) &&\n !isBoilerplateOnly(existing.lines)\n ) {\n return {\n action: \"preserved\",\n path: target.path,\n block: buildContinueBlock(opts),\n preservedMeta: existing.meta,\n };\n }\n\n // Case 2c — an authored handover from an EARLIER session, past the grace\n // window. Case 2 deleted it, on the reasoning that TODO.md must not freeze\n // and that `pai pause` mirrors authored bodies into the session note.\n //\n // The first half is right. The second is an assumption, and on 2026-08-04\n // it was false in the plainest way available: this project keeps no session\n // notes at all (`Notes/*` is gitignored and none exist on disk), so the\n // checkpoint WAS the only copy. It went at 08:36:29Z, to the autosave of a\n // session that had been open for sixteen minutes and had typed one line —\n // destroying, as its first act, the handover it had been started to read.\n //\n // This is also the escape the v0.27.2 guard could not close: that one asks\n // whether the block is RECENT, and a handover written the night before is\n // not. Age was never the question. Nothing here needs the old block gone;\n // it only needs the slot. So the block moves down the file instead, and the\n // freeze argument and the loss argument stop being in tension.\n //\n // Only for `model` blocks with something in them. An auto block is a\n // transcript scrape that regenerates on the next tick, and archiving those\n // would bury the real handovers under mechanical noise.\n if (\n existing.meta?.authored === \"model\" &&\n !isBoilerplateOnly(existing.lines) &&\n !isSameSession(existing.meta, opts)\n ) {\n archiveEntry = toArchiveEntry(existing.lines, existing.meta);\n }\n\n // Case 3 — unmarked block holding content nobody can attribute to us.\n if (!existing.meta && !isBoilerplateOnly(existing.lines)) {\n const salvaged = extractSectionContent(existing.lines);\n if (salvaged) {\n effectiveBody = [\n \"_Carried forward from the previous checkpoint (author unknown — this\",\n \"block predates checkpoint authorship markers):_\",\n \"\",\n salvaged,\n ].join(\"\\n\");\n carriedForward = true;\n }\n }\n }\n\n const block = buildContinueBlock({ ...opts, body: effectiveBody });\n\n if (opts.dryRun) {\n return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n }\n\n const rest = stripContinue(target.content).trimStart();\n const newContent = block + (archiveEntry ? archiveInto(rest, archiveEntry) : rest);\n const tmpPath = `${target.path}.continue.tmp`;\n\n try {\n writeFileSync(tmpPath, newContent, \"utf8\");\n renameSync(tmpPath, target.path);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return {\n action: \"failed\",\n path: target.path,\n block,\n error: String(err),\n };\n }\n\n return { action: \"written\", path: target.path, block, carriedForward, archived: !!archiveEntry };\n}\n\n// ---------------------------------------------------------------------------\n// Session-note discovery\n//\n// Lifted verbatim from end.ts so pause, end and any future caller share one\n// implementation. end.ts now imports these rather than carrying its own copy.\n// ---------------------------------------------------------------------------\n\n/** PAI_DIR — mirrors pai-paths.ts resolution. */\nfunction getPaiDir(): string {\n const envDir = process.env.PAI_DIR;\n if (envDir) {\n try {\n return realpathSync(envDir);\n } catch {\n return envDir;\n }\n }\n return join(homedir(), \".claude\");\n}\n\n/**\n * Find the notes directory for a project — local first, then the central\n * ~/.claude/projects/<encoded>/Notes fallback. Never creates.\n */\nexport function findNotesDir(\n rootPath: string,\n encodedDir: string\n): string | null {\n for (const rel of [\"Notes\", \"notes\", \".claude/Notes\"]) {\n const p = join(rootPath, rel);\n if (existsSync(p)) return p;\n }\n const central = join(getPaiDir(), \"projects\", encodedDir, \"Notes\");\n if (existsSync(central)) return central;\n return null;\n}\n\n/**\n * Find the current (highest-numbered) session note: current month, then the\n * previous month, then a flat notesDir as legacy fallback.\n */\nexport function findLatestNote(notesDir: string): string | null {\n const findIn = (dir: string): string | null => {\n if (!existsSync(dir)) return null;\n let files: string[];\n try {\n files = readdirSync(dir);\n } catch {\n return null;\n }\n const notes = files\n .filter((f) => /^\\d{3,4}[\\s_-].*\\.md$/.test(f))\n .sort((a, b) => {\n const na = parseInt(a.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n const nb = parseInt(b.match(/^(\\d+)/)?.[1] ?? \"0\", 10);\n return na - nb;\n });\n return notes.length > 0 ? join(dir, notes[notes.length - 1]) : null;\n };\n\n const now = new Date();\n const year = String(now.getFullYear());\n const month = String(now.getMonth() + 1).padStart(2, \"0\");\n\n const current = findIn(join(notesDir, year, month));\n if (current) return current;\n\n const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);\n const py = String(prev.getFullYear());\n const pm = String(prev.getMonth() + 1).padStart(2, \"0\");\n const prevFound = findIn(join(notesDir, py, pm));\n if (prevFound) return prevFound;\n\n return findIn(notesDir);\n}\n\n// ---------------------------------------------------------------------------\n// Session-note append\n// ---------------------------------------------------------------------------\n\n/**\n * Append the checkpoint body to a session note.\n *\n * TODO.md's ## Continue is a single slot that every later checkpoint\n * overwrites. The session note is the durable record, so the body goes to both.\n * Idempotent per timestamp: re-running with the same stamp will not duplicate.\n */\nexport function appendCheckpointToNote(\n notePath: string,\n body: string,\n timestamp?: string\n): { appended: boolean; error?: string } {\n const ts = timestamp ?? new Date().toISOString();\n const heading = `## Pause Checkpoint — ${ts}`;\n\n let existing: string;\n try {\n existing = readFileSync(notePath, \"utf8\");\n } catch (err) {\n return { appended: false, error: String(err) };\n }\n\n if (existing.includes(heading)) return { appended: false };\n\n const block = `\\n\\n---\\n\\n${heading}\\n\\n${body.trim()}\\n`;\n const tmpPath = `${notePath}.checkpoint.tmp`;\n\n try {\n writeFileSync(tmpPath, existing.trimEnd() + block, \"utf8\");\n renameSync(tmpPath, notePath);\n } catch (err) {\n try {\n if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);\n } catch {\n /* ignore */\n }\n return { appended: false, error: String(err) };\n }\n\n return { appended: true };\n}\n\n// ---------------------------------------------------------------------------\n// Body loading\n// ---------------------------------------------------------------------------\n\n/** Read a checkpoint body from a file, or from stdin when path is \"-\". */\nexport function readBodyFile(path: string): string {\n if (path === \"-\") {\n return readFileSync(0, \"utf8\");\n }\n return readFileSync(path, \"utf8\");\n}\n"],"mappings":"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"}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "../db-
|
|
3
|
-
import "../utils-
|
|
4
|
-
import "../migrate-
|
|
5
|
-
import "../db-
|
|
6
|
-
import "../helpers-
|
|
7
|
-
import "../sync
|
|
8
|
-
import "../embeddings-
|
|
9
|
-
import "../search-
|
|
10
|
-
import "../
|
|
11
|
-
import "../checkpoint-block-
|
|
12
|
-
import "../indexer-
|
|
13
|
-
import "../ipc-client-
|
|
14
|
-
import "../config-
|
|
15
|
-
import "../factory-
|
|
16
|
-
import "../main-resolver-
|
|
17
|
-
import {
|
|
2
|
+
import "../db-Ca5qfsMC.mjs";
|
|
3
|
+
import { i as err } from "../utils-9Err2RBW.mjs";
|
|
4
|
+
import "../migrate-Cjzeefn9.mjs";
|
|
5
|
+
import "../db-a1ixZQjr.mjs";
|
|
6
|
+
import "../helpers-IjZkXBhj.mjs";
|
|
7
|
+
import "../sync-BWbe8JTg.mjs";
|
|
8
|
+
import "../embeddings-DOLZnT1X.mjs";
|
|
9
|
+
import "../search-Rpk1cSBC.mjs";
|
|
10
|
+
import { n as drainStdio, t as buildProgram } from "../program-BnMNFb4O.mjs";
|
|
11
|
+
import "../checkpoint-block-DKYxCkBL.mjs";
|
|
12
|
+
import "../indexer-D7MvSQPY.mjs";
|
|
13
|
+
import "../ipc-client-BmypMNYk.mjs";
|
|
14
|
+
import "../config-B64vFg14.mjs";
|
|
15
|
+
import "../factory-Bsp7xOpO.mjs";
|
|
16
|
+
import "../main-resolver-BAbhKpeX.mjs";
|
|
17
|
+
import { CommanderError } from "commander";
|
|
18
18
|
|
|
19
19
|
//#region src/cli/index.ts
|
|
20
20
|
/**
|
|
@@ -32,7 +32,15 @@ import { buildProgram } from "./program.mjs";
|
|
|
32
32
|
* pai end → finalize session
|
|
33
33
|
* pai help [area] → rich man page for a command area
|
|
34
34
|
*/
|
|
35
|
-
buildProgram().
|
|
35
|
+
await buildProgram().parseAsync(process.argv).catch((e) => {
|
|
36
|
+
if (e instanceof CommanderError) {
|
|
37
|
+
process.exitCode = e.exitCode;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
console.error(err(e instanceof Error ? e.message : String(e)));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
});
|
|
43
|
+
await drainStdio();
|
|
36
44
|
|
|
37
45
|
//#endregion
|
|
38
46
|
export { };
|
package/dist/cli/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Knowledge OS — CLI entry point.\n *\n * Thin entry: builds the Commander program (see ./program.ts) and parses argv.\n * All command construction lives in buildProgram() so the docs generator can\n * introspect the same tree that powers `--help`.\n *\n * Daily surface:\n * pai → deduped session listing (one row per name)\n * pai <name> → universal: switch live tab / resume / fresh\n * pai <uuid-prefix> → direct session resume via filesystem scan\n * pai pause [all] → save state (or mass-pause every live session)\n * pai end → finalize session\n * pai help [area] → rich man page for a command area\n */\n\nimport { buildProgram } from \"./program.js\";\n\
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * PAI Knowledge OS — CLI entry point.\n *\n * Thin entry: builds the Commander program (see ./program.ts) and parses argv.\n * All command construction lives in buildProgram() so the docs generator can\n * introspect the same tree that powers `--help`.\n *\n * Daily surface:\n * pai → deduped session listing (one row per name)\n * pai <name> → universal: switch live tab / resume / fresh\n * pai <uuid-prefix> → direct session resume via filesystem scan\n * pai pause [all] → save state (or mass-pause every live session)\n * pai end → finalize session\n * pai help [area] → rich man page for a command area\n */\n\nimport { CommanderError } from \"commander\";\nimport { buildProgram } from \"./program.js\";\nimport { err } from \"./utils.js\";\nimport { drainStdio } from \"./lib/exit.js\";\n\n// `parse()` does not await async command actions — Commander fires the\n// action and returns immediately, leaving its promise unawaited. When that\n// promise later rejects (e.g. a Todoist call inside `task done`/`task add`\n// throws), it becomes an unhandled rejection racing the process's natural\n// exit: depending on timing, Node either prints the trace and exits 1, or —\n// when nothing else is keeping the event loop alive — wins the race and\n// exits 0 first, silently swallowing both the error and any output the\n// action had queued. `parseAsync` makes `.parse` wait for the action, which\n// turns that race into a deterministic await: any rejection is caught here\n// and reported instead of disappearing.\nawait buildProgram()\n .parseAsync(process.argv)\n .catch((e) => {\n if (e instanceof CommanderError) {\n // program.ts's exitOverride() throws instead of calling process.exit()\n // so this catch is reached for --help, --version, and usage errors\n // too. Commander already wrote the help text or the formatted error\n // message itself (via outputHelp / configureOutput's writeErr) — just\n // adopt its exit code and let the process end naturally. A natural\n // exit flushes stdout/stderr even when either is a pipe; calling\n // process.exit() here would risk truncating output that's still\n // in-flight, same as the bug this replaces.\n process.exitCode = e.exitCode;\n return;\n }\n console.error(err(e instanceof Error ? e.message : String(e)));\n process.exitCode = 1;\n });\n\n// Belt-and-suspenders: explicitly wait for stdout/stderr to drain before this\n// module finishes and Node is free to decide the process is done. Avoiding\n// process.exit() everywhere (see lib/exit.ts) means every command path\n// relies on the event loop staying \"non-empty\" until a pending pipe write\n// completes — true by default, but not something to leave unverified when a\n// dependency's socket/timer/handle could in principle unref itself and let\n// Node consider the loop empty a tick early. This makes the wait explicit\n// and covers every command, not just the error/help paths above.\nawait drainStdio();\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,cAAc,CACjB,WAAW,QAAQ,KAAK,CACxB,OAAO,MAAM;AACZ,KAAI,aAAa,gBAAgB;AAS/B,UAAQ,WAAW,EAAE;AACrB;;AAEF,SAAQ,MAAM,IAAI,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAAC,CAAC;AAC9D,SAAQ,WAAW;EACnB;AAUJ,MAAM,YAAY"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"program.d.mts","names":[],"sources":["../../src/cli/program.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"program.d.mts","names":[],"sources":["../../src/cli/program.ts"],"mappings":";;;;;;;iBAoGgB,YAAA,CAAA,GAAgB,OAAA"}
|