@tekmidian/pai 0.8.5 → 0.9.1
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/ARCHITECTURE.md +121 -0
- package/FEATURE.md +5 -0
- package/README.md +54 -0
- package/dist/{auto-route-C-DrW6BL.mjs → auto-route-CruBrTf-.mjs} +2 -2
- package/dist/{auto-route-C-DrW6BL.mjs.map → auto-route-CruBrTf-.mjs.map} +1 -1
- package/dist/cli/index.mjs +345 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{clusters-JIDQW65f.mjs → clusters-CRlPBpq8.mjs} +1 -1
- package/dist/{clusters-JIDQW65f.mjs.map → clusters-CRlPBpq8.mjs.map} +1 -1
- package/dist/daemon/index.mjs +6 -6
- package/dist/{daemon-BaYX-w_d.mjs → daemon-kp49BE7u.mjs} +93 -19
- package/dist/daemon-kp49BE7u.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +51 -0
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{detector-jGBuYQJM.mjs → detector-CNU3zCwP.mjs} +1 -1
- package/dist/{detector-jGBuYQJM.mjs.map → detector-CNU3zCwP.mjs.map} +1 -1
- package/dist/{factory-BzWfxsvK.mjs → factory-DKDPRhAN.mjs} +3 -3
- package/dist/{factory-BzWfxsvK.mjs.map → factory-DKDPRhAN.mjs.map} +1 -1
- package/dist/hooks/load-project-context.mjs +276 -89
- package/dist/hooks/load-project-context.mjs.map +4 -4
- package/dist/hooks/stop-hook.mjs +152 -2
- package/dist/hooks/stop-hook.mjs.map +3 -3
- package/dist/{indexer-backend-jcJFsmB4.mjs → indexer-backend-CIIlrYh6.mjs} +1 -1
- package/dist/{indexer-backend-jcJFsmB4.mjs.map → indexer-backend-CIIlrYh6.mjs.map} +1 -1
- package/dist/kg-B5ysyRLC.mjs +94 -0
- package/dist/kg-B5ysyRLC.mjs.map +1 -0
- package/dist/kg-extraction-BlGM40q7.mjs +211 -0
- package/dist/kg-extraction-BlGM40q7.mjs.map +1 -0
- package/dist/{latent-ideas-bTJo6Omd.mjs → latent-ideas-DvWBRHsy.mjs} +2 -2
- package/dist/{latent-ideas-bTJo6Omd.mjs.map → latent-ideas-DvWBRHsy.mjs.map} +1 -1
- package/dist/{neighborhood-BYYbEkUJ.mjs → neighborhood-u8ytjmWq.mjs} +1 -1
- package/dist/{neighborhood-BYYbEkUJ.mjs.map → neighborhood-u8ytjmWq.mjs.map} +1 -1
- package/dist/{note-context-BK24bX8Y.mjs → note-context-CG2_e-0W.mjs} +1 -1
- package/dist/{note-context-BK24bX8Y.mjs.map → note-context-CG2_e-0W.mjs.map} +1 -1
- package/dist/{postgres-DbUXNuy_.mjs → postgres-BGERehmX.mjs} +22 -1
- package/dist/{postgres-DbUXNuy_.mjs.map → postgres-BGERehmX.mjs.map} +1 -1
- package/dist/{query-feedback-Dv43XKHM.mjs → query-feedback-CQSumXDy.mjs} +1 -1
- package/dist/{query-feedback-Dv43XKHM.mjs.map → query-feedback-CQSumXDy.mjs.map} +1 -1
- package/dist/skills/Reconstruct/SKILL.md +36 -0
- package/dist/{sqlite-l-s9xPjY.mjs → sqlite-BJrME_vg.mjs} +1 -1
- package/dist/{sqlite-l-s9xPjY.mjs.map → sqlite-BJrME_vg.mjs.map} +1 -1
- package/dist/{state-C6_vqz7w.mjs → state-BIlxNRUn.mjs} +1 -1
- package/dist/{state-C6_vqz7w.mjs.map → state-BIlxNRUn.mjs.map} +1 -1
- package/dist/{themes-BvYF0W8T.mjs → themes-9jxFn3Rf.mjs} +1 -1
- package/dist/{themes-BvYF0W8T.mjs.map → themes-9jxFn3Rf.mjs.map} +1 -1
- package/dist/{tools-BXSwlzeH.mjs → tools-8t7BQrm9.mjs} +717 -15
- package/dist/tools-8t7BQrm9.mjs.map +1 -0
- package/dist/{trace-CRx9lPuc.mjs → trace-C2XrzssW.mjs} +1 -1
- package/dist/{trace-CRx9lPuc.mjs.map → trace-C2XrzssW.mjs.map} +1 -1
- package/dist/{vault-indexer-B-aJpRZC.mjs → vault-indexer-TTCl1QOL.mjs} +1 -1
- package/dist/{vault-indexer-B-aJpRZC.mjs.map → vault-indexer-TTCl1QOL.mjs.map} +1 -1
- package/dist/{zettelkasten-DhBKZQHF.mjs → zettelkasten-BdaMzTGQ.mjs} +3 -3
- package/dist/{zettelkasten-DhBKZQHF.mjs.map → zettelkasten-BdaMzTGQ.mjs.map} +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/session-start/load-project-context.ts +36 -0
- package/src/hooks/ts/stop/stop-hook.ts +203 -1
- package/dist/daemon-BaYX-w_d.mjs.map +0 -1
- package/dist/indexer-D53l5d1U.mjs +0 -1
- package/dist/tools-BXSwlzeH.mjs.map +0 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -250,6 +250,127 @@ Restart Claude Code after installation for the tools to appear.
|
|
|
250
250
|
|
|
251
251
|
---
|
|
252
252
|
|
|
253
|
+
## Memory Intelligence Layer
|
|
254
|
+
|
|
255
|
+
These features extend the core memory engine with structured, time-aware, and cross-project intelligence. They ship in PAI v0.8.6, inspired by patterns from [mempalace](https://github.com/milla-jovovich/mempalace).
|
|
256
|
+
|
|
257
|
+
### 4-Layer Wake-Up Context
|
|
258
|
+
|
|
259
|
+
The `memory_wakeup` MCP tool loads context progressively at session start. The goal is a meaningful cold-start without flooding the context window.
|
|
260
|
+
|
|
261
|
+
| Layer | Source | Content |
|
|
262
|
+
|-------|--------|---------|
|
|
263
|
+
| L0 | `~/.pai/identity.txt` | Stable identity: who you are, your style, your key projects |
|
|
264
|
+
| L1 | Recent session notes (registry + filesystem) | Essential story: what you were doing, decisions made, where things stand |
|
|
265
|
+
| L2 | On-demand `memory_search` query | Topic-specific context fetched when needed |
|
|
266
|
+
| L3 | Full `memory_search` with reranking | Deep retrieval for complex questions |
|
|
267
|
+
|
|
268
|
+
L0 and L1 are injected automatically via the `SessionStart` hook. L2 and L3 are triggered by the model during the session as needed.
|
|
269
|
+
|
|
270
|
+
**File locations:**
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
~/.pai/identity.txt # L0 identity — edit this to describe yourself
|
|
274
|
+
Notes/ # L1 source — the most recent session notes are read here
|
|
275
|
+
~/.config/pai/config.json # wakeupL1Count: N controls how many notes are read for L1
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Temporal Knowledge Graph
|
|
279
|
+
|
|
280
|
+
The `kg_triples` table stores knowledge as typed, time-bounded triples. This allows facts to evolve over time rather than accumulating as a flat, undated collection.
|
|
281
|
+
|
|
282
|
+
**Schema:**
|
|
283
|
+
|
|
284
|
+
```sql
|
|
285
|
+
CREATE TABLE kg_triples (
|
|
286
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
287
|
+
tenant_id UUID NOT NULL,
|
|
288
|
+
subject TEXT NOT NULL,
|
|
289
|
+
predicate TEXT NOT NULL,
|
|
290
|
+
object TEXT NOT NULL,
|
|
291
|
+
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
292
|
+
valid_to TIMESTAMPTZ, -- NULL means currently valid
|
|
293
|
+
confidence REAL DEFAULT 1.0,
|
|
294
|
+
source TEXT, -- where this fact came from
|
|
295
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
CREATE INDEX kg_triples_subject_idx ON kg_triples (tenant_id, subject);
|
|
299
|
+
CREATE INDEX kg_triples_valid_idx ON kg_triples (tenant_id, valid_from, valid_to);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**MCP tools:**
|
|
303
|
+
|
|
304
|
+
| Tool | Description |
|
|
305
|
+
|------|-------------|
|
|
306
|
+
| `kg_add` | Add a subject-predicate-object triple with `valid_from` (defaults to now) and optional `valid_to` |
|
|
307
|
+
| `kg_query` | Query triples by subject, predicate, or object; filter by point-in-time (defaults to now) |
|
|
308
|
+
| `kg_invalidate` | Set `valid_to = now()` on a triple, marking it as no longer true |
|
|
309
|
+
| `kg_contradictions` | Surface triples that share a subject and predicate but have conflicting objects within overlapping validity windows |
|
|
310
|
+
|
|
311
|
+
Contradiction detection uses predicate inversion rules (`uses → not_uses`, `works_at → not_works_at`, etc.) to identify direct contradictions. Fuzzy contradictions (same subject/predicate, different object without an explicit inversion rule) are flagged with lower confidence.
|
|
312
|
+
|
|
313
|
+
### Memory Taxonomy
|
|
314
|
+
|
|
315
|
+
`memory_taxonomy` returns a structured overview of the current state of indexed memory:
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"projects": [
|
|
320
|
+
{ "slug": "pai", "sessions": 142, "chunks": 89341, "last_active": "2026-04-07" }
|
|
321
|
+
],
|
|
322
|
+
"totals": {
|
|
323
|
+
"projects": 77,
|
|
324
|
+
"sessions": 1204,
|
|
325
|
+
"chunks": 449000,
|
|
326
|
+
"embeddings_coverage": 0.94
|
|
327
|
+
},
|
|
328
|
+
"recent_activity": [
|
|
329
|
+
{ "project": "pai", "session": "0142", "date": "2026-04-07", "topic": "Memory Intelligence Layer" }
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This is used both as a user-facing status tool and as a model-facing context signal — the model can call `memory_taxonomy` to understand what is indexed before deciding how to search.
|
|
335
|
+
|
|
336
|
+
### Mid-Session Auto-Save
|
|
337
|
+
|
|
338
|
+
The Stop hook normally fires once at the end of a session. The auto-save feature extends this: the Stop hook also fires every N human messages during a session, pushing a `session-summary` work item to the daemon and blocking the Stop event so the session continues.
|
|
339
|
+
|
|
340
|
+
**Configuration:**
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
PAI_AUTO_SAVE_INTERVAL=15 # default: 15 human messages between saves
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Set in `~/.config/pai/config.json` as `autoSaveInterval` or via the environment variable. Set to `0` to disable mid-session saves.
|
|
347
|
+
|
|
348
|
+
**Loop prevention:** The hook sets a `stop_hook_active` flag in the work item. The daemon checks this flag before re-queuing save work. This prevents an auto-save from triggering another auto-save in a feedback loop.
|
|
349
|
+
|
|
350
|
+
### Cross-Project Tunnel Detection
|
|
351
|
+
|
|
352
|
+
`memory_tunnels` detects concepts that appear across multiple projects by comparing the vocabulary of each project's indexed content.
|
|
353
|
+
|
|
354
|
+
**SQLite mode:** Uses the FTS5 vocabulary virtual table to extract term frequencies per project, then identifies terms that appear significantly in three or more projects.
|
|
355
|
+
|
|
356
|
+
**PostgreSQL mode:** Uses `ts_stat()` on each project's `tsvector` content to extract term frequencies, then compares across projects using the same multi-project threshold.
|
|
357
|
+
|
|
358
|
+
**Output:** A ranked list of concept-tunnel pairs:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
[
|
|
362
|
+
{
|
|
363
|
+
"concept": "rate limiting",
|
|
364
|
+
"projects": ["pai-daemon", "whazaa-mcp", "ringsaday-backend"],
|
|
365
|
+
"tunnel_strength": 0.87
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Tunnel strength is a normalized score based on term frequency across projects, IDF weighting, and the number of projects involved. Higher is more significant.
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
253
374
|
## Search Modes
|
|
254
375
|
|
|
255
376
|
Three modes, selectable via the `--mode` flag on the CLI or the `mode` parameter in MCP tool calls.
|
package/FEATURE.md
CHANGED
|
@@ -40,6 +40,11 @@ different direction: persistent memory, session continuity, and deep Claude Code
|
|
|
40
40
|
| **Automatic session notes** | No | Yes — AI-generated via daemon worker (Opus/Sonnet), topic-based splitting |
|
|
41
41
|
| **Topic-based note splitting** | No | Yes — Jaccard similarity detects topic shifts, creates separate notes |
|
|
42
42
|
| **Whisper rules** | No | Yes — injects critical rules on every prompt, survives compaction and /clear |
|
|
43
|
+
| **4-layer wake-up context** | No | Yes — `memory_wakeup` tool loads identity (L0), recent story (L1), on-demand topic (L2), deep search (L3) |
|
|
44
|
+
| **Temporal knowledge graph** | No | Yes — `kg_add`, `kg_query`, `kg_invalidate`, `kg_contradictions` tools; facts have `valid_from`/`valid_to` timestamps |
|
|
45
|
+
| **Memory taxonomy tool** | No | Yes — `memory_taxonomy` surfaces project/session/chunk counts and recent activity at a glance |
|
|
46
|
+
| **Mid-session auto-save** | No | Yes — Stop hook fires every 15 human messages (configurable via `PAI_AUTO_SAVE_INTERVAL`), saves without ending session |
|
|
47
|
+
| **Cross-project tunnel detection** | No | Yes — `memory_tunnels` finds concepts shared across multiple projects via FTS vocabulary comparison |
|
|
43
48
|
| **Session note reconstruction** | No | Yes — /reconstruct skill retroactively creates notes from JSONL + git history |
|
|
44
49
|
| **Backup / restore** | No | Yes — timestamped pg_dump + registry export |
|
|
45
50
|
| **Multi-session concurrency** | n/a | Yes — daemon multiplexes Claude sessions |
|
package/README.md
CHANGED
|
@@ -190,6 +190,60 @@ When spawning headless Claude CLI processes for summarization, the daemon strips
|
|
|
190
190
|
|
|
191
191
|
---
|
|
192
192
|
|
|
193
|
+
## Progressive Memory Loading
|
|
194
|
+
|
|
195
|
+
PAI loads context in layers at session start rather than all at once. This keeps early-session latency low while giving Claude everything it needs to be useful immediately.
|
|
196
|
+
|
|
197
|
+
### The Four Layers
|
|
198
|
+
|
|
199
|
+
| Layer | What it loads | When |
|
|
200
|
+
|-------|---------------|------|
|
|
201
|
+
| **L0 — Identity** | Your identity file (`~/.pai/identity.txt`) — who you are, your working style, key preferences | Always, at every session start |
|
|
202
|
+
| **L1 — Essential story** | Summaries from the most recent session notes — what you were doing, what decisions were made, where things stand | Always, at session start |
|
|
203
|
+
| **L2 — Topic queries** | On-demand retrieval for the current topic — fetched when a specific question or task is identified | On demand, during the session |
|
|
204
|
+
| **L3 — Deep search** | Full `memory_search` across all indexed content — for when L2 is not enough | On demand, when explicitly needed |
|
|
205
|
+
|
|
206
|
+
L0 and L1 fire automatically via the `memory_wakeup` MCP tool, which is called by the `SessionStart` hook. L2 and L3 are invoked as needed — the model decides when to go deeper based on the question at hand.
|
|
207
|
+
|
|
208
|
+
### Configuring Your Identity File
|
|
209
|
+
|
|
210
|
+
Create `~/.pai/identity.txt` with a short description of yourself and your working style. Claude will see this at every session start. Example:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
Matthias. Principal engineer. Work across TypeScript, Dart, and shell scripting.
|
|
214
|
+
Projects: PAI (AI infrastructure), RingsADay (Flutter app), Scribe (MCP server).
|
|
215
|
+
Prefer concise explanations, hate unnecessary hedging.
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Advanced Memory Tools
|
|
221
|
+
|
|
222
|
+
### Temporal Knowledge Graph
|
|
223
|
+
|
|
224
|
+
Facts change over time. The `kg_triples` table stores knowledge as subject-predicate-object triples with `valid_from` and `valid_to` timestamps, so facts can expire and contradict each other rather than accumulating in an undated blob.
|
|
225
|
+
|
|
226
|
+
Four MCP tools cover the full lifecycle:
|
|
227
|
+
|
|
228
|
+
- `kg_add` — Add a fact with a start date (and optional end date)
|
|
229
|
+
- `kg_query` — Query the graph, filtered to facts valid at a given point in time
|
|
230
|
+
- `kg_invalidate` — Mark a fact as no longer true (sets `valid_to`)
|
|
231
|
+
- `kg_contradictions` — Surface facts that directly contradict each other, using predicate inversion rules
|
|
232
|
+
|
|
233
|
+
Example: "Matthias uses PostgreSQL" added in March; "Matthias uses SQLite" added in April with the March fact invalidated. `kg_query` in April sees only the current fact; `kg_query` for March sees the historical one.
|
|
234
|
+
|
|
235
|
+
### Memory Taxonomy
|
|
236
|
+
|
|
237
|
+
`memory_taxonomy` gives a shape-of-memory overview: projects, session counts, chunk counts, embedding coverage, and recent activity. Think of it as a dashboard for your knowledge base — useful both for the model (to understand what it knows) and for you (to audit what is indexed).
|
|
238
|
+
|
|
239
|
+
### Cross-Project Tunnels
|
|
240
|
+
|
|
241
|
+
`memory_tunnels` detects concepts that appear across multiple projects. It works by comparing FTS vocabulary in SQLite mode or `ts_stat` output in PostgreSQL mode. When a concept — a library name, a design pattern, a person's name — shows up in three separate projects, PAI surfaces that connection as a tunnel.
|
|
242
|
+
|
|
243
|
+
This reveals unexpected intellectual bridges: the same concurrency pattern used in PAI's daemon showing up in your Flutter app's state management, or a vendor name appearing in both your notes and your job applications.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
193
247
|
## Automatic Observation Capture
|
|
194
248
|
|
|
195
249
|
PAI automatically classifies and stores every significant tool call during your sessions. When you edit a file, run a command, or make a decision, PAI captures it as a structured observation — building a searchable timeline of everything you've done across all projects.
|
|
@@ -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-CNU3zCwP.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-
|
|
86
|
+
//# sourceMappingURL=auto-route-CruBrTf-.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-route-
|
|
1
|
+
{"version":3,"file":"auto-route-CruBrTf-.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"}
|