@tekmidian/pai 0.14.2 → 0.16.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.
Files changed (148) hide show
  1. package/dist/{auto-route-CLYcToZ5.mjs → auto-route-CdvjEWs5.mjs} +4 -4
  2. package/dist/{auto-route-CLYcToZ5.mjs.map → auto-route-CdvjEWs5.mjs.map} +1 -1
  3. package/dist/{kg-extraction-C8DEUHTS.mjs → checkpoint-block-Bl0HD3U2.mjs} +442 -13
  4. package/dist/checkpoint-block-Bl0HD3U2.mjs.map +1 -0
  5. package/dist/cli/index.mjs +14 -12
  6. package/dist/cli/index.mjs.map +1 -1
  7. package/dist/cli/program.d.mts.map +1 -1
  8. package/dist/cli/program.mjs +31 -16
  9. package/dist/cli/program.mjs.map +1 -1
  10. package/dist/{clusters-BYPw7vfW.mjs → clusters-CKWDwcMX.mjs} +2 -2
  11. package/dist/{clusters-BYPw7vfW.mjs.map → clusters-CKWDwcMX.mjs.map} +1 -1
  12. package/dist/{config-C8m-tPhP.mjs → config-DGOfoGfm.mjs} +1 -1
  13. package/dist/config-DGOfoGfm.mjs.map +1 -0
  14. package/dist/daemon/index.mjs +18 -16
  15. package/dist/daemon/index.mjs.map +1 -1
  16. package/dist/{daemon-COSvHTNu.mjs → daemon-1rKbN-fd.mjs} +31 -30
  17. package/dist/{daemon-COSvHTNu.mjs.map → daemon-1rKbN-fd.mjs.map} +1 -1
  18. package/dist/daemon-mcp/index.mjs +24 -2
  19. package/dist/daemon-mcp/index.mjs.map +1 -1
  20. package/dist/db-BtuN768f.mjs +206 -0
  21. package/dist/db-BtuN768f.mjs.map +1 -0
  22. package/dist/{db-CmYbAVCD.mjs → db-CYmBWcjh.mjs} +1 -1
  23. package/dist/{db-CmYbAVCD.mjs.map → db-CYmBWcjh.mjs.map} +1 -1
  24. package/dist/{detect-KjycLtXM.mjs → detect-Bf2z-oKB.mjs} +1 -1
  25. package/dist/{detect-KjycLtXM.mjs.map → detect-Bf2z-oKB.mjs.map} +1 -1
  26. package/dist/{detector-ZiOhHszd.mjs → detector-rq9JGTmS.mjs} +2 -2
  27. package/dist/{detector-ZiOhHszd.mjs.map → detector-rq9JGTmS.mjs.map} +1 -1
  28. package/dist/{embeddings-BJPOcbik.mjs → embeddings-DGRAPAYb.mjs} +1 -1
  29. package/dist/{embeddings-BJPOcbik.mjs.map → embeddings-DGRAPAYb.mjs.map} +1 -1
  30. package/dist/{factory-Q88X1bAN.mjs → factory-HcQsQqh0.mjs} +8 -5
  31. package/dist/{factory-Q88X1bAN.mjs.map → factory-HcQsQqh0.mjs.map} +1 -1
  32. package/dist/{helpers-IjZkXBhj.mjs → helpers-crDEr6S2.mjs} +1 -1
  33. package/dist/{helpers-IjZkXBhj.mjs.map → helpers-crDEr6S2.mjs.map} +1 -1
  34. package/dist/hooks/capture-all-events.mjs.map +1 -1
  35. package/dist/hooks/capture-session-summary.mjs.map +1 -1
  36. package/dist/hooks/capture-tool-output.mjs.map +1 -1
  37. package/dist/hooks/cleanup-session-files.mjs.map +1 -1
  38. package/dist/hooks/context-compression-hook.mjs +304 -48
  39. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  40. package/dist/hooks/initialize-session.mjs.map +1 -1
  41. package/dist/hooks/inject-observations.mjs.map +1 -1
  42. package/dist/hooks/load-core-context.mjs.map +1 -1
  43. package/dist/hooks/load-project-context.mjs.map +1 -1
  44. package/dist/hooks/observe.mjs.map +1 -1
  45. package/dist/hooks/post-compact-inject.mjs.map +1 -1
  46. package/dist/hooks/security-validator.mjs.map +1 -1
  47. package/dist/hooks/session-commands.mjs.map +1 -1
  48. package/dist/hooks/stop-hook.mjs +299 -43
  49. package/dist/hooks/stop-hook.mjs.map +4 -4
  50. package/dist/hooks/subagent-stop-hook.mjs.map +1 -1
  51. package/dist/hooks/sync-todo-to-md.mjs.map +1 -1
  52. package/dist/hooks/update-tab-on-action.mjs.map +1 -1
  53. package/dist/hooks/update-tab-titles.mjs.map +1 -1
  54. package/dist/hooks/whisper-rules.mjs.map +1 -1
  55. package/dist/index.d.mts +0 -9
  56. package/dist/index.d.mts.map +1 -1
  57. package/dist/index.mjs +10 -9
  58. package/dist/indexer-AEcT8wHf.mjs +1 -0
  59. package/dist/{indexer-backend-Cm5RS7y0.mjs → indexer-backend-BQZXoarl.mjs} +3 -3
  60. package/dist/{indexer-backend-Cm5RS7y0.mjs.map → indexer-backend-BQZXoarl.mjs.map} +1 -1
  61. package/dist/{ipc-client-CoyUHPod.mjs → ipc-client-vO2GV325.mjs} +1 -1
  62. package/dist/{ipc-client-CoyUHPod.mjs.map → ipc-client-vO2GV325.mjs.map} +1 -1
  63. package/dist/{kg-entity-LXblD7LZ.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  64. package/dist/{kg-entity-LXblD7LZ.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  65. package/dist/{latent-ideas-RyC6eyqI.mjs → latent-ideas-C6IFFT0-.mjs} +4 -4
  66. package/dist/{latent-ideas-RyC6eyqI.mjs.map → latent-ideas-C6IFFT0-.mjs.map} +1 -1
  67. package/dist/{main-resolver-uFxNiDy7.mjs → main-resolver-DimnS2qP.mjs} +22 -22
  68. package/dist/{main-resolver-uFxNiDy7.mjs.map → main-resolver-DimnS2qP.mjs.map} +1 -1
  69. package/dist/{migrate-B02wDDgS.mjs → migrate-fLD6rAdO.mjs} +2 -2
  70. package/dist/{migrate-B02wDDgS.mjs.map → migrate-fLD6rAdO.mjs.map} +1 -1
  71. package/dist/{neighborhood-CklGIB8r.mjs → neighborhood-CzRNG2Oy.mjs} +2 -2
  72. package/dist/{neighborhood-CklGIB8r.mjs.map → neighborhood-CzRNG2Oy.mjs.map} +1 -1
  73. package/dist/{note-context-qvZlVXVO.mjs → note-context-CrfMbr4R.mjs} +1 -1
  74. package/dist/{note-context-qvZlVXVO.mjs.map → note-context-CrfMbr4R.mjs.map} +1 -1
  75. package/dist/{pai-marker-HVBwwBW-.mjs → pai-marker-B20KqhA8.mjs} +44 -2
  76. package/dist/pai-marker-B20KqhA8.mjs.map +1 -0
  77. package/dist/{pick-gne3kOJG.mjs → pick-B3zEYjin.mjs} +2316 -483
  78. package/dist/pick-B3zEYjin.mjs.map +1 -0
  79. package/dist/{postgres-B451Hnkm.mjs → postgres-zjKqPl4X.mjs} +2 -2
  80. package/dist/{postgres-B451Hnkm.mjs.map → postgres-zjKqPl4X.mjs.map} +1 -1
  81. package/dist/{query-feedback-fapbqRgh.mjs → query-feedback-BoY8_Dbb.mjs} +1 -1
  82. package/dist/{query-feedback-fapbqRgh.mjs.map → query-feedback-BoY8_Dbb.mjs.map} +1 -1
  83. package/dist/{reranker-C08R99zn.mjs → reranker-CMNZcfVx.mjs} +1 -1
  84. package/dist/{reranker-C08R99zn.mjs.map → reranker-CMNZcfVx.mjs.map} +1 -1
  85. package/dist/{search-HcdKtMla.mjs → search-i2nlQ-JM.mjs} +3 -3
  86. package/dist/{search-HcdKtMla.mjs.map → search-i2nlQ-JM.mjs.map} +1 -1
  87. package/dist/skills/End/SKILL.md +78 -0
  88. package/dist/skills/Pause/SKILL.md +77 -0
  89. package/dist/skills/Tasks/SKILL.md +22 -0
  90. package/dist/{sqlite-DQpY1Esi.mjs → sqlite-ChSCYwiC.mjs} +3 -3
  91. package/dist/{sqlite-DQpY1Esi.mjs.map → sqlite-ChSCYwiC.mjs.map} +1 -1
  92. package/dist/{state-BIlxNRUn.mjs → state-BXIdbxDs.mjs} +1 -1
  93. package/dist/{state-BIlxNRUn.mjs.map → state-BXIdbxDs.mjs.map} +1 -1
  94. package/dist/{stop-words-BwplsQ3z.mjs → stop-words-BaMEGVeY.mjs} +1 -1
  95. package/dist/{stop-words-BwplsQ3z.mjs.map → stop-words-BaMEGVeY.mjs.map} +1 -1
  96. package/dist/{sync-uPR4g438.mjs → sync-CmBKOL3K.mjs} +6 -205
  97. package/dist/sync-CmBKOL3K.mjs.map +1 -0
  98. package/dist/{themes-Cg8oG9Ra.mjs → themes-nvRM8v84.mjs} +3 -3
  99. package/dist/{themes-Cg8oG9Ra.mjs.map → themes-nvRM8v84.mjs.map} +1 -1
  100. package/dist/{tools-Op3C2Nm6.mjs → tools-CznjzfYz.mjs} +26 -26
  101. package/dist/{tools-Op3C2Nm6.mjs.map → tools-CznjzfYz.mjs.map} +1 -1
  102. package/dist/{trace-CLK-NPkb.mjs → trace-B8oz1ok0.mjs} +1 -1
  103. package/dist/{trace-CLK-NPkb.mjs.map → trace-B8oz1ok0.mjs.map} +1 -1
  104. package/dist/{utils-CqqgB0dH.mjs → utils-BAxjW3j8.mjs} +1 -1
  105. package/dist/{utils-CqqgB0dH.mjs.map → utils-BAxjW3j8.mjs.map} +1 -1
  106. package/dist/{vault-indexer-Dn_BccVK.mjs → vault-indexer-DrttVeEk.mjs} +2 -2
  107. package/dist/{vault-indexer-Dn_BccVK.mjs.map → vault-indexer-DrttVeEk.mjs.map} +1 -1
  108. package/dist/{work-queue-worker-BwouhVkb.mjs → work-queue-worker-BPXLkEik.mjs} +47 -30
  109. package/dist/work-queue-worker-BPXLkEik.mjs.map +1 -0
  110. package/dist/{zettelkasten-NwBD1NsT.mjs → zettelkasten-CTlt1dGv.mjs} +4 -4
  111. package/dist/{zettelkasten-NwBD1NsT.mjs.map → zettelkasten-CTlt1dGv.mjs.map} +1 -1
  112. package/docs/commands/README.md +17 -0
  113. package/docs/commands/backup.md +1 -1
  114. package/docs/commands/clear-names.md +1 -1
  115. package/docs/commands/daemon.md +1 -1
  116. package/docs/commands/db.md +1 -1
  117. package/docs/commands/end.md +4 -1
  118. package/docs/commands/help.md +1 -1
  119. package/docs/commands/kg.md +1 -1
  120. package/docs/commands/mcp.md +1 -1
  121. package/docs/commands/memory.md +1 -1
  122. package/docs/commands/notify.md +1 -1
  123. package/docs/commands/observation.md +1 -1
  124. package/docs/commands/obsidian.md +1 -1
  125. package/docs/commands/pause.md +4 -1
  126. package/docs/commands/project.md +1 -1
  127. package/docs/commands/projects.md +1 -1
  128. package/docs/commands/registry.md +18 -1
  129. package/docs/commands/restore.md +1 -1
  130. package/docs/commands/session.md +271 -0
  131. package/docs/commands/sessions.md +1 -1
  132. package/docs/commands/setup.md +1 -1
  133. package/docs/commands/shell-init.md +1 -1
  134. package/docs/commands/skill.md +1 -1
  135. package/docs/commands/task.md +2 -1
  136. package/docs/commands/topic.md +1 -1
  137. package/docs/commands/update.md +1 -1
  138. package/docs/commands/zettel.md +1 -1
  139. package/package.json +1 -1
  140. package/scripts/build-hooks.mjs +32 -3
  141. package/src/hooks/ts/lib/project-utils/todo.test.ts +132 -0
  142. package/src/hooks/ts/lib/project-utils/todo.ts +60 -34
  143. package/dist/config-C8m-tPhP.mjs.map +0 -1
  144. package/dist/kg-extraction-C8DEUHTS.mjs.map +0 -1
  145. package/dist/pai-marker-HVBwwBW-.mjs.map +0 -1
  146. package/dist/pick-gne3kOJG.mjs.map +0 -1
  147. package/dist/sync-uPR4g438.mjs.map +0 -1
  148. package/dist/work-queue-worker-BwouhVkb.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
- import { r as readPaiMarker } from "./pai-marker-HVBwwBW-.mjs";
2
- import { t as detectProject } from "./detect-KjycLtXM.mjs";
1
+ import { r as readPaiMarker } from "./pai-marker-B20KqhA8.mjs";
2
+ import { t as detectProject } from "./detect-Bf2z-oKB.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-ZiOhHszd.mjs").then((n) => n.n);
29
+ const { detectTopicShift } = await import("./detector-rq9JGTmS.mjs").then((n) => n.n);
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-CLYcToZ5.mjs.map
86
+ //# sourceMappingURL=auto-route-CdvjEWs5.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"auto-route-CLYcToZ5.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
+ {"version":3,"file":"auto-route-CdvjEWs5.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,8 +1,8 @@
1
- import { _ as warn, c as ok, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-CqqgB0dH.mjs";
2
- import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-B02wDDgS.mjs";
3
- import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-HVBwwBW-.mjs";
4
- import { i as kgAdd, o as kgInvalidate, r as upsertKgEntity, s as kgQuery } from "./kg-entity-LXblD7LZ.mjs";
5
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
1
+ import { _ as warn, c as ok, i as err, n as dim, r as encodeDir, s as now, t as bold } from "./utils-BAxjW3j8.mjs";
2
+ import { a as slugify, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "./migrate-fLD6rAdO.mjs";
3
+ import { n as ensurePaiMarker, t as discoverPaiMarkers } from "./pai-marker-B20KqhA8.mjs";
4
+ import { i as kgAdd, o as kgInvalidate, r as upsertKgEntity, s as kgQuery } from "./kg-entity-DbOMPdF9.mjs";
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";
8
8
 
@@ -119,6 +119,23 @@ function saveScanConfig(config) {
119
119
  mkdirSync(PAI_CONFIG_DIR, { recursive: true });
120
120
  writeFileSync(PAI_CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf8");
121
121
  }
122
+ /**
123
+ * Resolve a path to its canonical form, falling back to the input.
124
+ *
125
+ * Every path that becomes a `projects.root_path` must go through this. That
126
+ * column is UNIQUE, so two spellings of one directory create two projects and
127
+ * split session history between them. The spellings are not hypothetical: a
128
+ * configured scan_dir of `~/dev/ai` (where `~/dev` is a symlink) makes every
129
+ * project under it register under the symlinked path, while a session started
130
+ * from the resolved path registers a rival row.
131
+ */
132
+ function canonicalPath(p) {
133
+ try {
134
+ return realpathSync(p);
135
+ } catch {
136
+ return p;
137
+ }
138
+ }
122
139
  function resolveHome(p) {
123
140
  if (p.startsWith("~/")) return join(homedir(), p.slice(2));
124
141
  return resolve(p);
@@ -166,6 +183,7 @@ function performScan(db) {
166
183
  result.projectsScanned++;
167
184
  continue;
168
185
  }
186
+ rootPath = canonicalPath(rootPath);
169
187
  const slug = slugify(basename(rootPath) || encodedDir);
170
188
  const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);
171
189
  result.projectsScanned++;
@@ -223,7 +241,7 @@ function performScan(db) {
223
241
  }
224
242
  });
225
243
  for (const child of children) {
226
- const childPath = join(scanDir, child);
244
+ const childPath = canonicalPath(join(scanDir, child));
227
245
  const childSlug = slugify(child);
228
246
  const childEncoded = encodeDir(childPath);
229
247
  const existing = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(childPath);
@@ -269,15 +287,16 @@ function performScan(db) {
269
287
  for (const marker of markers) {
270
288
  const registeredRow = db.prepare("SELECT id, root_path, slug FROM projects WHERE slug = ?").get(marker.slug);
271
289
  if (!registeredRow) continue;
272
- if (registeredRow.root_path !== marker.projectRoot) {
273
- const newEncoded = encodeDir(marker.projectRoot);
290
+ const markerRoot = canonicalPath(marker.projectRoot);
291
+ if (registeredRow.root_path !== markerRoot) {
292
+ const newEncoded = encodeDir(markerRoot);
274
293
  const now4 = Date.now();
275
294
  const encodedOwner = db.prepare("SELECT id FROM projects WHERE encoded_dir = ?").get(newEncoded);
276
- const pathOwner = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(marker.projectRoot);
295
+ const pathOwner = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(markerRoot);
277
296
  const encodedSafe = !encodedOwner || encodedOwner.id === registeredRow.id;
278
297
  const pathSafe = !pathOwner || pathOwner.id === registeredRow.id;
279
- if (encodedSafe && pathSafe) db.prepare("UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?").run(marker.projectRoot, newEncoded, now4, registeredRow.id);
280
- else if (pathSafe) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(marker.projectRoot, now4, registeredRow.id);
298
+ if (encodedSafe && pathSafe) db.prepare("UPDATE projects SET root_path = ?, encoded_dir = ?, updated_at = ? WHERE id = ?").run(markerRoot, newEncoded, now4, registeredRow.id);
299
+ else if (pathSafe) db.prepare("UPDATE projects SET root_path = ?, updated_at = ? WHERE id = ?").run(markerRoot, now4, registeredRow.id);
281
300
  }
282
301
  }
283
302
  }
@@ -565,5 +584,415 @@ async function extractAndStoreTriples(pool, params) {
565
584
  }
566
585
 
567
586
  //#endregion
568
- export { resolveHome as a, upsertSession as c, performScan as i, cmdScan as n, saveScanConfig as o, loadScanConfig as r, upsertProject as s, extractAndStoreTriples as t };
569
- //# sourceMappingURL=kg-extraction-C8DEUHTS.mjs.map
587
+ //#region src/session/checkpoint-block.ts
588
+ /**
589
+ * Shared "## Continue" checkpoint logic for a project's TODO.md.
590
+ *
591
+ * WHY THIS MODULE EXISTS
592
+ * ----------------------
593
+ * Before this, `pause.ts` and `handover.ts` each carried their own copy of
594
+ * findProjectTodo / stripContinueSection / block-builder. Both wrote the same
595
+ * fixed four-line block, and both stripped any existing ## Continue section
596
+ * unconditionally. The consequence was that a rich, model-authored checkpoint
597
+ * could never survive:
598
+ *
599
+ * 1. `pai pause` had no way to accept a body, so the model printed its
600
+ * checkpoint to the terminal and it was lost.
601
+ * 2. Even if a body had been written by hand, the session-stop hook runs
602
+ * `pai session handover` on every clean exit, which regenerated the
603
+ * generic block and erased it.
604
+ *
605
+ * So there are two jobs here:
606
+ *
607
+ * - AUTHORED writes (`pai pause --body-file`) carry the model's markdown
608
+ * verbatim, wrapped in explicit start/end markers.
609
+ * - AUTO writes (hooks) must never destroy an authored checkpoint belonging
610
+ * to the *same* session. They may replace a stale one left by an earlier
611
+ * session, otherwise TODO.md would show a checkpoint that no longer
612
+ * describes where the work stands.
613
+ *
614
+ * PARSING
615
+ * -------
616
+ * A rich body can legitimately contain `---` rules and `##` headings, which the
617
+ * old heuristic scanner treated as section terminators. Authored blocks are
618
+ * therefore delimited by an explicit HTML-comment pair; the legacy heuristic is
619
+ * kept only as a fallback for blocks written before this change.
620
+ */
621
+ /** Locations searched for a project TODO.md, in priority order. */
622
+ const TODO_LOCATIONS = [
623
+ "Notes/TODO.md",
624
+ ".claude/Notes/TODO.md",
625
+ "tasks/todo.md",
626
+ "TODO.md"
627
+ ];
628
+ const MARKER_OPEN = "<!-- pai:checkpoint";
629
+ const MARKER_CLOSE = "<!-- /pai:checkpoint -->";
630
+ const CONTINUE_HEADING = "## Continue";
631
+ function findProjectTodo(rootPath) {
632
+ for (const rel of TODO_LOCATIONS) {
633
+ const full = join(rootPath, rel);
634
+ if (existsSync(full)) try {
635
+ return {
636
+ path: full,
637
+ content: readFileSync(full, "utf8")
638
+ };
639
+ } catch {}
640
+ }
641
+ return null;
642
+ }
643
+ /**
644
+ * Resolve the TODO.md to write to, creating Notes/ if nothing exists yet.
645
+ * Returns null only when the directory could not be created.
646
+ */
647
+ function resolveTodoTarget(rootPath, opts = {}) {
648
+ const found = findProjectTodo(rootPath);
649
+ if (found) return found;
650
+ const notesDir = join(rootPath, "Notes");
651
+ if (opts.create !== false) try {
652
+ if (!existsSync(notesDir)) mkdirSync(notesDir, { recursive: true });
653
+ } catch {
654
+ return null;
655
+ }
656
+ return {
657
+ path: join(notesDir, "TODO.md"),
658
+ content: ""
659
+ };
660
+ }
661
+ /**
662
+ * Parse a `<!-- pai:checkpoint key="value" ... -->` marker line.
663
+ * Returns null when the line is not a marker.
664
+ */
665
+ function parseMarker(line) {
666
+ const trimmed = line.trim();
667
+ if (!trimmed.startsWith(MARKER_OPEN)) return null;
668
+ const attrs = {};
669
+ for (const m of trimmed.matchAll(/([a-zA-Z][\w-]*)="([^"]*)"/g)) attrs[m[1]] = m[2];
670
+ return {
671
+ authored: attrs.authored === "model" ? "model" : "auto",
672
+ session: attrs.session || void 0,
673
+ sessionId: attrs["session-id"] || void 0,
674
+ ts: attrs.ts || void 0
675
+ };
676
+ }
677
+ /**
678
+ * Lines an auto-generated block is made of. Anything else in a section is
679
+ * content somebody put there deliberately.
680
+ */
681
+ const BOILERPLATE_PATTERNS = [
682
+ /^##\s+Continue$/,
683
+ /^<!--\s*\/?pai:checkpoint/,
684
+ /^>\s*\*\*Last session:\*\*/,
685
+ /^>\s*\*\*Paused at:\*\*/,
686
+ /^>\s*Working directory:/,
687
+ /^>\s*Resume with:/,
688
+ /^>\s*_No checkpoint body was recorded/,
689
+ /^>?\s*$/,
690
+ /^-{3,}$/
691
+ ];
692
+ /**
693
+ * True when a section contains nothing but generated header lines.
694
+ *
695
+ * This is the guard that stops an auto write from destroying content it did
696
+ * not author. A session that hit the old clobbering bug may have worked around
697
+ * it by hand — writing its state into a subsection underneath the generated
698
+ * header lines, in an unmarked block. Those blocks predate the marker, so
699
+ * authorship cannot be read off them; the only safe signal is whether anything
700
+ * beyond boilerplate is present.
701
+ */
702
+ function isBoilerplateOnly(lines) {
703
+ return lines.every((line) => {
704
+ const t = line.trim();
705
+ return BOILERPLATE_PATTERNS.some((re) => re.test(t));
706
+ });
707
+ }
708
+ /** Extract the non-boilerplate content of a section, or "" if there is none. */
709
+ function extractSectionContent(lines) {
710
+ const kept = [];
711
+ for (const line of lines) {
712
+ const t = line.trim();
713
+ if (BOILERPLATE_PATTERNS.some((re) => re.test(t))) continue;
714
+ kept.push(line);
715
+ }
716
+ return kept.join("\n").trim();
717
+ }
718
+ /**
719
+ * Locate the existing ## Continue section.
720
+ *
721
+ * When the section carries an explicit marker pair, the close marker defines
722
+ * the end — this is what lets a rich body contain `---` and `##` safely.
723
+ * Otherwise the legacy heuristic applies: stop at the first `---` or the next
724
+ * `##` heading.
725
+ */
726
+ function locateContinue(content) {
727
+ const lines = content.split("\n");
728
+ const startIdx = lines.findIndex((l) => l.trim() === CONTINUE_HEADING);
729
+ if (startIdx === -1) return null;
730
+ let meta = null;
731
+ let markerIdx = -1;
732
+ for (let i = startIdx + 1; i < Math.min(startIdx + 6, lines.length); i++) {
733
+ const parsed = parseMarker(lines[i]);
734
+ if (parsed) {
735
+ meta = parsed;
736
+ markerIdx = i;
737
+ break;
738
+ }
739
+ if (lines[i].trim() !== "") break;
740
+ }
741
+ let endIdx = lines.length;
742
+ if (markerIdx !== -1) {
743
+ const closeIdx = lines.findIndex((l, i) => i > markerIdx && l.trim() === MARKER_CLOSE);
744
+ if (closeIdx !== -1) endIdx = closeIdx + 1;
745
+ else endIdx = heuristicEnd(lines, startIdx);
746
+ } else endIdx = heuristicEnd(lines, startIdx);
747
+ let trailingEnd = endIdx;
748
+ while (trailingEnd < lines.length && lines[trailingEnd].trim() === "") trailingEnd += 1;
749
+ if (trailingEnd < lines.length && lines[trailingEnd].trim() === "---") trailingEnd += 1;
750
+ else trailingEnd = endIdx;
751
+ return {
752
+ startIdx,
753
+ endIdx: trailingEnd,
754
+ meta,
755
+ lines: lines.slice(startIdx, trailingEnd)
756
+ };
757
+ }
758
+ /**
759
+ * Legacy scanner for blocks written before checkpoint markers existed.
760
+ *
761
+ * Terminates on a horizontal rule or the next level-2 heading. Note the
762
+ * `(?!#)` — `###` is a *subsection* of `## Continue`, not a terminator. The
763
+ * original scanner stopped at any run of `#`, which meant a `### Restored
764
+ * state` subsection fell outside the section entirely and could not be seen,
765
+ * let alone carried forward.
766
+ */
767
+ function heuristicEnd(lines, startIdx) {
768
+ for (let i = startIdx + 1; i < lines.length; i++) {
769
+ const trimmed = lines[i].trim();
770
+ if (trimmed === "---" || /^##(?!#)/.test(trimmed) && trimmed !== CONTINUE_HEADING) return i;
771
+ }
772
+ return lines.length;
773
+ }
774
+ /** Remove the ## Continue section, returning the remainder of the document. */
775
+ function stripContinue(content) {
776
+ const found = locateContinue(content);
777
+ if (!found) return content;
778
+ const lines = content.split("\n");
779
+ const before = lines.slice(0, found.startIdx);
780
+ const after = lines.slice(found.endIdx);
781
+ while (after.length > 0 && after[0].trim() === "") after.shift();
782
+ return [...before, ...after].join("\n");
783
+ }
784
+ function escapeAttr(value) {
785
+ return value.replace(/"/g, "'");
786
+ }
787
+ function buildContinueBlock(opts) {
788
+ const ts = opts.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
789
+ const attrs = [
790
+ `authored="${opts.authored}"`,
791
+ `session="${escapeAttr(opts.sessionLine)}"`,
792
+ opts.sessionId ? `session-id="${escapeAttr(opts.sessionId)}"` : null,
793
+ `ts="${ts}"`
794
+ ].filter(Boolean).join(" ");
795
+ const header = [
796
+ `> **Last session:** ${opts.sessionLine}`,
797
+ `> **Paused at:** ${ts}`,
798
+ ">",
799
+ `> Working directory: ${opts.cwd}`
800
+ ];
801
+ if (opts.sessionId) header.push(">", `> Resume with: \`claude --resume ${opts.sessionId}\``);
802
+ const body = (opts.body ?? "").trim();
803
+ const parts = [
804
+ CONTINUE_HEADING,
805
+ "",
806
+ `${MARKER_OPEN} ${attrs} -->`,
807
+ "",
808
+ ...header
809
+ ];
810
+ if (body) parts.push("", body);
811
+ else parts.push(">", "> _No checkpoint body was recorded — see the latest session note._");
812
+ parts.push("", MARKER_CLOSE, "", "---", "");
813
+ return parts.join("\n");
814
+ }
815
+ /**
816
+ * Write the ## Continue block, honouring the preservation rules.
817
+ *
818
+ * An AUTO write is unattended — it fires from the session-stop and pre-compact
819
+ * hooks — so it operates under one governing rule: **never destroy content it
820
+ * did not author.** Three cases follow from that:
821
+ *
822
+ * 1. An authored checkpoint for the SAME session is left untouched. The hooks
823
+ * fire after the model has already recorded the real state; overwriting it
824
+ * with metadata is the bug this module exists to fix.
825
+ * 2. An authored checkpoint from an EARLIER session is stale — TODO.md would
826
+ * otherwise keep pointing at the wrong session — so it is replaced. Its
827
+ * content is not lost: `pai pause` mirrors every authored body into the
828
+ * session note.
829
+ * 3. An UNMARKED block predates the marker, so authorship cannot be read off
830
+ * it. If it is nothing but generated header lines it is replaced. If it
831
+ * carries anything else, that content was put there deliberately — quite
832
+ * possibly as a hand-rolled workaround for the very clobbering this fixes —
833
+ * and is carried forward into the new block rather than dropped.
834
+ *
835
+ * A MODEL write always replaces: the model is authoring the checkpoint, and a
836
+ * newer one supersedes an older one.
837
+ */
838
+ function applyContinue(opts) {
839
+ const target = resolveTodoTarget(opts.rootPath, { create: !opts.dryRun });
840
+ if (!target) return {
841
+ action: "failed",
842
+ path: null,
843
+ block: buildContinueBlock(opts),
844
+ error: "Could not resolve or create a TODO.md target"
845
+ };
846
+ const existing = locateContinue(target.content);
847
+ let carriedForward = false;
848
+ let effectiveBody = opts.body;
849
+ if (opts.authored === "auto" && existing) {
850
+ if (existing.meta?.authored === "model" && existing.meta.session === opts.sessionLine) return {
851
+ action: "preserved",
852
+ path: target.path,
853
+ block: buildContinueBlock(opts),
854
+ preservedMeta: existing.meta
855
+ };
856
+ if (!existing.meta && !isBoilerplateOnly(existing.lines)) {
857
+ const salvaged = extractSectionContent(existing.lines);
858
+ if (salvaged) {
859
+ effectiveBody = [
860
+ "_Carried forward from the previous checkpoint (author unknown — this",
861
+ "block predates checkpoint authorship markers):_",
862
+ "",
863
+ salvaged
864
+ ].join("\n");
865
+ carriedForward = true;
866
+ }
867
+ }
868
+ }
869
+ const block = buildContinueBlock({
870
+ ...opts,
871
+ body: effectiveBody
872
+ });
873
+ if (opts.dryRun) return {
874
+ action: "written",
875
+ path: target.path,
876
+ block,
877
+ carriedForward
878
+ };
879
+ const newContent = block + stripContinue(target.content).trimStart();
880
+ const tmpPath = `${target.path}.continue.tmp`;
881
+ try {
882
+ writeFileSync(tmpPath, newContent, "utf8");
883
+ renameSync(tmpPath, target.path);
884
+ } catch (err) {
885
+ try {
886
+ if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);
887
+ } catch {}
888
+ return {
889
+ action: "failed",
890
+ path: target.path,
891
+ block,
892
+ error: String(err)
893
+ };
894
+ }
895
+ return {
896
+ action: "written",
897
+ path: target.path,
898
+ block,
899
+ carriedForward
900
+ };
901
+ }
902
+ /** PAI_DIR — mirrors pai-paths.ts resolution. */
903
+ function getPaiDir() {
904
+ const envDir = process.env.PAI_DIR;
905
+ if (envDir) try {
906
+ return realpathSync(envDir);
907
+ } catch {
908
+ return envDir;
909
+ }
910
+ return join(homedir(), ".claude");
911
+ }
912
+ /**
913
+ * Find the notes directory for a project — local first, then the central
914
+ * ~/.claude/projects/<encoded>/Notes fallback. Never creates.
915
+ */
916
+ function findNotesDir(rootPath, encodedDir) {
917
+ for (const rel of [
918
+ "Notes",
919
+ "notes",
920
+ ".claude/Notes"
921
+ ]) {
922
+ const p = join(rootPath, rel);
923
+ if (existsSync(p)) return p;
924
+ }
925
+ const central = join(getPaiDir(), "projects", encodedDir, "Notes");
926
+ if (existsSync(central)) return central;
927
+ return null;
928
+ }
929
+ /**
930
+ * Find the current (highest-numbered) session note: current month, then the
931
+ * previous month, then a flat notesDir as legacy fallback.
932
+ */
933
+ function findLatestNote(notesDir) {
934
+ const findIn = (dir) => {
935
+ if (!existsSync(dir)) return null;
936
+ let files;
937
+ try {
938
+ files = readdirSync(dir);
939
+ } catch {
940
+ return null;
941
+ }
942
+ const notes = files.filter((f) => /^\d{3,4}[\s_-].*\.md$/.test(f)).sort((a, b) => {
943
+ return parseInt(a.match(/^(\d+)/)?.[1] ?? "0", 10) - parseInt(b.match(/^(\d+)/)?.[1] ?? "0", 10);
944
+ });
945
+ return notes.length > 0 ? join(dir, notes[notes.length - 1]) : null;
946
+ };
947
+ const now = /* @__PURE__ */ new Date();
948
+ const current = findIn(join(notesDir, String(now.getFullYear()), String(now.getMonth() + 1).padStart(2, "0")));
949
+ if (current) return current;
950
+ const prev = new Date(now.getFullYear(), now.getMonth() - 1, 1);
951
+ const prevFound = findIn(join(notesDir, String(prev.getFullYear()), String(prev.getMonth() + 1).padStart(2, "0")));
952
+ if (prevFound) return prevFound;
953
+ return findIn(notesDir);
954
+ }
955
+ /**
956
+ * Append the checkpoint body to a session note.
957
+ *
958
+ * TODO.md's ## Continue is a single slot that every later checkpoint
959
+ * overwrites. The session note is the durable record, so the body goes to both.
960
+ * Idempotent per timestamp: re-running with the same stamp will not duplicate.
961
+ */
962
+ function appendCheckpointToNote(notePath, body, timestamp) {
963
+ const heading = `## Pause Checkpoint — ${timestamp ?? (/* @__PURE__ */ new Date()).toISOString()}`;
964
+ let existing;
965
+ try {
966
+ existing = readFileSync(notePath, "utf8");
967
+ } catch (err) {
968
+ return {
969
+ appended: false,
970
+ error: String(err)
971
+ };
972
+ }
973
+ if (existing.includes(heading)) return { appended: false };
974
+ const block = `\n\n---\n\n${heading}\n\n${body.trim()}\n`;
975
+ const tmpPath = `${notePath}.checkpoint.tmp`;
976
+ try {
977
+ writeFileSync(tmpPath, existing.trimEnd() + block, "utf8");
978
+ renameSync(tmpPath, notePath);
979
+ } catch (err) {
980
+ try {
981
+ if (existsSync(tmpPath)) renameSync(tmpPath, `${tmpPath}.dead`);
982
+ } catch {}
983
+ return {
984
+ appended: false,
985
+ error: String(err)
986
+ };
987
+ }
988
+ return { appended: true };
989
+ }
990
+ /** Read a checkpoint body from a file, or from stdin when path is "-". */
991
+ function readBodyFile(path) {
992
+ if (path === "-") return readFileSync(0, "utf8");
993
+ return readFileSync(path, "utf8");
994
+ }
995
+
996
+ //#endregion
997
+ export { readBodyFile as a, loadScanConfig as c, saveScanConfig as d, upsertProject as f, findNotesDir as i, performScan as l, applyContinue as n, extractAndStoreTriples as o, upsertSession as p, findLatestNote as r, cmdScan as s, appendCheckpointToNote as t, resolveHome as u };
998
+ //# sourceMappingURL=checkpoint-block-Bl0HD3U2.mjs.map