zam-core 0.3.3 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,232 @@
1
- // src/kernel/db/connection.ts
2
- import Database from "libsql";
3
- import { existsSync, mkdirSync, rmSync } from "fs";
1
+ // src/kernel/analytics/stats.ts
2
+ function q(db, sql, ...params) {
3
+ return db.prepare(sql).get(...params);
4
+ }
5
+ function getUserStats(db, userId) {
6
+ return {
7
+ userId,
8
+ totalTokens: q(db, "SELECT COUNT(*) as n FROM tokens").n,
9
+ cardsInDeck: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ?", userId).n,
10
+ dueToday: q(
11
+ db,
12
+ "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
13
+ userId
14
+ ).n,
15
+ blocked: q(
16
+ db,
17
+ "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1",
18
+ userId
19
+ ).n,
20
+ mature: q(
21
+ db,
22
+ "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
23
+ userId
24
+ ).n,
25
+ avgStability: (() => {
26
+ const v = q(
27
+ db,
28
+ "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0",
29
+ userId
30
+ );
31
+ return v.v ? Math.round(v.v * 100) / 100 : null;
32
+ })(),
33
+ totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
34
+ lastSession: (() => {
35
+ const r = db.prepare(
36
+ "SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
37
+ ).get(userId);
38
+ return r?.started_at ?? null;
39
+ })()
40
+ };
41
+ }
42
+ function getDomainCompetence(db, userId) {
43
+ const domains = db.prepare(
44
+ `SELECT DISTINCT t.domain FROM cards c
45
+ JOIN tokens t ON t.id = c.token_id
46
+ WHERE c.user_id = ? AND t.domain != ''`
47
+ ).all(userId);
48
+ return domains.map((d) => {
49
+ const total = q(
50
+ db,
51
+ `SELECT COUNT(*) as n FROM cards c
52
+ JOIN tokens t ON t.id = c.token_id
53
+ WHERE c.user_id = ? AND t.domain = ?`,
54
+ userId,
55
+ d.domain
56
+ ).n;
57
+ const mature = q(
58
+ db,
59
+ `SELECT COUNT(*) as n FROM cards c
60
+ JOIN tokens t ON t.id = c.token_id
61
+ WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
62
+ userId,
63
+ d.domain
64
+ ).n;
65
+ const avgStab = q(
66
+ db,
67
+ `SELECT AVG(c.stability) as v FROM cards c
68
+ JOIN tokens t ON t.id = c.token_id
69
+ WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
70
+ userId,
71
+ d.domain
72
+ ).v ?? 0;
73
+ const reviews = q(
74
+ db,
75
+ `SELECT COUNT(*) as total,
76
+ SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
77
+ FROM review_logs
78
+ WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
79
+ userId,
80
+ d.domain
81
+ );
82
+ const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
83
+ let suggestedMode;
84
+ if (retentionRate > 0.9 && avgStab > 30) {
85
+ suggestedMode = "autonomy";
86
+ } else if (retentionRate > 0.7 && avgStab > 7) {
87
+ suggestedMode = "copilot";
88
+ } else {
89
+ suggestedMode = "shadowing";
90
+ }
91
+ return {
92
+ domain: d.domain,
93
+ totalCards: total,
94
+ matureCards: mature,
95
+ avgStability: Math.round(avgStab * 100) / 100,
96
+ retentionRate: Math.round(retentionRate * 1e3) / 1e3,
97
+ suggestedMode
98
+ };
99
+ });
100
+ }
101
+
102
+ // src/kernel/credentials.ts
103
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
104
  import { homedir } from "os";
5
105
  import { dirname, join } from "path";
106
+ var DEFAULT_CREDENTIALS_PATH = join(homedir(), ".zam", "credentials.json");
107
+ function loadCredentials(path) {
108
+ const p = path ?? DEFAULT_CREDENTIALS_PATH;
109
+ if (!existsSync(p)) return {};
110
+ try {
111
+ return JSON.parse(readFileSync(p, "utf-8"));
112
+ } catch {
113
+ return {};
114
+ }
115
+ }
116
+ function saveCredentials(creds, path) {
117
+ const p = path ?? DEFAULT_CREDENTIALS_PATH;
118
+ const dir = dirname(p);
119
+ if (!existsSync(dir)) {
120
+ mkdirSync(dir, { recursive: true });
121
+ }
122
+ writeFileSync(p, `${JSON.stringify(creds, null, 2)}
123
+ `, "utf-8");
124
+ }
125
+ function getTursoCredentials(path) {
126
+ const creds = loadCredentials(path);
127
+ if (creds.turso?.url && creds.turso?.token) {
128
+ return { url: creds.turso.url, token: creds.turso.token };
129
+ }
130
+ return null;
131
+ }
132
+ function setTursoCredentials(url, token, path) {
133
+ const creds = loadCredentials(path);
134
+ creds.turso = { url, token };
135
+ saveCredentials(creds, path);
136
+ }
137
+ function clearTursoCredentials(path) {
138
+ const creds = loadCredentials(path);
139
+ delete creds.turso;
140
+ saveCredentials(creds, path);
141
+ }
142
+ function getADOCredentials(path) {
143
+ const creds = loadCredentials(path);
144
+ if (creds.ado?.org_url && creds.ado?.project && creds.ado?.pat) {
145
+ return {
146
+ org_url: creds.ado.org_url,
147
+ project: creds.ado.project,
148
+ pat: creds.ado.pat
149
+ };
150
+ }
151
+ return null;
152
+ }
153
+ function setADOCredentials(orgUrl, project, pat, path) {
154
+ const creds = loadCredentials(path);
155
+ creds.ado = { org_url: orgUrl, project, pat };
156
+ saveCredentials(creds, path);
157
+ }
158
+ function clearADOCredentials(path) {
159
+ const creds = loadCredentials(path);
160
+ delete creds.ado;
161
+ saveCredentials(creds, path);
162
+ }
163
+
164
+ // src/kernel/connectors/azure-devops.ts
165
+ function loadADOConfig() {
166
+ const creds = getADOCredentials();
167
+ if (!creds) return null;
168
+ return {
169
+ orgUrl: creds.org_url.replace(/\/+$/, ""),
170
+ project: creds.project,
171
+ pat: creds.pat
172
+ };
173
+ }
174
+ function authHeader(pat) {
175
+ return `Basic ${Buffer.from(`:${pat}`).toString("base64")}`;
176
+ }
177
+ async function fetchActiveWorkItems(config) {
178
+ const { orgUrl, project, pat } = config;
179
+ const wiqlUrl = `${orgUrl}/${project}/_apis/wit/wiql?api-version=7.1`;
180
+ const wiqlBody = {
181
+ query: `SELECT [System.Id] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] NOT IN ('Closed', 'Completed', 'Done', 'Removed') ORDER BY [Microsoft.VSTS.Common.Priority] ASC, [System.ChangedDate] DESC`
182
+ };
183
+ const wiqlRes = await fetch(wiqlUrl, {
184
+ method: "POST",
185
+ headers: {
186
+ Authorization: authHeader(pat),
187
+ "Content-Type": "application/json"
188
+ },
189
+ body: JSON.stringify(wiqlBody)
190
+ });
191
+ if (!wiqlRes.ok) {
192
+ const text = await wiqlRes.text();
193
+ throw new Error(`ADO WIQL query failed (${wiqlRes.status}): ${text}`);
194
+ }
195
+ const wiqlData = await wiqlRes.json();
196
+ const ids = wiqlData.workItems.map((wi) => wi.id);
197
+ if (ids.length === 0) return [];
198
+ const batchIds = ids.slice(0, 200);
199
+ const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
200
+ const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
201
+ const detailRes = await fetch(detailUrl, {
202
+ headers: { Authorization: authHeader(pat) }
203
+ });
204
+ if (!detailRes.ok) {
205
+ const text = await detailRes.text();
206
+ throw new Error(
207
+ `ADO work items fetch failed (${detailRes.status}): ${text}`
208
+ );
209
+ }
210
+ const detailData = await detailRes.json();
211
+ return detailData.value.map((wi) => ({
212
+ id: wi.id,
213
+ title: wi.fields["System.Title"],
214
+ state: wi.fields["System.State"],
215
+ type: wi.fields["System.WorkItemType"],
216
+ assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
217
+ }));
218
+ }
219
+
220
+ // src/kernel/db/connection.ts
221
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync } from "fs";
222
+ import { homedir as homedir2 } from "os";
223
+ import { dirname as dirname2, join as join2 } from "path";
224
+ import Database from "libsql";
6
225
 
7
226
  // src/kernel/db/schema.ts
8
227
  var SCHEMA = `
9
- -- Use WAL mode for concurrent reads (AI CLI + user CLI)
10
- PRAGMA journal_mode = WAL;
11
- PRAGMA foreign_keys = ON;
228
+ -- PRAGMAs (WAL, foreign_keys) are set programmatically in connection.ts,
229
+ -- not here, because libsql embedded replicas manage their own WAL.
12
230
 
13
231
  -- Knowledge tokens: atomic concepts/facts with Bloom levels
14
232
  CREATE TABLE IF NOT EXISTS tokens (
@@ -19,9 +237,11 @@ CREATE TABLE IF NOT EXISTS tokens (
19
237
  bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
20
238
  context TEXT NOT NULL DEFAULT '',
21
239
  symbiosis_mode TEXT CHECK (symbiosis_mode IN ('shadowing', 'copilot', 'autonomy')),
240
+ source_link TEXT,
22
241
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
23
242
  updated_at TEXT NOT NULL DEFAULT (datetime('now')),
24
- deprecated_at TEXT
243
+ deprecated_at TEXT,
244
+ question TEXT
25
245
  );
26
246
 
27
247
  -- Prerequisite dependency graph: "to learn A, first know B"
@@ -115,58 +335,90 @@ CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id
115
335
  `;
116
336
 
117
337
  // src/kernel/db/connection.ts
118
- var DEFAULT_DB_DIR = join(homedir(), ".zam");
119
- var DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, "zam.db");
338
+ var DEFAULT_DB_DIR = join2(homedir2(), ".zam");
339
+ var DEFAULT_DB_PATH = join2(DEFAULT_DB_DIR, "zam.db");
340
+ function isRemoteDatabasePath(dbPath) {
341
+ return /^(libsql|https?):\/\//i.test(dbPath);
342
+ }
120
343
  function openDatabase(options = {}) {
121
- const dbPath = options.dbPath ?? DEFAULT_DB_PATH;
122
- if (options.initialize) {
123
- const dir = dirname(dbPath);
124
- if (!existsSync(dir)) {
125
- mkdirSync(dir, { recursive: true });
344
+ const configuredCloud = options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl ? getTursoCredentials() : null;
345
+ let requiresTurso = false;
346
+ try {
347
+ const configPath = join2(process.cwd(), ".zam", "config.yaml");
348
+ if (existsSync2(configPath)) {
349
+ const configText = readFileSync2(configPath, "utf-8");
350
+ if (/[\s\S]*turso:[\s\S]*url:/m.test(configText)) {
351
+ requiresTurso = true;
352
+ }
353
+ }
354
+ } catch (_e) {
355
+ }
356
+ if (requiresTurso && !configuredCloud && options.useConfiguredCloud !== false && !options.dbPath && !options.syncUrl) {
357
+ throw new Error(
358
+ "Turso cloud database is configured in .zam/config.yaml but missing local credentials. Run: zam connector setup turso"
359
+ );
360
+ }
361
+ const dbPath = configuredCloud?.url ?? options.dbPath ?? DEFAULT_DB_PATH;
362
+ const isRemote = isRemoteDatabasePath(dbPath);
363
+ const isEmbeddedReplica = Boolean(options.syncUrl);
364
+ if (options.initialize && !isRemote) {
365
+ const dir = dirname2(dbPath);
366
+ if (!existsSync2(dir)) {
367
+ mkdirSync2(dir, { recursive: true });
126
368
  }
127
369
  }
128
370
  const dbOpts = {};
129
371
  if (options.syncUrl) {
130
372
  dbOpts.syncUrl = options.syncUrl;
373
+ const metaPath = `${dbPath}.meta`;
374
+ const infoPath = `${dbPath}-info`;
375
+ if (existsSync2(dbPath) && !existsSync2(metaPath) && !existsSync2(infoPath)) {
376
+ for (const suffix of ["", "-wal", "-shm"]) {
377
+ const f = `${dbPath}${suffix}`;
378
+ if (existsSync2(f)) rmSync(f, { force: true });
379
+ }
380
+ } else if (!existsSync2(dbPath) && (existsSync2(metaPath) || existsSync2(infoPath))) {
381
+ if (existsSync2(metaPath)) rmSync(metaPath);
382
+ if (existsSync2(infoPath)) rmSync(infoPath);
383
+ }
131
384
  }
132
- if (options.authToken) {
133
- dbOpts.authToken = options.authToken;
385
+ const authToken = configuredCloud?.token ?? options.authToken;
386
+ if (authToken) {
387
+ dbOpts.authToken = authToken;
134
388
  }
135
389
  let db;
136
390
  try {
137
391
  db = new Database(dbPath, dbOpts);
138
392
  } catch (err) {
139
- if (options.syncUrl && err.message?.includes("InvalidLocalState")) {
140
- for (const suffix of ["", "-wal", "-shm"]) {
141
- const f = `${dbPath}${suffix}`;
142
- if (existsSync(f)) rmSync(f, { force: true });
143
- }
393
+ const msg = err.message;
394
+ if (msg.includes("InvalidLocalState") && options.syncUrl) {
395
+ const metaPath = `${dbPath}.meta`;
396
+ const infoPath = `${dbPath}-info`;
397
+ if (existsSync2(metaPath)) rmSync(metaPath);
398
+ if (existsSync2(infoPath)) rmSync(infoPath);
144
399
  db = new Database(dbPath, dbOpts);
145
400
  } else {
146
401
  throw err;
147
402
  }
148
403
  }
149
- db.pragma("journal_mode = WAL");
404
+ if (!isRemote && !isEmbeddedReplica) {
405
+ db.pragma("journal_mode = WAL");
406
+ }
150
407
  db.pragma("foreign_keys = ON");
151
- db.pragma("busy_timeout = 5000");
408
+ if (!isRemote) {
409
+ db.pragma("busy_timeout = 5000");
410
+ }
411
+ if (isEmbeddedReplica) {
412
+ db.sync();
413
+ }
152
414
  if (options.initialize) {
153
415
  db.exec(SCHEMA);
154
416
  }
155
417
  runMigrations(db);
156
- if (options.syncUrl) {
157
- db.sync();
158
- }
159
418
  return db;
160
419
  }
161
420
  function openDatabaseWithSync(options = {}) {
162
- const db = openDatabase(options);
163
- const syncUrl = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.url");
164
- const authToken = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.token");
165
- if (!syncUrl || !authToken) return db;
166
- db.pragma("wal_checkpoint(TRUNCATE)");
167
- db.pragma("journal_mode = DELETE");
168
- db.close();
169
- return openDatabase({ ...options, syncUrl: syncUrl.value, authToken: authToken.value });
421
+ return openDatabase(options);
170
422
  }
171
423
  function getDefaultDbPath() {
172
424
  return DEFAULT_DB_PATH;
@@ -182,6 +434,12 @@ function runMigrations(db) {
182
434
  if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
183
435
  db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
184
436
  }
437
+ if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "source_link")) {
438
+ db.exec(`ALTER TABLE tokens ADD COLUMN source_link TEXT`);
439
+ }
440
+ if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "question")) {
441
+ db.exec(`ALTER TABLE tokens ADD COLUMN question TEXT`);
442
+ }
185
443
  db.exec(`
186
444
  CREATE TABLE IF NOT EXISTS agent_skills (
187
445
  id TEXT PRIMARY KEY,
@@ -196,111 +454,232 @@ function runMigrations(db) {
196
454
  `);
197
455
  }
198
456
 
199
- // src/kernel/models/token.ts
200
- import { ulid } from "ulid";
201
- function createToken(db, input) {
202
- const id = ulid();
203
- const now = (/* @__PURE__ */ new Date()).toISOString();
204
- const bloom = input.bloom_level ?? 1;
205
- if (bloom < 1 || bloom > 5) {
206
- throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
207
- }
208
- db.prepare(`
209
- INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, created_at, updated_at)
210
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
211
- `).run(
212
- id,
213
- input.slug,
214
- input.concept,
215
- input.domain ?? "",
216
- bloom,
217
- input.context ?? "",
218
- input.symbiosis_mode ?? null,
219
- now,
220
- now
221
- );
222
- return getTokenById(db, id);
223
- }
224
- function getTokenBySlug(db, slug) {
225
- return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
226
- }
227
- function getTokenById(db, id) {
228
- return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
457
+ // src/kernel/goals/engine.ts
458
+ import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
459
+ import { basename, join as join3 } from "path";
460
+
461
+ // src/kernel/goals/parser.ts
462
+ function parseGoalFile(content, slug, filePath) {
463
+ const { frontmatter, body } = splitFrontmatter(content);
464
+ const validStatuses = [
465
+ "active",
466
+ "completed",
467
+ "paused",
468
+ "abandoned"
469
+ ];
470
+ const status = validStatuses.includes(frontmatter.status) ? frontmatter.status : "active";
471
+ const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
472
+ return {
473
+ slug,
474
+ title: frontmatter.title || slug,
475
+ status,
476
+ parent: frontmatter.parent || null,
477
+ created: frontmatter.created || now,
478
+ updated: frontmatter.updated || now,
479
+ body,
480
+ filePath
481
+ };
229
482
  }
230
- function deprecateToken(db, slug) {
231
- const token = getTokenBySlug(db, slug);
232
- if (!token) {
233
- throw new Error(`Token not found: ${slug}`);
483
+ function serializeGoal(goal) {
484
+ const lines = ["---", `title: ${goal.title}`, `status: ${goal.status}`];
485
+ if (goal.parent) {
486
+ lines.push(`parent: ${goal.parent}`);
234
487
  }
235
- if (token.deprecated_at) {
236
- throw new Error(`Token already deprecated: ${slug}`);
488
+ lines.push(`created: ${goal.created}`);
489
+ lines.push(`updated: ${goal.updated}`);
490
+ lines.push("---");
491
+ lines.push("");
492
+ if (goal.body.trim()) {
493
+ lines.push(goal.body.trim());
494
+ lines.push("");
237
495
  }
238
- const now = (/* @__PURE__ */ new Date()).toISOString();
239
- db.prepare("UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?").run(
240
- now,
241
- now,
242
- slug
243
- );
244
- return getTokenBySlug(db, slug);
496
+ return lines.join("\n");
245
497
  }
246
- function findTokens(db, query) {
247
- const normalised = query.toLowerCase();
248
- const qTokens = new Set(
249
- normalised.split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter((t) => t.length > 2)
250
- );
251
- const tokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
252
- const scored = [];
253
- for (const t of tokens) {
254
- const words = (t.slug + " " + t.concept + " " + t.domain).toLowerCase().split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter(Boolean);
255
- let score = 0;
256
- for (const w of words) {
257
- if (qTokens.has(w)) score++;
258
- }
259
- if (t.concept.toLowerCase().includes(normalised.slice(0, 25))) {
260
- score += 3;
498
+ function extractTasks(body) {
499
+ const tasks = [];
500
+ const taskRegex = /^[-*]\s+\[([ xX])\]\s+(.+)$/gm;
501
+ let match = taskRegex.exec(body);
502
+ while (match !== null) {
503
+ tasks.push({
504
+ done: match[1] !== " ",
505
+ text: match[2].trim()
506
+ });
507
+ match = taskRegex.exec(body);
508
+ }
509
+ return tasks;
510
+ }
511
+ function extractTokenRefs(body) {
512
+ const tokensSection = body.match(/## Tokens\n([\s\S]*?)(?=\n## |\n*$)/);
513
+ if (!tokensSection) return [];
514
+ const refs = [];
515
+ const lines = tokensSection[1].split("\n");
516
+ for (const line of lines) {
517
+ const match = line.match(/^[-*]\s+(\S+)/);
518
+ if (match) {
519
+ refs.push(match[1]);
261
520
  }
262
- if (score > 0) {
263
- scored.push({ score, ...t });
521
+ }
522
+ return refs;
523
+ }
524
+ function splitFrontmatter(content) {
525
+ const trimmed = content.trim();
526
+ if (!trimmed.startsWith("---")) {
527
+ return { frontmatter: {}, body: trimmed };
528
+ }
529
+ const endIndex = trimmed.indexOf("---", 3);
530
+ if (endIndex === -1) {
531
+ return { frontmatter: {}, body: trimmed };
532
+ }
533
+ const fmBlock = trimmed.slice(3, endIndex).trim();
534
+ const body = trimmed.slice(endIndex + 3).trim();
535
+ const frontmatter = {};
536
+ for (const line of fmBlock.split("\n")) {
537
+ const colonIndex = line.indexOf(":");
538
+ if (colonIndex === -1) continue;
539
+ const key = line.slice(0, colonIndex).trim();
540
+ const value = line.slice(colonIndex + 1).trim();
541
+ if (key && value) {
542
+ frontmatter[key] = value;
264
543
  }
265
544
  }
266
- scored.sort((a, b) => b.score - a.score);
267
- return scored;
545
+ return { frontmatter, body };
268
546
  }
269
- function listTokens(db, options) {
270
- if (options?.domain) {
271
- return db.prepare(
272
- "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
273
- ).all(options.domain);
547
+
548
+ // src/kernel/goals/engine.ts
549
+ function listGoals(goalsDir) {
550
+ if (!existsSync3(goalsDir)) return [];
551
+ const files = readdirSync(goalsDir).filter(
552
+ (f) => f.endsWith(".md") && f !== "README.md"
553
+ );
554
+ const summaries = [];
555
+ for (const file of files) {
556
+ const filePath = join3(goalsDir, file);
557
+ const content = readFileSync3(filePath, "utf-8");
558
+ const slug = basename(file, ".md");
559
+ const goal = parseGoalFile(content, slug, filePath);
560
+ const tasks = extractTasks(goal.body);
561
+ const tokens = extractTokenRefs(goal.body);
562
+ summaries.push({
563
+ slug: goal.slug,
564
+ title: goal.title,
565
+ status: goal.status,
566
+ parent: goal.parent,
567
+ taskCount: tasks.length,
568
+ tasksDone: tasks.filter((t2) => t2.done).length,
569
+ tokenCount: tokens.length
570
+ });
274
571
  }
275
- return db.prepare(
276
- "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
277
- ).all();
572
+ const statusOrder = {
573
+ active: 0,
574
+ paused: 1,
575
+ completed: 2,
576
+ abandoned: 3
577
+ };
578
+ summaries.sort((a, b) => {
579
+ const statusDiff = statusOrder[a.status] - statusOrder[b.status];
580
+ if (statusDiff !== 0) return statusDiff;
581
+ return a.title.localeCompare(b.title);
582
+ });
583
+ return summaries;
584
+ }
585
+ function getGoal(goalsDir, slug) {
586
+ const filePath = join3(goalsDir, `${slug}.md`);
587
+ if (!existsSync3(filePath)) return void 0;
588
+ const content = readFileSync3(filePath, "utf-8");
589
+ return parseGoalFile(content, slug, filePath);
278
590
  }
591
+ function createGoal(goalsDir, input) {
592
+ const filePath = join3(goalsDir, `${input.slug}.md`);
593
+ if (existsSync3(filePath)) {
594
+ throw new Error(`Goal already exists: ${input.slug}`);
595
+ }
596
+ const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
597
+ const goal = {
598
+ slug: input.slug,
599
+ title: input.title,
600
+ status: input.status ?? "active",
601
+ parent: input.parent ?? null,
602
+ created: now,
603
+ updated: now,
604
+ body: input.description ? `## Description
605
+ ${input.description}
279
606
 
280
- // src/kernel/models/prerequisite.ts
281
- function addPrerequisite(db, tokenId, requiresId) {
282
- if (tokenId === requiresId) {
283
- throw new Error("A token cannot be a prerequisite of itself");
607
+ ## Tasks
608
+
609
+ ## Tokens` : "## Description\n\n## Tasks\n\n## Tokens",
610
+ filePath
611
+ };
612
+ writeFileSync2(filePath, serializeGoal(goal), "utf-8");
613
+ return goal;
614
+ }
615
+ function updateGoalStatus(goalsDir, slug, status) {
616
+ const goal = getGoal(goalsDir, slug);
617
+ if (!goal) throw new Error(`Goal not found: ${slug}`);
618
+ goal.status = status;
619
+ goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
620
+ writeFileSync2(goal.filePath, serializeGoal(goal), "utf-8");
621
+ return goal;
622
+ }
623
+ function getGoalTree(goalsDir) {
624
+ const all = listGoals(goalsDir);
625
+ const bySlug = new Map(all.map((g) => [g.slug, g]));
626
+ const roots = [];
627
+ const children = /* @__PURE__ */ new Map();
628
+ for (const g of all) {
629
+ if (g.parent && bySlug.has(g.parent)) {
630
+ const list = children.get(g.parent) ?? [];
631
+ list.push(g);
632
+ children.set(g.parent, list);
633
+ }
634
+ }
635
+ for (const g of all) {
636
+ if (!g.parent || !bySlug.has(g.parent)) {
637
+ roots.push({ ...g, children: children.get(g.slug) ?? [] });
638
+ }
639
+ }
640
+ return roots;
641
+ }
642
+
643
+ // src/kernel/models/agent-skill.ts
644
+ import { ulid } from "ulid";
645
+ function parseRow(row) {
646
+ return {
647
+ ...row,
648
+ steps: JSON.parse(row.steps),
649
+ token_slugs: JSON.parse(row.token_slugs)
650
+ };
651
+ }
652
+ function createAgentSkill(db, input) {
653
+ const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
654
+ if (existing) {
655
+ throw new Error(`Agent skill already exists: ${input.slug}`);
284
656
  }
657
+ const id = ulid();
658
+ const now = (/* @__PURE__ */ new Date()).toISOString();
285
659
  db.prepare(
286
- "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
287
- ).run(tokenId, requiresId);
660
+ `INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
661
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
662
+ ).run(
663
+ id,
664
+ input.slug,
665
+ input.description,
666
+ JSON.stringify(input.steps),
667
+ JSON.stringify(input.token_slugs ?? []),
668
+ input.source ?? "learned",
669
+ now,
670
+ now
671
+ );
672
+ return parseRow(
673
+ db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
674
+ );
288
675
  }
289
- function getPrerequisites(db, tokenId) {
290
- return db.prepare(
291
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
292
- FROM prerequisites p
293
- JOIN tokens t ON t.id = p.requires_id
294
- WHERE p.token_id = ?`
295
- ).all(tokenId);
676
+ function getAgentSkill(db, slug) {
677
+ const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
678
+ return row ? parseRow(row) : void 0;
296
679
  }
297
- function getDependents(db, tokenId) {
298
- return db.prepare(
299
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
300
- FROM prerequisites p
301
- JOIN tokens t ON t.id = p.token_id
302
- WHERE p.requires_id = ?`
303
- ).all(tokenId);
680
+ function listAgentSkills(db) {
681
+ const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
682
+ return rows.map(parseRow);
304
683
  }
305
684
 
306
685
  // src/kernel/models/card.ts
@@ -319,6 +698,9 @@ function ensureCard(db, tokenId, userId) {
319
698
  function getCard(db, tokenId, userId) {
320
699
  return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
321
700
  }
701
+ function getCardById(db, cardId) {
702
+ return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
703
+ }
322
704
  function updateCard(db, cardId, updates) {
323
705
  const fields = [];
324
706
  const values = [];
@@ -372,6 +754,23 @@ function updateCard(db, cardId, updates) {
372
754
  }
373
755
  return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
374
756
  }
757
+ function getCardDeletionImpact(db, tokenId, userId) {
758
+ const card = getCard(db, tokenId, userId);
759
+ if (!card) {
760
+ throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
761
+ }
762
+ const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE card_id = ?").get(card.id);
763
+ return { review_logs: reviewLogs.n };
764
+ }
765
+ function deleteCardForUser(db, tokenId, userId) {
766
+ const card = getCard(db, tokenId, userId);
767
+ if (!card) {
768
+ throw new Error(`Card not found for token ${tokenId} and user ${userId}`);
769
+ }
770
+ const impact = getCardDeletionImpact(db, tokenId, userId);
771
+ db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
772
+ return { card, impact };
773
+ }
375
774
  function getDueCards(db, userId, now) {
376
775
  const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
377
776
  return db.prepare(
@@ -392,6 +791,32 @@ function getBlockedCards(db, userId) {
392
791
  ).all(userId);
393
792
  }
394
793
 
794
+ // src/kernel/models/prerequisite.ts
795
+ function addPrerequisite(db, tokenId, requiresId) {
796
+ if (tokenId === requiresId) {
797
+ throw new Error("A token cannot be a prerequisite of itself");
798
+ }
799
+ db.prepare(
800
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
801
+ ).run(tokenId, requiresId);
802
+ }
803
+ function getPrerequisites(db, tokenId) {
804
+ return db.prepare(
805
+ `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
806
+ FROM prerequisites p
807
+ JOIN tokens t ON t.id = p.requires_id
808
+ WHERE p.token_id = ?`
809
+ ).all(tokenId);
810
+ }
811
+ function getDependents(db, tokenId) {
812
+ return db.prepare(
813
+ `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
814
+ FROM prerequisites p
815
+ JOIN tokens t ON t.id = p.token_id
816
+ WHERE p.requires_id = ?`
817
+ ).all(tokenId);
818
+ }
819
+
395
820
  // src/kernel/models/review.ts
396
821
  import { ulid as ulid3 } from "ulid";
397
822
  function logReview(db, input) {
@@ -461,12 +886,17 @@ function endSession(db, sessionId) {
461
886
  throw new Error(`Session already completed: ${sessionId}`);
462
887
  }
463
888
  const now = (/* @__PURE__ */ new Date()).toISOString();
464
- db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(now, sessionId);
889
+ db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(
890
+ now,
891
+ sessionId
892
+ );
465
893
  return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
466
894
  }
467
895
  function logStep(db, input) {
468
896
  if (input.done_by !== "user" && input.done_by !== "agent") {
469
- throw new Error(`done_by must be 'user' or 'agent', got '${input.done_by}'`);
897
+ throw new Error(
898
+ `done_by must be 'user' or 'agent', got '${input.done_by}'`
899
+ );
470
900
  }
471
901
  if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
472
902
  throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
@@ -506,52 +936,10 @@ function getSessionSummary(db, sessionId) {
506
936
  return { session, steps };
507
937
  }
508
938
 
509
- // src/kernel/models/agent-skill.ts
510
- import { ulid as ulid5 } from "ulid";
511
- function parseRow(row) {
512
- return {
513
- ...row,
514
- steps: JSON.parse(row.steps),
515
- token_slugs: JSON.parse(row.token_slugs)
516
- };
517
- }
518
- function createAgentSkill(db, input) {
519
- const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
520
- if (existing) {
521
- throw new Error(`Agent skill already exists: ${input.slug}`);
522
- }
523
- const id = ulid5();
524
- const now = (/* @__PURE__ */ new Date()).toISOString();
525
- db.prepare(
526
- `INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
527
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
528
- ).run(
529
- id,
530
- input.slug,
531
- input.description,
532
- JSON.stringify(input.steps),
533
- JSON.stringify(input.token_slugs ?? []),
534
- input.source ?? "learned",
535
- now,
536
- now
537
- );
538
- return parseRow(
539
- db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
540
- );
541
- }
542
- function getAgentSkill(db, slug) {
543
- const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
544
- return row ? parseRow(row) : void 0;
545
- }
546
- function listAgentSkills(db) {
547
- const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
548
- return rows.map(parseRow);
549
- }
550
-
551
- // src/kernel/models/settings.ts
552
- function getSetting(db, key) {
553
- const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
554
- return row?.value;
939
+ // src/kernel/models/settings.ts
940
+ function getSetting(db, key) {
941
+ const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
942
+ return row?.value;
555
943
  }
556
944
  function getAllSettings(db) {
557
945
  const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
@@ -576,1323 +964,2328 @@ function deleteSetting(db, key) {
576
964
  return result.changes > 0;
577
965
  }
578
966
 
579
- // src/kernel/scheduler/fsrs.ts
580
- var DEFAULT_W = [
581
- 0.4072,
582
- 1.1829,
583
- 3.1262,
584
- 15.4722,
585
- // w0–w3: initial stability per rating
586
- 7.2102,
587
- 0.5316,
588
- 1.0651,
589
- // w4–w6: difficulty
590
- 92e-4,
591
- 1.5988,
592
- 0.1176,
593
- 1.0014,
594
- // w7–w10: stability after forgetting
595
- 2.0032,
596
- 0.0266,
597
- 0.3077,
598
- 0.15,
599
- // w11–w14: stability increase
600
- 0,
601
- 2.7849,
602
- 0.3477,
603
- 0.6831
604
- // w15–w18: additional parameters
605
- ];
606
- var DEFAULT_REQUEST_RETENTION = 0.9;
607
- function clamp(value, lo, hi) {
608
- return Math.min(hi, Math.max(lo, value));
609
- }
610
- function daysBetween(a, b) {
611
- return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
612
- }
613
- function initialStability(w, rating) {
614
- return w[rating - 1];
615
- }
616
- function initialDifficulty(w, rating) {
617
- return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
618
- }
619
- function nextDifficulty(w, d, rating) {
620
- const d0ForGood = initialDifficulty(w, 3);
621
- const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
622
- return clamp(updated, 1, 10);
623
- }
624
- function retrievability(elapsed, stability) {
625
- if (stability <= 0) return 0;
626
- return Math.pow(1 + elapsed / (9 * stability), -1);
627
- }
628
- function stabilityAfterSuccess(w, s, d, r, rating) {
629
- const hardPenalty = rating === 2 ? w[15] : 1;
630
- const easyBonus = rating === 4 ? w[16] : 1;
631
- const inner = Math.exp(w[8]) * (11 - d) * Math.pow(s, -w[9]) * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
632
- return s * (inner + 1);
967
+ // src/kernel/models/token.ts
968
+ import { ulid as ulid5 } from "ulid";
969
+ function createToken(db, input) {
970
+ const id = ulid5();
971
+ const now = (/* @__PURE__ */ new Date()).toISOString();
972
+ const bloom = input.bloom_level ?? 1;
973
+ if (bloom < 1 || bloom > 5) {
974
+ throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
975
+ }
976
+ db.prepare(`
977
+ INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, created_at, updated_at)
978
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
979
+ `).run(
980
+ id,
981
+ input.slug,
982
+ input.concept,
983
+ input.domain ?? "",
984
+ bloom,
985
+ input.context ?? "",
986
+ input.symbiosis_mode ?? null,
987
+ input.source_link ?? null,
988
+ input.question ?? null,
989
+ now,
990
+ now
991
+ );
992
+ return getTokenById(db, id);
633
993
  }
634
- function stabilityAfterForgetting(w, s, d, r) {
635
- return w[11] * Math.pow(d, -w[12]) * (Math.pow(s + 1, w[13]) - 1) * Math.exp(w[14] * (1 - r));
994
+ function getTokenBySlug(db, slug) {
995
+ return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
636
996
  }
637
- function nextInterval(stability, requestRetention) {
638
- const interval = 9 * stability * (1 / requestRetention - 1);
639
- return Math.max(1, Math.round(interval));
997
+ function getTokenById(db, id) {
998
+ return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
640
999
  }
641
- function createFSRS(params) {
642
- const resolvedParams = {
643
- w: params?.w ?? [...DEFAULT_W],
644
- requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
645
- };
646
- function schedule(card, rating, now) {
647
- const reviewTime = now ?? /* @__PURE__ */ new Date();
648
- const w = resolvedParams.w;
649
- const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
650
- if (card.state === "new") {
651
- const s = initialStability(w, rating);
652
- const d = initialDifficulty(w, rating);
653
- const interval2 = nextInterval(s, resolvedParams.requestRetention);
654
- const dueAt2 = new Date(reviewTime);
655
- dueAt2.setDate(dueAt2.getDate() + interval2);
656
- return {
657
- stability: s,
658
- difficulty: d,
659
- elapsedDays: 0,
660
- scheduledDays: interval2,
661
- reps: rating >= 2 ? 1 : 0,
662
- lapses: rating === 1 ? 1 : 0,
663
- state: "learning",
664
- dueAt: dueAt2,
665
- lastReviewAt: reviewTime
666
- };
1000
+ function updateToken(db, slug, updates) {
1001
+ const token = getTokenBySlug(db, slug);
1002
+ if (!token) {
1003
+ throw new Error(`Token not found: ${slug}`);
1004
+ }
1005
+ const fields = [];
1006
+ const values = [];
1007
+ if (updates.concept !== void 0) {
1008
+ fields.push("concept = ?");
1009
+ values.push(updates.concept);
1010
+ }
1011
+ if (updates.domain !== void 0) {
1012
+ fields.push("domain = ?");
1013
+ values.push(updates.domain);
1014
+ }
1015
+ if (updates.bloom_level !== void 0) {
1016
+ if (updates.bloom_level < 1 || updates.bloom_level > 5) {
1017
+ throw new Error(
1018
+ `bloom_level must be between 1 and 5, got ${updates.bloom_level}`
1019
+ );
667
1020
  }
668
- const r = retrievability(elapsed, card.stability);
669
- let newStability;
670
- let newDifficulty;
671
- let newReps;
672
- let newLapses;
673
- let newState;
674
- if (rating === 1) {
675
- newStability = stabilityAfterForgetting(w, card.stability, card.difficulty, r);
676
- newDifficulty = nextDifficulty(w, card.difficulty, rating);
677
- newReps = 0;
678
- newLapses = card.lapses + 1;
679
- newState = "relearning";
680
- } else {
681
- newStability = stabilityAfterSuccess(w, card.stability, card.difficulty, r, rating);
682
- newDifficulty = nextDifficulty(w, card.difficulty, rating);
683
- newReps = card.reps + 1;
684
- newLapses = card.lapses;
685
- newState = "review";
1021
+ fields.push("bloom_level = ?");
1022
+ values.push(updates.bloom_level);
1023
+ }
1024
+ if (updates.context !== void 0) {
1025
+ fields.push("context = ?");
1026
+ values.push(updates.context);
1027
+ }
1028
+ if (updates.symbiosis_mode !== void 0) {
1029
+ const validModes = ["shadowing", "copilot", "autonomy"];
1030
+ if (updates.symbiosis_mode !== null && !validModes.includes(updates.symbiosis_mode)) {
1031
+ throw new Error(`Invalid symbiosis_mode: ${updates.symbiosis_mode}`);
686
1032
  }
687
- const interval = nextInterval(newStability, resolvedParams.requestRetention);
688
- const dueAt = new Date(reviewTime);
689
- dueAt.setDate(dueAt.getDate() + interval);
690
- return {
691
- stability: newStability,
692
- difficulty: newDifficulty,
693
- elapsedDays: elapsed,
694
- scheduledDays: interval,
695
- reps: newReps,
696
- lapses: newLapses,
697
- state: newState,
698
- dueAt,
699
- lastReviewAt: reviewTime
700
- };
1033
+ fields.push("symbiosis_mode = ?");
1034
+ values.push(updates.symbiosis_mode);
1035
+ }
1036
+ if (updates.source_link !== void 0) {
1037
+ fields.push("source_link = ?");
1038
+ values.push(updates.source_link);
1039
+ }
1040
+ if (updates.question !== void 0) {
1041
+ fields.push("question = ?");
1042
+ values.push(updates.question);
1043
+ }
1044
+ if (fields.length === 0) {
1045
+ throw new Error("updateToken called with no fields to update");
1046
+ }
1047
+ fields.push("updated_at = ?");
1048
+ values.push((/* @__PURE__ */ new Date()).toISOString());
1049
+ values.push(slug);
1050
+ db.prepare(`UPDATE tokens SET ${fields.join(", ")} WHERE slug = ?`).run(
1051
+ ...values
1052
+ );
1053
+ return getTokenBySlug(db, slug);
1054
+ }
1055
+ function deprecateToken(db, slug) {
1056
+ const token = getTokenBySlug(db, slug);
1057
+ if (!token) {
1058
+ throw new Error(`Token not found: ${slug}`);
1059
+ }
1060
+ if (token.deprecated_at) {
1061
+ throw new Error(`Token already deprecated: ${slug}`);
1062
+ }
1063
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1064
+ db.prepare(
1065
+ "UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?"
1066
+ ).run(now, now, slug);
1067
+ return getTokenBySlug(db, slug);
1068
+ }
1069
+ function getTokenDeleteImpact(db, slug) {
1070
+ const token = getTokenBySlug(db, slug);
1071
+ if (!token) {
1072
+ throw new Error(`Token not found: ${slug}`);
701
1073
  }
1074
+ const cards = db.prepare("SELECT COUNT(*) AS n FROM cards WHERE token_id = ?").get(token.id);
1075
+ const reviewLogs = db.prepare("SELECT COUNT(*) AS n FROM review_logs WHERE token_id = ?").get(token.id);
1076
+ const prereqsFrom = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE token_id = ?").get(token.id);
1077
+ const prereqsTo = db.prepare("SELECT COUNT(*) AS n FROM prerequisites WHERE requires_id = ?").get(token.id);
1078
+ const sessionSteps = db.prepare("SELECT COUNT(*) AS n FROM session_steps WHERE token_id = ?").get(token.id);
1079
+ const sessionsTouched = db.prepare(
1080
+ "SELECT COUNT(DISTINCT session_id) AS n FROM session_steps WHERE token_id = ?"
1081
+ ).get(token.id);
1082
+ const skillRows = db.prepare("SELECT token_slugs FROM agent_skills").all();
1083
+ const agentSkills = skillRows.filter((row) => {
1084
+ const tokenSlugs = JSON.parse(row.token_slugs);
1085
+ return tokenSlugs.includes(slug);
1086
+ }).length;
702
1087
  return {
703
- schedule,
704
- params: Object.freeze(resolvedParams)
1088
+ cards: cards.n,
1089
+ review_logs: reviewLogs.n,
1090
+ prerequisite_edges_from_token: prereqsFrom.n,
1091
+ prerequisite_edges_to_token: prereqsTo.n,
1092
+ session_steps: sessionSteps.n,
1093
+ sessions_touched: sessionsTouched.n,
1094
+ agent_skills: agentSkills
705
1095
  };
706
1096
  }
707
-
708
- // src/kernel/scheduler/blocker.ts
709
- function cascadeBlock(db, userId, tokenSlug) {
710
- const token = getTokenBySlug(db, tokenSlug);
1097
+ function deleteToken(db, slug) {
1098
+ const token = getTokenBySlug(db, slug);
711
1099
  if (!token) {
712
- throw new Error(`Unknown token slug: ${tokenSlug}`);
1100
+ throw new Error(`Token not found: ${slug}`);
713
1101
  }
714
- ensureCard(db, token.id, userId);
715
- db.prepare(
716
- "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
717
- ).run(token.id, userId);
718
- const prereqs = getPrerequisites(db, token.id);
719
- const surfaced = [];
720
- for (const prereq of prereqs) {
721
- const card = ensureCard(db, prereq.requires_id, userId);
722
- if (card.blocked === 1) {
723
- const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
724
- if (prereqOfPrereq.n === 0) {
725
- const now = (/* @__PURE__ */ new Date()).toISOString();
1102
+ const impact = getTokenDeleteImpact(db, slug);
1103
+ db.exec("BEGIN");
1104
+ try {
1105
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1106
+ const skillRows = db.prepare("SELECT id, token_slugs FROM agent_skills").all();
1107
+ for (const row of skillRows) {
1108
+ const tokenSlugs = JSON.parse(row.token_slugs);
1109
+ const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
1110
+ if (filtered.length !== tokenSlugs.length) {
726
1111
  db.prepare(
727
- "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
728
- ).run(now, prereq.requires_id, userId);
1112
+ "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1113
+ ).run(JSON.stringify(filtered), now, row.id);
729
1114
  }
730
1115
  }
731
- surfaced.push({
732
- slug: prereq.slug,
733
- concept: prereq.concept,
734
- bloomLevel: prereq.bloom_level
735
- });
1116
+ db.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
1117
+ db.exec("COMMIT");
1118
+ } catch (err) {
1119
+ db.exec("ROLLBACK");
1120
+ throw err;
736
1121
  }
737
- return {
738
- blockedSlug: tokenSlug,
739
- prerequisites: surfaced
740
- };
1122
+ return { token, impact };
741
1123
  }
742
- function unblockReady(db, userId) {
743
- const blockedCards = db.prepare(
744
- `SELECT c.token_id, t.slug, t.concept
745
- FROM cards c
746
- JOIN tokens t ON t.id = c.token_id
747
- WHERE c.user_id = ? AND c.blocked = 1`
748
- ).all(userId);
749
- const unblocked = [];
750
- for (const card of blockedCards) {
751
- const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
752
- const metPrereqs = db.prepare(
753
- `SELECT COUNT(*) as n FROM cards c
754
- JOIN prerequisites p ON p.requires_id = c.token_id
755
- WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
756
- ).get(card.token_id, userId);
757
- if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
758
- const now = (/* @__PURE__ */ new Date()).toISOString();
759
- db.prepare(
760
- "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
761
- ).run(now, card.token_id, userId);
762
- unblocked.push({ slug: card.slug, concept: card.concept });
1124
+ function findTokens(db, query) {
1125
+ const normalised = query.toLowerCase();
1126
+ const qTokens = new Set(
1127
+ normalised.split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter((t2) => t2.length > 2)
1128
+ );
1129
+ const tokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
1130
+ const scored = [];
1131
+ for (const t2 of tokens) {
1132
+ const words = `${t2.slug} ${t2.concept} ${t2.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1133
+ let score = 0;
1134
+ for (const w of words) {
1135
+ if (qTokens.has(w)) score++;
1136
+ }
1137
+ if (t2.concept.toLowerCase().includes(normalised.slice(0, 25))) {
1138
+ score += 3;
1139
+ }
1140
+ if (score > 0) {
1141
+ scored.push({ score, ...t2 });
763
1142
  }
764
1143
  }
765
- return { unblocked };
1144
+ scored.sort((a, b) => b.score - a.score);
1145
+ return scored;
1146
+ }
1147
+ function listTokens(db, options) {
1148
+ if (options?.domain) {
1149
+ return db.prepare(
1150
+ "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1151
+ ).all(options.domain);
1152
+ }
1153
+ return db.prepare(
1154
+ "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1155
+ ).all();
766
1156
  }
767
1157
 
768
- // src/kernel/scheduler/interleaver.ts
769
- function interleave(items, maxConsecutive = 2) {
770
- if (items.length <= 1) return [...items];
771
- const byDomain = /* @__PURE__ */ new Map();
772
- for (const item of items) {
773
- const group = byDomain.get(item.domain);
774
- if (group) {
775
- group.push(item);
776
- } else {
777
- byDomain.set(item.domain, [item]);
1158
+ // src/kernel/observation/analyzer.ts
1159
+ function parseMonitorLog(jsonl) {
1160
+ const events = [];
1161
+ for (const line of jsonl.split("\n")) {
1162
+ const trimmed = line.trim();
1163
+ if (!trimmed) continue;
1164
+ try {
1165
+ events.push(JSON.parse(trimmed));
1166
+ } catch {
778
1167
  }
779
1168
  }
780
- if (byDomain.size === 1) return [...items];
781
- const result = [];
782
- let consecutiveCount = 0;
783
- let lastDomain = null;
784
- const cursors = /* @__PURE__ */ new Map();
785
- for (const domain of byDomain.keys()) {
786
- cursors.set(domain, 0);
1169
+ return events;
1170
+ }
1171
+ function pairCommands(events) {
1172
+ const starts = /* @__PURE__ */ new Map();
1173
+ const records = [];
1174
+ for (const e of events) {
1175
+ if (e.type === "command_start" && e.seq != null) {
1176
+ const key = `${e.pid ?? 0}:${e.seq}`;
1177
+ starts.set(key, e);
1178
+ } else if (e.type === "command_end" && e.seq != null) {
1179
+ const key = `${e.pid ?? 0}:${e.seq}`;
1180
+ const start = starts.get(key);
1181
+ if (start) {
1182
+ const startMs = new Date(start.ts).getTime();
1183
+ const endMs = new Date(e.ts).getTime();
1184
+ records.push({
1185
+ seq: e.seq,
1186
+ pid: e.pid ?? 0,
1187
+ command: start.command ?? "",
1188
+ cwd: start.cwd ?? "",
1189
+ startedAt: start.ts,
1190
+ endedAt: e.ts,
1191
+ durationMs: endMs - startMs,
1192
+ exitCode: e.exit_code ?? null
1193
+ });
1194
+ starts.delete(key);
1195
+ }
1196
+ }
787
1197
  }
788
- while (result.length < items.length) {
789
- const activeDomains = [...byDomain.entries()].filter(([domain]) => cursors.get(domain) < byDomain.get(domain).length).sort((a, b) => {
790
- const remainA = a[1].length - cursors.get(a[0]);
791
- const remainB = b[1].length - cursors.get(b[0]);
792
- return remainB - remainA;
1198
+ for (const [, start] of starts) {
1199
+ records.push({
1200
+ seq: start.seq ?? 0,
1201
+ pid: start.pid ?? 0,
1202
+ command: start.command ?? "",
1203
+ cwd: start.cwd ?? "",
1204
+ startedAt: start.ts,
1205
+ endedAt: null,
1206
+ durationMs: null,
1207
+ exitCode: null
793
1208
  });
794
- if (activeDomains.length === 0) break;
795
- let pickedThisRound = false;
796
- for (const [domain, group] of activeDomains) {
797
- const cursor = cursors.get(domain);
798
- if (cursor >= group.length) continue;
799
- if (domain === lastDomain && consecutiveCount >= maxConsecutive) {
800
- continue;
801
- }
802
- result.push(group[cursor]);
803
- cursors.set(domain, cursor + 1);
804
- pickedThisRound = true;
805
- if (domain === lastDomain) {
806
- consecutiveCount++;
807
- } else {
808
- lastDomain = domain;
809
- consecutiveCount = 1;
1209
+ }
1210
+ records.sort(
1211
+ (a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
1212
+ );
1213
+ return records;
1214
+ }
1215
+ var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
1216
+ var HELP_WINDOW_MS = 6e4;
1217
+ function matchesToken(command, patterns) {
1218
+ const lower = command.toLowerCase();
1219
+ return patterns.some((p) => lower.includes(p.toLowerCase()));
1220
+ }
1221
+ function isHelpCommand(command) {
1222
+ const lower = command.toLowerCase();
1223
+ return HELP_PATTERNS.some((p) => lower.includes(p));
1224
+ }
1225
+ function commandPrefix(command) {
1226
+ return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
1227
+ }
1228
+ function computeMedian(values) {
1229
+ if (values.length === 0) return null;
1230
+ const sorted = [...values].sort((a, b) => a - b);
1231
+ const mid = Math.floor(sorted.length / 2);
1232
+ return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1233
+ }
1234
+ function analyzeObservation(commands, tokenPatterns) {
1235
+ const matchedSet = /* @__PURE__ */ new Set();
1236
+ const ratings = [];
1237
+ for (const tp of tokenPatterns) {
1238
+ const matchIndices = [];
1239
+ const matchedTexts = [];
1240
+ for (let i = 0; i < commands.length; i++) {
1241
+ if (matchesToken(commands[i].command, tp.patterns)) {
1242
+ matchIndices.push(i);
1243
+ matchedTexts.push(commands[i].command);
1244
+ matchedSet.add(i);
810
1245
  }
811
- break;
812
1246
  }
813
- if (!pickedThisRound) {
814
- for (const [domain, group] of activeDomains) {
815
- const cursor = cursors.get(domain);
816
- if (cursor >= group.length) continue;
817
- result.push(group[cursor]);
818
- cursors.set(domain, cursor + 1);
819
- if (domain === lastDomain) {
820
- consecutiveCount++;
821
- } else {
822
- lastDomain = domain;
823
- consecutiveCount = 1;
1247
+ if (matchIndices.length === 0) {
1248
+ ratings.push({
1249
+ tokenSlug: tp.slug,
1250
+ rating: null,
1251
+ confidence: "low",
1252
+ evidence: {
1253
+ matchedCommands: 0,
1254
+ helpSeeking: false,
1255
+ errorCount: 0,
1256
+ selfCorrections: 0,
1257
+ medianGapMs: null,
1258
+ thinkingGapMs: null
1259
+ },
1260
+ matchedCommandTexts: []
1261
+ });
1262
+ continue;
1263
+ }
1264
+ let helpSeeking = false;
1265
+ for (const mi of matchIndices) {
1266
+ const matchTime = new Date(commands[mi].startedAt).getTime();
1267
+ for (let j = 0; j < commands.length; j++) {
1268
+ if (j === mi) continue;
1269
+ const cmdTime = new Date(commands[j].startedAt).getTime();
1270
+ if (cmdTime >= matchTime - HELP_WINDOW_MS && cmdTime < matchTime) {
1271
+ if (isHelpCommand(commands[j].command)) {
1272
+ helpSeeking = true;
1273
+ break;
1274
+ }
824
1275
  }
825
- break;
1276
+ }
1277
+ if (helpSeeking) break;
1278
+ }
1279
+ let errorCount = 0;
1280
+ for (const mi of matchIndices) {
1281
+ if (commands[mi].exitCode != null && commands[mi].exitCode !== 0) {
1282
+ errorCount++;
1283
+ }
1284
+ }
1285
+ let selfCorrections = 0;
1286
+ const prefixGroups = /* @__PURE__ */ new Map();
1287
+ for (const mi of matchIndices) {
1288
+ const prefix = commandPrefix(commands[mi].command);
1289
+ prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
1290
+ }
1291
+ for (const count of prefixGroups.values()) {
1292
+ if (count > 1) selfCorrections += count - 1;
1293
+ }
1294
+ const gaps = [];
1295
+ for (let k = 1; k < matchIndices.length; k++) {
1296
+ const prev = commands[matchIndices[k - 1]];
1297
+ const curr = commands[matchIndices[k]];
1298
+ if (prev.endedAt) {
1299
+ const gap = new Date(curr.startedAt).getTime() - new Date(prev.endedAt).getTime();
1300
+ if (gap >= 0) gaps.push(gap);
1301
+ }
1302
+ }
1303
+ let thinkingGapMs = null;
1304
+ const firstMatchIdx = matchIndices[0];
1305
+ if (firstMatchIdx > 0) {
1306
+ const prev = commands[firstMatchIdx - 1];
1307
+ if (prev.endedAt) {
1308
+ thinkingGapMs = new Date(commands[firstMatchIdx].startedAt).getTime() - new Date(prev.endedAt).getTime();
826
1309
  }
827
1310
  }
1311
+ const medianGapMs = computeMedian(gaps);
1312
+ const rating = inferRating({
1313
+ helpSeeking,
1314
+ errorCount,
1315
+ selfCorrections,
1316
+ medianGapMs,
1317
+ thinkingGapMs,
1318
+ matchedCommands: matchIndices.length
1319
+ });
1320
+ const confidence = matchIndices.length >= 3 ? "high" : matchIndices.length >= 2 ? "medium" : "low";
1321
+ ratings.push({
1322
+ tokenSlug: tp.slug,
1323
+ rating,
1324
+ confidence,
1325
+ evidence: {
1326
+ matchedCommands: matchIndices.length,
1327
+ helpSeeking,
1328
+ errorCount,
1329
+ selfCorrections,
1330
+ medianGapMs,
1331
+ thinkingGapMs
1332
+ },
1333
+ matchedCommandTexts: matchedTexts
1334
+ });
828
1335
  }
829
- return result;
830
- }
831
-
832
- // src/kernel/scheduler/queue.ts
833
- function buildReviewQueue(db, options) {
834
- const maxNew = options.maxNew ?? 10;
835
- const maxReviews = options.maxReviews ?? 50;
836
- const now = options.now ?? /* @__PURE__ */ new Date();
837
- const nowISO = now.toISOString();
838
- const dueRows = db.prepare(
839
- `SELECT
840
- c.id AS card_id,
841
- c.token_id AS token_id,
842
- t.slug AS slug,
843
- t.concept AS concept,
844
- t.domain AS domain,
845
- t.bloom_level AS bloom_level,
846
- c.state AS state,
847
- c.due_at AS due_at
848
- FROM cards c
849
- JOIN tokens t ON t.id = c.token_id
850
- WHERE c.user_id = ?
851
- AND c.blocked = 0
852
- AND c.due_at <= ?
853
- AND c.state IN ('review', 'relearning', 'learning')
854
- AND t.deprecated_at IS NULL
855
- ORDER BY c.due_at ASC`
856
- ).all(options.userId, nowISO);
857
- const newRows = db.prepare(
858
- `SELECT
859
- c.id AS card_id,
860
- c.token_id AS token_id,
861
- t.slug AS slug,
862
- t.concept AS concept,
863
- t.domain AS domain,
864
- t.bloom_level AS bloom_level,
865
- c.state AS state,
866
- c.due_at AS due_at
867
- FROM cards c
868
- JOIN tokens t ON t.id = c.token_id
869
- WHERE c.user_id = ?
870
- AND c.blocked = 0
871
- AND c.state = 'new'
872
- AND t.deprecated_at IS NULL
873
- ORDER BY t.bloom_level ASC, t.slug ASC
874
- LIMIT ?`
875
- ).all(options.userId, maxNew);
876
- const nowMs = now.getTime();
877
- const sortedDue = [...dueRows].sort((a, b) => {
878
- const overdueA = nowMs - new Date(a.due_at).getTime();
879
- const overdueB = nowMs - new Date(b.due_at).getTime();
880
- return overdueB - overdueA;
881
- });
882
- const interleavedDue = interleave(
883
- sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
884
- );
885
- const newItems = newRows.map(rowToItem);
886
- const merged = intersperseNew(interleavedDue, newItems, 5);
887
- const capped = merged.slice(0, maxReviews);
888
- let newCount = 0;
889
- let reviewCount = 0;
890
- let relearnCount = 0;
891
- const domainSet = /* @__PURE__ */ new Set();
892
- for (const item of capped) {
893
- domainSet.add(item.domain);
894
- switch (item.state) {
895
- case "new":
896
- newCount++;
897
- break;
898
- case "relearning":
899
- relearnCount++;
900
- break;
901
- default:
902
- reviewCount++;
903
- break;
1336
+ const unmatchedCommands = [];
1337
+ for (let i = 0; i < commands.length; i++) {
1338
+ if (!matchedSet.has(i) && !isHelpCommand(commands[i].command)) {
1339
+ unmatchedCommands.push(commands[i].command);
904
1340
  }
905
1341
  }
906
- return {
907
- items: capped,
908
- newCount,
909
- reviewCount,
910
- relearnCount,
911
- totalDomains: [...domainSet].sort()
912
- };
1342
+ let timeSpan = null;
1343
+ if (commands.length > 0) {
1344
+ const first = commands[0];
1345
+ const last = commands[commands.length - 1];
1346
+ const endTs = last.endedAt ?? last.startedAt;
1347
+ timeSpan = {
1348
+ start: first.startedAt,
1349
+ end: endTs,
1350
+ durationMs: new Date(endTs).getTime() - new Date(first.startedAt).getTime()
1351
+ };
1352
+ }
1353
+ return { ratings, unmatchedCommands, timeSpan };
913
1354
  }
914
- function rowToItem(row) {
915
- return {
916
- cardId: row.card_id,
917
- tokenId: row.token_id,
918
- slug: row.slug,
919
- concept: row.concept,
920
- domain: row.domain,
921
- bloomLevel: row.bloom_level,
922
- state: row.state,
923
- dueAt: row.due_at
924
- };
1355
+ function inferRating(signals) {
1356
+ const {
1357
+ helpSeeking,
1358
+ errorCount,
1359
+ selfCorrections,
1360
+ medianGapMs,
1361
+ thinkingGapMs
1362
+ } = signals;
1363
+ let negatives = 0;
1364
+ if (helpSeeking) negatives += 2;
1365
+ if (errorCount >= 3) negatives += 3;
1366
+ else if (errorCount >= 1) negatives += 1;
1367
+ if (selfCorrections >= 2) negatives += 2;
1368
+ else if (selfCorrections >= 1) negatives += 1;
1369
+ if (medianGapMs != null && medianGapMs > 3e4) negatives += 2;
1370
+ else if (medianGapMs != null && medianGapMs > 1e4) negatives += 1;
1371
+ if (thinkingGapMs != null && thinkingGapMs > 3e4) negatives += 1;
1372
+ if (negatives >= 5) return 1;
1373
+ if (negatives >= 3) return 2;
1374
+ if (negatives >= 1) return 3;
1375
+ return 4;
925
1376
  }
926
- function intersperseNew(reviews, newCards, interval) {
927
- if (newCards.length === 0) return [...reviews];
928
- if (reviews.length === 0) return [...newCards];
929
- const result = [];
930
- let reviewIdx = 0;
931
- let newIdx = 0;
932
- let position = 0;
933
- while (reviewIdx < reviews.length || newIdx < newCards.length) {
934
- if (newIdx < newCards.length && position > 0 && position % interval === interval - 1) {
935
- result.push(newCards[newIdx]);
936
- newIdx++;
937
- } else if (reviewIdx < reviews.length) {
938
- result.push(reviews[reviewIdx]);
939
- reviewIdx++;
940
- } else if (newIdx < newCards.length) {
941
- result.push(newCards[newIdx]);
942
- newIdx++;
943
- }
944
- position++;
1377
+
1378
+ // src/kernel/observation/monitor-io.ts
1379
+ import {
1380
+ appendFileSync,
1381
+ existsSync as existsSync4,
1382
+ mkdirSync as mkdirSync3,
1383
+ readFileSync as readFileSync4,
1384
+ statSync
1385
+ } from "fs";
1386
+ import { homedir as homedir3 } from "os";
1387
+ import { join as join4 } from "path";
1388
+ var MONITOR_DIR = join4(homedir3(), ".zam", "monitor");
1389
+ function getMonitorDir() {
1390
+ return MONITOR_DIR;
1391
+ }
1392
+ function getMonitorPath(sessionId) {
1393
+ return join4(MONITOR_DIR, `${sessionId}.jsonl`);
1394
+ }
1395
+ function ensureMonitorDir() {
1396
+ if (!existsSync4(MONITOR_DIR)) {
1397
+ mkdirSync3(MONITOR_DIR, { recursive: true, mode: 448 });
945
1398
  }
946
- return result;
947
1399
  }
948
-
949
- // src/kernel/recall/prompter.ts
950
- var BLOOM_VERBS = {
951
- 1: "Remember",
952
- 2: "Understand",
953
- 3: "Apply",
954
- 4: "Analyze",
955
- 5: "Synthesize"
956
- };
957
- var BLOOM_PROMPTS = {
958
- 1: (c) => `What is: ${c}?`,
959
- 2: (c) => `Explain how this works: ${c}`,
960
- 3: (c) => `Apply this concept: ${c}`,
961
- 4: (c) => `Analyze the trade-offs: ${c}`,
962
- 5: (c) => `Design a solution using: ${c}`
963
- };
964
- function generatePrompt(input) {
965
- const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
966
- return {
967
- cardId: input.cardId,
968
- tokenId: input.tokenId,
969
- slug: input.slug,
970
- question: BLOOM_PROMPTS[bloom](input.concept),
971
- concept: input.concept,
972
- domain: input.domain,
973
- bloomLevel: bloom,
974
- bloomVerb: BLOOM_VERBS[bloom],
975
- hints: []
976
- };
1400
+ function writeMonitorEvent(sessionId, event) {
1401
+ ensureMonitorDir();
1402
+ const path = getMonitorPath(sessionId);
1403
+ appendFileSync(path, `${JSON.stringify(event)}
1404
+ `);
977
1405
  }
978
-
979
- // src/kernel/recall/evaluator.ts
980
- import { ulid as ulid6 } from "ulid";
981
- function evaluateRating(db, input) {
982
- const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
983
- if (!card) {
984
- throw new Error(`Card not found: ${input.cardId}`);
1406
+ function readMonitorLog(sessionId) {
1407
+ const path = getMonitorPath(sessionId);
1408
+ if (!existsSync4(path)) {
1409
+ return [];
985
1410
  }
986
- const now = /* @__PURE__ */ new Date();
987
- const fsrs = createFSRS();
988
- const schedulingCard = {
989
- stability: card.stability,
990
- difficulty: card.difficulty,
991
- elapsedDays: card.elapsed_days,
992
- scheduledDays: card.scheduled_days,
993
- reps: card.reps,
994
- lapses: card.lapses,
995
- state: card.state,
996
- dueAt: new Date(card.due_at),
997
- lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
998
- };
999
- const updated = fsrs.schedule(schedulingCard, input.rating, now);
1000
- updateCard(db, input.cardId, {
1001
- stability: updated.stability,
1002
- difficulty: updated.difficulty,
1003
- elapsed_days: updated.elapsedDays,
1004
- scheduled_days: updated.scheduledDays,
1005
- reps: updated.reps,
1006
- lapses: updated.lapses,
1007
- state: updated.state,
1008
- due_at: updated.dueAt.toISOString(),
1009
- last_review_at: now.toISOString()
1010
- });
1011
- db.prepare(
1012
- `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
1013
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1014
- ).run(
1015
- ulid6(),
1016
- input.cardId,
1017
- input.tokenId,
1018
- input.userId,
1019
- input.rating,
1020
- input.responseTimeMs ?? null,
1021
- now.toISOString(),
1022
- card.due_at,
1023
- input.sessionId ?? null
1024
- );
1025
- return {
1026
- nextDueAt: updated.dueAt.toISOString(),
1027
- stability: updated.stability,
1028
- difficulty: updated.difficulty,
1029
- state: updated.state,
1030
- scheduledDays: updated.scheduledDays,
1031
- reps: updated.reps,
1032
- lapses: updated.lapses
1033
- };
1411
+ const content = readFileSync4(path, "utf-8");
1412
+ return parseMonitorLog(content);
1034
1413
  }
1035
-
1036
- // src/kernel/analytics/stats.ts
1037
- function q(db, sql, ...params) {
1038
- return db.prepare(sql).get(...params);
1414
+ function monitorLogExists(sessionId) {
1415
+ return existsSync4(getMonitorPath(sessionId));
1039
1416
  }
1040
- function getUserStats(db, userId) {
1041
- return {
1042
- userId,
1043
- totalTokens: q(db, "SELECT COUNT(*) as n FROM tokens").n,
1044
- cardsInDeck: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ?", userId).n,
1045
- dueToday: q(
1046
- db,
1047
- "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
1048
- userId
1049
- ).n,
1050
- blocked: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1", userId).n,
1051
- mature: q(
1052
- db,
1053
- "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
1054
- userId
1055
- ).n,
1056
- avgStability: (() => {
1057
- const v = q(db, "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0", userId);
1058
- return v.v ? Math.round(v.v * 100) / 100 : null;
1059
- })(),
1060
- totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
1061
- lastSession: (() => {
1062
- const r = db.prepare(
1063
- "SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
1064
- ).get(userId);
1065
- return r?.started_at ?? null;
1066
- })()
1067
- };
1417
+ function getMonitorLogStats(sessionId) {
1418
+ const path = getMonitorPath(sessionId);
1419
+ if (!existsSync4(path)) {
1420
+ return { exists: false, sizeBytes: 0, lineCount: 0 };
1421
+ }
1422
+ const stat = statSync(path);
1423
+ const content = readFileSync4(path, "utf-8");
1424
+ const lineCount = content.split("\n").filter((l) => l.trim()).length;
1425
+ return { exists: true, sizeBytes: stat.size, lineCount };
1068
1426
  }
1069
- function getDomainCompetence(db, userId) {
1070
- const domains = db.prepare(
1071
- `SELECT DISTINCT t.domain FROM cards c
1072
- JOIN tokens t ON t.id = c.token_id
1073
- WHERE c.user_id = ? AND t.domain != ''`
1074
- ).all(userId);
1075
- return domains.map((d) => {
1076
- const total = q(
1077
- db,
1078
- `SELECT COUNT(*) as n FROM cards c
1079
- JOIN tokens t ON t.id = c.token_id
1080
- WHERE c.user_id = ? AND t.domain = ?`,
1081
- userId,
1082
- d.domain
1083
- ).n;
1084
- const mature = q(
1085
- db,
1086
- `SELECT COUNT(*) as n FROM cards c
1087
- JOIN tokens t ON t.id = c.token_id
1088
- WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
1089
- userId,
1090
- d.domain
1091
- ).n;
1092
- const avgStab = q(
1093
- db,
1094
- `SELECT AVG(c.stability) as v FROM cards c
1095
- JOIN tokens t ON t.id = c.token_id
1096
- WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
1097
- userId,
1098
- d.domain
1099
- ).v ?? 0;
1100
- const reviews = q(
1101
- db,
1102
- `SELECT COUNT(*) as total,
1103
- SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
1104
- FROM review_logs
1105
- WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
1106
- userId,
1107
- d.domain
1108
- );
1109
- const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
1110
- let suggestedMode;
1111
- if (retentionRate > 0.9 && avgStab > 30) {
1112
- suggestedMode = "autonomy";
1113
- } else if (retentionRate > 0.7 && avgStab > 7) {
1114
- suggestedMode = "copilot";
1115
- } else {
1116
- suggestedMode = "shadowing";
1117
- }
1118
- return {
1119
- domain: d.domain,
1120
- totalCards: total,
1121
- matureCards: mature,
1122
- avgStability: Math.round(avgStab * 100) / 100,
1123
- retentionRate: Math.round(retentionRate * 1e3) / 1e3,
1124
- suggestedMode
1125
- };
1126
- });
1427
+
1428
+ // src/kernel/observation/shell-hooks.ts
1429
+ function psSingleQuoted(value) {
1430
+ return `'${value.replace(/'/g, "''")}'`;
1431
+ }
1432
+ function generateZshHooks(monitorFile, sessionId) {
1433
+ return `
1434
+ # ZAM monitor hooks for session ${sessionId}
1435
+ export __ZAM_MONITOR_FILE="${monitorFile}"
1436
+ export __ZAM_MONITOR_SEQ=0
1437
+ export __ZAM_MONITOR_SESSION="${sessionId}"
1438
+
1439
+ __zam_ts() {
1440
+ if [[ -n "\${EPOCHREALTIME:-}" ]]; then
1441
+ local sec="\${EPOCHREALTIME%%.*}"
1442
+ local frac="\${EPOCHREALTIME##*.}"
1443
+ frac="\${frac:0:3}"
1444
+ printf '%s.%sZ' "$(date -u -r "$sec" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date -u '+%Y-%m-%dT%H:%M:%S')" "$frac"
1445
+ else
1446
+ date -u '+%Y-%m-%dT%H:%M:%SZ'
1447
+ fi
1127
1448
  }
1128
1449
 
1129
- // src/kernel/observation/analyzer.ts
1130
- function parseMonitorLog(jsonl) {
1131
- const events = [];
1132
- for (const line of jsonl.split("\n")) {
1133
- const trimmed = line.trim();
1134
- if (!trimmed) continue;
1135
- try {
1136
- events.push(JSON.parse(trimmed));
1137
- } catch {
1138
- }
1450
+ __zam_preexec() {
1451
+ (( __ZAM_MONITOR_SEQ++ ))
1452
+ local cmd="\${1//\\"/\\\\\\"}"
1453
+ local cwd="\${PWD//\\"/\\\\\\"}"
1454
+ local ts="$(__zam_ts)"
1455
+ printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
1456
+ "$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
1457
+ >> "$__ZAM_MONITOR_FILE"
1458
+ }
1459
+
1460
+ __zam_precmd() {
1461
+ local exit_code=$?
1462
+ [[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
1463
+ local ts="$(__zam_ts)"
1464
+ printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
1465
+ "$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
1466
+ >> "$__ZAM_MONITOR_FILE"
1467
+ }
1468
+
1469
+ autoload -Uz add-zsh-hook
1470
+ add-zsh-hook preexec __zam_preexec
1471
+ add-zsh-hook precmd __zam_precmd
1472
+
1473
+ echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
1474
+ `.trim();
1475
+ }
1476
+ function generateBashHooks(monitorFile, sessionId) {
1477
+ return `
1478
+ # ZAM monitor hooks for session ${sessionId}
1479
+ export __ZAM_MONITOR_FILE="${monitorFile}"
1480
+ export __ZAM_MONITOR_SEQ=0
1481
+ export __ZAM_MONITOR_SESSION="${sessionId}"
1482
+ export __ZAM_MONITOR_CMD_ACTIVE=0
1483
+
1484
+ __zam_ts() {
1485
+ date -u '+%Y-%m-%dT%H:%M:%SZ'
1486
+ }
1487
+
1488
+ __zam_debug_trap() {
1489
+ [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]] && return
1490
+ __ZAM_MONITOR_CMD_ACTIVE=1
1491
+ (( __ZAM_MONITOR_SEQ++ ))
1492
+ local cmd="\${BASH_COMMAND//\\"/\\\\\\"}"
1493
+ local cwd="\${PWD//\\"/\\\\\\"}"
1494
+ local ts="$(__zam_ts)"
1495
+ printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
1496
+ "$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
1497
+ >> "$__ZAM_MONITOR_FILE"
1498
+ }
1499
+
1500
+ __zam_prompt_cmd() {
1501
+ local exit_code=$?
1502
+ if [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]]; then
1503
+ __ZAM_MONITOR_CMD_ACTIVE=0
1504
+ local ts="$(__zam_ts)"
1505
+ printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
1506
+ "$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
1507
+ >> "$__ZAM_MONITOR_FILE"
1508
+ fi
1509
+ }
1510
+
1511
+ trap '__zam_debug_trap' DEBUG
1512
+ PROMPT_COMMAND="__zam_prompt_cmd;\${PROMPT_COMMAND:-}"
1513
+
1514
+ echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
1515
+ `.trim();
1516
+ }
1517
+ function generatePowerShellHooks(monitorFile, sessionId) {
1518
+ return `
1519
+ # ZAM monitor hooks for session ${sessionId}
1520
+ $global:__ZAM_MONITOR_FILE = ${psSingleQuoted(monitorFile)}
1521
+ $global:__ZAM_MONITOR_SEQ = 0
1522
+ $global:__ZAM_MONITOR_SESSION = ${psSingleQuoted(sessionId)}
1523
+ $global:__ZAM_MONITOR_SKIP_NEXT_PROMPT = $true
1524
+
1525
+ function global:__zam_write_monitor_event {
1526
+ param([hashtable]$Event)
1527
+ $json = $Event | ConvertTo-Json -Compress -Depth 4
1528
+ $utf8NoBom = New-Object System.Text.UTF8Encoding $false
1529
+ [System.IO.File]::AppendAllText($global:__ZAM_MONITOR_FILE, $json + [Environment]::NewLine, $utf8NoBom)
1530
+ }
1531
+
1532
+ function global:__zam_iso_utc {
1533
+ param([datetime]$Date)
1534
+ if ($Date -eq [datetime]::MinValue) {
1535
+ return (Get-Date).ToUniversalTime().ToString("o")
1139
1536
  }
1140
- return events;
1537
+ return $Date.ToUniversalTime().ToString("o")
1141
1538
  }
1142
- function pairCommands(events) {
1143
- const starts = /* @__PURE__ */ new Map();
1144
- const records = [];
1145
- for (const e of events) {
1146
- if (e.type === "command_start" && e.seq != null) {
1147
- const key = `${e.pid ?? 0}:${e.seq}`;
1148
- starts.set(key, e);
1149
- } else if (e.type === "command_end" && e.seq != null) {
1150
- const key = `${e.pid ?? 0}:${e.seq}`;
1151
- const start = starts.get(key);
1152
- if (start) {
1153
- const startMs = new Date(start.ts).getTime();
1154
- const endMs = new Date(e.ts).getTime();
1155
- records.push({
1156
- seq: e.seq,
1157
- pid: e.pid ?? 0,
1158
- command: start.command ?? "",
1159
- cwd: start.cwd ?? "",
1160
- startedAt: start.ts,
1161
- endedAt: e.ts,
1162
- durationMs: endMs - startMs,
1163
- exitCode: e.exit_code ?? null
1164
- });
1165
- starts.delete(key);
1166
- }
1539
+
1540
+ function global:__zam_update_last_history_id {
1541
+ $history = Get-History -Count 1
1542
+ if ($null -ne $history) {
1543
+ $global:__ZAM_MONITOR_LAST_HISTORY_ID = $history.Id
1544
+ } elseif ($null -eq $global:__ZAM_MONITOR_LAST_HISTORY_ID) {
1545
+ $global:__ZAM_MONITOR_LAST_HISTORY_ID = 0
1546
+ }
1547
+ }
1548
+
1549
+ function global:__zam_record_last_history {
1550
+ param(
1551
+ [bool]$Success,
1552
+ [object]$NativeExitCode
1553
+ )
1554
+
1555
+ $history = Get-History -Count 1
1556
+ if ($null -eq $history) { return }
1557
+ if ($history.Id -le $global:__ZAM_MONITOR_LAST_HISTORY_ID) { return }
1558
+
1559
+ $global:__ZAM_MONITOR_LAST_HISTORY_ID = $history.Id
1560
+ $global:__ZAM_MONITOR_SEQ += 1
1561
+
1562
+ $exitCode = 0
1563
+ if (-not $Success) {
1564
+ if ($NativeExitCode -is [int] -and $NativeExitCode -ne 0) {
1565
+ $exitCode = $NativeExitCode
1566
+ } else {
1567
+ $exitCode = 1
1167
1568
  }
1168
1569
  }
1169
- for (const [, start] of starts) {
1170
- records.push({
1171
- seq: start.seq ?? 0,
1172
- pid: start.pid ?? 0,
1173
- command: start.command ?? "",
1174
- cwd: start.cwd ?? "",
1175
- startedAt: start.ts,
1176
- endedAt: null,
1177
- durationMs: null,
1178
- exitCode: null
1179
- });
1570
+
1571
+ $cwd = (Get-Location).Path
1572
+ __zam_write_monitor_event @{
1573
+ type = "command_start"
1574
+ ts = (__zam_iso_utc $history.StartExecutionTime)
1575
+ command = $history.CommandLine
1576
+ cwd = $cwd
1577
+ seq = $global:__ZAM_MONITOR_SEQ
1578
+ pid = $PID
1579
+ }
1580
+ __zam_write_monitor_event @{
1581
+ type = "command_end"
1582
+ ts = (__zam_iso_utc $history.EndExecutionTime)
1583
+ exit_code = $exitCode
1584
+ seq = $global:__ZAM_MONITOR_SEQ
1585
+ pid = $PID
1180
1586
  }
1181
- records.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
1182
- return records;
1183
1587
  }
1184
- var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
1185
- var HELP_WINDOW_MS = 6e4;
1186
- function matchesToken(command, patterns) {
1187
- const lower = command.toLowerCase();
1188
- return patterns.some((p) => lower.includes(p.toLowerCase()));
1588
+
1589
+ if (-not (Test-Path function:\\__zam_previous_prompt) -and (Test-Path function:\\prompt)) {
1590
+ Set-Item -Path function:\\__zam_previous_prompt -Value (Get-Item function:\\prompt).ScriptBlock
1189
1591
  }
1190
- function isHelpCommand(command) {
1191
- const lower = command.toLowerCase();
1192
- return HELP_PATTERNS.some((p) => lower.includes(p));
1592
+ __zam_update_last_history_id
1593
+
1594
+ function global:prompt {
1595
+ $zamSuccess = $?
1596
+ $zamNativeExitCode = $global:LASTEXITCODE
1597
+
1598
+ if ($global:__ZAM_MONITOR_SKIP_NEXT_PROMPT) {
1599
+ __zam_update_last_history_id
1600
+ $global:__ZAM_MONITOR_SKIP_NEXT_PROMPT = $false
1601
+ } else {
1602
+ __zam_record_last_history -Success $zamSuccess -NativeExitCode $zamNativeExitCode
1603
+ }
1604
+
1605
+ if (Test-Path function:\\__zam_previous_prompt) {
1606
+ & (Get-Item function:\\__zam_previous_prompt).ScriptBlock
1607
+ } else {
1608
+ "PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "
1609
+ }
1193
1610
  }
1194
- function commandPrefix(command) {
1195
- return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
1611
+
1612
+ Write-Host "ZAM monitor active for session $global:__ZAM_MONITOR_SESSION"
1613
+ `.trim();
1196
1614
  }
1197
- function computeMedian(values) {
1198
- if (values.length === 0) return null;
1199
- const sorted = [...values].sort((a, b) => a - b);
1200
- const mid = Math.floor(sorted.length / 2);
1201
- return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1615
+ function generateZshUnhooks() {
1616
+ return `
1617
+ # Remove ZAM monitor hooks
1618
+ add-zsh-hook -d preexec __zam_preexec 2>/dev/null
1619
+ add-zsh-hook -d precmd __zam_precmd 2>/dev/null
1620
+ unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
1621
+ unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
1622
+ echo "ZAM monitor stopped."
1623
+ `.trim();
1202
1624
  }
1203
- function analyzeObservation(commands, tokenPatterns) {
1204
- const matchedSet = /* @__PURE__ */ new Set();
1205
- const ratings = [];
1206
- for (const tp of tokenPatterns) {
1207
- const matchIndices = [];
1208
- const matchedTexts = [];
1209
- for (let i = 0; i < commands.length; i++) {
1210
- if (matchesToken(commands[i].command, tp.patterns)) {
1211
- matchIndices.push(i);
1212
- matchedTexts.push(commands[i].command);
1213
- matchedSet.add(i);
1214
- }
1215
- }
1216
- if (matchIndices.length === 0) {
1217
- ratings.push({
1218
- tokenSlug: tp.slug,
1219
- rating: null,
1220
- confidence: "low",
1221
- evidence: {
1222
- matchedCommands: 0,
1223
- helpSeeking: false,
1224
- errorCount: 0,
1225
- selfCorrections: 0,
1226
- medianGapMs: null,
1227
- thinkingGapMs: null
1228
- },
1229
- matchedCommandTexts: []
1230
- });
1231
- continue;
1625
+ function generateBashUnhooks() {
1626
+ return `
1627
+ # Remove ZAM monitor hooks
1628
+ trap - DEBUG
1629
+ PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
1630
+ unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
1631
+ unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
1632
+ echo "ZAM monitor stopped."
1633
+ `.trim();
1634
+ }
1635
+ function generatePowerShellUnhooks() {
1636
+ return `
1637
+ # Remove ZAM monitor hooks
1638
+ if (Test-Path function:\\__zam_previous_prompt) {
1639
+ Set-Item -Path function:\\prompt -Value (Get-Item function:\\__zam_previous_prompt).ScriptBlock
1640
+ Remove-Item function:\\__zam_previous_prompt -Force -ErrorAction SilentlyContinue
1641
+ }
1642
+ Remove-Item function:\\__zam_write_monitor_event,function:\\__zam_iso_utc,function:\\__zam_update_last_history_id,function:\\__zam_record_last_history -ErrorAction SilentlyContinue
1643
+ Remove-Variable -Name __ZAM_MONITOR_FILE,__ZAM_MONITOR_SEQ,__ZAM_MONITOR_SESSION,__ZAM_MONITOR_LAST_HISTORY_ID,__ZAM_MONITOR_SKIP_NEXT_PROMPT -Scope Global -ErrorAction SilentlyContinue
1644
+ Write-Host "ZAM monitor stopped."
1645
+ `.trim();
1646
+ }
1647
+
1648
+ // src/kernel/observation/skill-discovery.ts
1649
+ function discoverSkills(sessionCommands, options = {}) {
1650
+ const minSessions = options.minSessions ?? 2;
1651
+ const minLen = options.minSequenceLength ?? 2;
1652
+ const maxLen = options.maxSequenceLength ?? 5;
1653
+ const existing = new Set(options.existingSkillSlugs ?? []);
1654
+ const sessionSequences = /* @__PURE__ */ new Map();
1655
+ for (const [sessionId, commands] of sessionCommands) {
1656
+ const sequences = extractSequences(commands, minLen, maxLen);
1657
+ if (sequences.length > 0) {
1658
+ sessionSequences.set(sessionId, sequences);
1232
1659
  }
1233
- let helpSeeking = false;
1234
- for (const mi of matchIndices) {
1235
- const matchTime = new Date(commands[mi].startedAt).getTime();
1236
- for (let j = 0; j < commands.length; j++) {
1237
- if (j === mi) continue;
1238
- const cmdTime = new Date(commands[j].startedAt).getTime();
1239
- if (cmdTime >= matchTime - HELP_WINDOW_MS && cmdTime < matchTime) {
1240
- if (isHelpCommand(commands[j].command)) {
1241
- helpSeeking = true;
1242
- break;
1243
- }
1244
- }
1660
+ }
1661
+ const sequenceIndex = /* @__PURE__ */ new Map();
1662
+ for (const [, sequences] of sessionSequences) {
1663
+ const seen = /* @__PURE__ */ new Set();
1664
+ for (const seq of sequences) {
1665
+ const key = seq.join(" \u2192 ");
1666
+ if (seen.has(key)) continue;
1667
+ seen.add(key);
1668
+ const entry = sequenceIndex.get(key);
1669
+ if (entry) {
1670
+ entry.sessionCount++;
1671
+ entry.totalOccurrences++;
1672
+ } else {
1673
+ sequenceIndex.set(key, {
1674
+ steps: seq,
1675
+ sessionCount: 1,
1676
+ totalOccurrences: 1,
1677
+ examples: []
1678
+ });
1245
1679
  }
1246
- if (helpSeeking) break;
1247
1680
  }
1248
- let errorCount = 0;
1249
- for (const mi of matchIndices) {
1250
- if (commands[mi].exitCode != null && commands[mi].exitCode !== 0) {
1251
- errorCount++;
1681
+ }
1682
+ const lastSessionId = [...sessionCommands.keys()].pop();
1683
+ if (lastSessionId) {
1684
+ const lastCommands = sessionCommands.get(lastSessionId);
1685
+ for (const [_key, entry] of sequenceIndex) {
1686
+ if (entry.sessionCount >= minSessions) {
1687
+ entry.examples = findExamplesForSequence(lastCommands, entry.steps);
1252
1688
  }
1253
1689
  }
1254
- let selfCorrections = 0;
1255
- const prefixGroups = /* @__PURE__ */ new Map();
1256
- for (const mi of matchIndices) {
1257
- const prefix = commandPrefix(commands[mi].command);
1258
- prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
1259
- }
1260
- for (const count of prefixGroups.values()) {
1261
- if (count > 1) selfCorrections += count - 1;
1690
+ }
1691
+ const candidates = [...sequenceIndex.values()].filter(
1692
+ (s) => s.sessionCount >= minSessions
1693
+ );
1694
+ const pruned = removeSubsequences(candidates);
1695
+ const proposals = [];
1696
+ for (const seq of pruned) {
1697
+ const slug = generateSlug(seq.steps);
1698
+ if (existing.has(slug)) continue;
1699
+ proposals.push({
1700
+ slug,
1701
+ description: describeSequence(seq.steps),
1702
+ steps: seq.steps,
1703
+ sessionCount: seq.sessionCount,
1704
+ confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
1705
+ examples: seq.examples
1706
+ });
1707
+ }
1708
+ const confidenceOrder = { high: 0, medium: 1, low: 2 };
1709
+ proposals.sort((a, b) => {
1710
+ const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
1711
+ if (confDiff !== 0) return confDiff;
1712
+ return b.sessionCount - a.sessionCount;
1713
+ });
1714
+ return proposals;
1715
+ }
1716
+ function normalizeCommand(command) {
1717
+ const parts = command.trim().split(/\s+/);
1718
+ const multiWord = ["docker compose", "npm run", "npx", "git"];
1719
+ const lower = command.toLowerCase();
1720
+ for (const mw of multiWord) {
1721
+ if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
1722
+ return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
1262
1723
  }
1263
- const gaps = [];
1264
- for (let k = 1; k < matchIndices.length; k++) {
1265
- const prev = commands[matchIndices[k - 1]];
1266
- const curr = commands[matchIndices[k]];
1267
- if (prev.endedAt) {
1268
- const gap = new Date(curr.startedAt).getTime() - new Date(prev.endedAt).getTime();
1269
- if (gap >= 0) gaps.push(gap);
1724
+ }
1725
+ return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
1726
+ }
1727
+ function extractSequences(commands, minLen, maxLen) {
1728
+ const filtered = commands.filter((c) => {
1729
+ const lower = c.command.toLowerCase().trim();
1730
+ return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
1731
+ });
1732
+ const normalized = filtered.map((c) => normalizeCommand(c.command));
1733
+ const sequences = [];
1734
+ for (let len = minLen; len <= maxLen; len++) {
1735
+ for (let i = 0; i <= normalized.length - len; i++) {
1736
+ const seq = normalized.slice(i, i + len);
1737
+ if (new Set(seq).size >= 2) {
1738
+ sequences.push(seq);
1270
1739
  }
1271
1740
  }
1272
- let thinkingGapMs = null;
1273
- const firstMatchIdx = matchIndices[0];
1274
- if (firstMatchIdx > 0) {
1275
- const prev = commands[firstMatchIdx - 1];
1276
- if (prev.endedAt) {
1277
- thinkingGapMs = new Date(commands[firstMatchIdx].startedAt).getTime() - new Date(prev.endedAt).getTime();
1741
+ }
1742
+ return sequences;
1743
+ }
1744
+ function findExamplesForSequence(commands, steps) {
1745
+ const normalized = commands.map((c) => ({
1746
+ norm: normalizeCommand(c.command),
1747
+ full: c.command
1748
+ }));
1749
+ for (let i = 0; i <= normalized.length - steps.length; i++) {
1750
+ let match = true;
1751
+ for (let j = 0; j < steps.length; j++) {
1752
+ if (normalized[i + j].norm !== steps[j]) {
1753
+ match = false;
1754
+ break;
1278
1755
  }
1279
1756
  }
1280
- const medianGapMs = computeMedian(gaps);
1281
- const rating = inferRating({
1282
- helpSeeking,
1283
- errorCount,
1284
- selfCorrections,
1285
- medianGapMs,
1286
- thinkingGapMs,
1287
- matchedCommands: matchIndices.length
1757
+ if (match) {
1758
+ return normalized.slice(i, i + steps.length).map((n) => n.full);
1759
+ }
1760
+ }
1761
+ return [];
1762
+ }
1763
+ function removeSubsequences(candidates) {
1764
+ const sorted = [...candidates].sort(
1765
+ (a, b) => b.steps.length - a.steps.length
1766
+ );
1767
+ const result = [];
1768
+ for (const candidate of sorted) {
1769
+ const key = candidate.steps.join(" \u2192 ");
1770
+ const isSubsequence = result.some((longer) => {
1771
+ const longerKey = longer.steps.join(" \u2192 ");
1772
+ return longerKey.includes(key) && longerKey !== key;
1288
1773
  });
1289
- const confidence = matchIndices.length >= 3 ? "high" : matchIndices.length >= 2 ? "medium" : "low";
1290
- ratings.push({
1291
- tokenSlug: tp.slug,
1292
- rating,
1293
- confidence,
1294
- evidence: {
1295
- matchedCommands: matchIndices.length,
1296
- helpSeeking,
1297
- errorCount,
1298
- selfCorrections,
1299
- medianGapMs,
1300
- thinkingGapMs
1301
- },
1302
- matchedCommandTexts: matchedTexts
1774
+ if (!isSubsequence) {
1775
+ result.push(candidate);
1776
+ }
1777
+ }
1778
+ return result;
1779
+ }
1780
+ function generateSlug(steps) {
1781
+ return steps.map((s) => {
1782
+ const parts = s.split(/\s+/);
1783
+ return parts[parts.length - 1];
1784
+ }).join("-");
1785
+ }
1786
+ function describeSequence(steps) {
1787
+ return `Recurring pattern: ${steps.join(" \u2192 ")}`;
1788
+ }
1789
+
1790
+ // src/kernel/scheduler/blocker.ts
1791
+ function cascadeBlock(db, userId, tokenSlug) {
1792
+ const token = getTokenBySlug(db, tokenSlug);
1793
+ if (!token) {
1794
+ throw new Error(`Unknown token slug: ${tokenSlug}`);
1795
+ }
1796
+ ensureCard(db, token.id, userId);
1797
+ db.prepare(
1798
+ "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
1799
+ ).run(token.id, userId);
1800
+ const prereqs = getPrerequisites(db, token.id);
1801
+ const surfaced = [];
1802
+ for (const prereq of prereqs) {
1803
+ const card = ensureCard(db, prereq.requires_id, userId);
1804
+ if (card.blocked === 1) {
1805
+ const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
1806
+ if (prereqOfPrereq.n === 0) {
1807
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1808
+ db.prepare(
1809
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
1810
+ ).run(now, prereq.requires_id, userId);
1811
+ }
1812
+ }
1813
+ surfaced.push({
1814
+ slug: prereq.slug,
1815
+ concept: prereq.concept,
1816
+ bloomLevel: prereq.bloom_level
1303
1817
  });
1304
1818
  }
1305
- const unmatchedCommands = [];
1306
- for (let i = 0; i < commands.length; i++) {
1307
- if (!matchedSet.has(i) && !isHelpCommand(commands[i].command)) {
1308
- unmatchedCommands.push(commands[i].command);
1819
+ return {
1820
+ blockedSlug: tokenSlug,
1821
+ prerequisites: surfaced
1822
+ };
1823
+ }
1824
+ function unblockReady(db, userId) {
1825
+ const blockedCards = db.prepare(
1826
+ `SELECT c.token_id, t.slug, t.concept
1827
+ FROM cards c
1828
+ JOIN tokens t ON t.id = c.token_id
1829
+ WHERE c.user_id = ? AND c.blocked = 1`
1830
+ ).all(userId);
1831
+ const unblocked = [];
1832
+ for (const card of blockedCards) {
1833
+ const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
1834
+ const metPrereqs = db.prepare(
1835
+ `SELECT COUNT(*) as n FROM cards c
1836
+ JOIN prerequisites p ON p.requires_id = c.token_id
1837
+ WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
1838
+ ).get(card.token_id, userId);
1839
+ if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
1840
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1841
+ db.prepare(
1842
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
1843
+ ).run(now, card.token_id, userId);
1844
+ unblocked.push({ slug: card.slug, concept: card.concept });
1309
1845
  }
1310
1846
  }
1311
- let timeSpan = null;
1312
- if (commands.length > 0) {
1313
- const first = commands[0];
1314
- const last = commands[commands.length - 1];
1315
- const endTs = last.endedAt ?? last.startedAt;
1316
- timeSpan = {
1317
- start: first.startedAt,
1318
- end: endTs,
1319
- durationMs: new Date(endTs).getTime() - new Date(first.startedAt).getTime()
1847
+ return { unblocked };
1848
+ }
1849
+
1850
+ // src/kernel/recall/evaluator.ts
1851
+ import { ulid as ulid6 } from "ulid";
1852
+
1853
+ // src/kernel/scheduler/fsrs.ts
1854
+ var DEFAULT_W = [
1855
+ 0.4072,
1856
+ 1.1829,
1857
+ 3.1262,
1858
+ 15.4722,
1859
+ // w0–w3: initial stability per rating
1860
+ 7.2102,
1861
+ 0.5316,
1862
+ 1.0651,
1863
+ // w4–w6: difficulty
1864
+ 92e-4,
1865
+ 1.5988,
1866
+ 0.1176,
1867
+ 1.0014,
1868
+ // w7–w10: stability after forgetting
1869
+ 2.0032,
1870
+ 0.0266,
1871
+ 0.3077,
1872
+ 0.15,
1873
+ // w11–w14: stability increase
1874
+ 0,
1875
+ 2.7849,
1876
+ 0.3477,
1877
+ 0.6831
1878
+ // w15–w18: additional parameters
1879
+ ];
1880
+ var DEFAULT_REQUEST_RETENTION = 0.9;
1881
+ function clamp(value, lo, hi) {
1882
+ return Math.min(hi, Math.max(lo, value));
1883
+ }
1884
+ function daysBetween(a, b) {
1885
+ return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
1886
+ }
1887
+ function initialStability(w, rating) {
1888
+ return w[rating - 1];
1889
+ }
1890
+ function initialDifficulty(w, rating) {
1891
+ return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
1892
+ }
1893
+ function nextDifficulty(w, d, rating) {
1894
+ const d0ForGood = initialDifficulty(w, 3);
1895
+ const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
1896
+ return clamp(updated, 1, 10);
1897
+ }
1898
+ function retrievability(elapsed, stability) {
1899
+ if (stability <= 0) return 0;
1900
+ return (1 + elapsed / (9 * stability)) ** -1;
1901
+ }
1902
+ function stabilityAfterSuccess(w, s, d, r, rating) {
1903
+ const hardPenalty = rating === 2 ? w[15] : 1;
1904
+ const easyBonus = rating === 4 ? w[16] : 1;
1905
+ const inner = Math.exp(w[8]) * (11 - d) * s ** -w[9] * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
1906
+ return s * (inner + 1);
1907
+ }
1908
+ function stabilityAfterForgetting(w, s, d, r) {
1909
+ return w[11] * d ** -w[12] * ((s + 1) ** w[13] - 1) * Math.exp(w[14] * (1 - r));
1910
+ }
1911
+ function nextInterval(stability, requestRetention) {
1912
+ const interval = 9 * stability * (1 / requestRetention - 1);
1913
+ return Math.max(1, Math.round(interval));
1914
+ }
1915
+ function createFSRS(params) {
1916
+ const resolvedParams = {
1917
+ w: params?.w ?? [...DEFAULT_W],
1918
+ requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
1919
+ };
1920
+ function schedule(card, rating, now) {
1921
+ const reviewTime = now ?? /* @__PURE__ */ new Date();
1922
+ const w = resolvedParams.w;
1923
+ const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
1924
+ if (card.state === "new") {
1925
+ const s = initialStability(w, rating);
1926
+ const d = initialDifficulty(w, rating);
1927
+ const interval2 = nextInterval(s, resolvedParams.requestRetention);
1928
+ const dueAt2 = new Date(reviewTime);
1929
+ dueAt2.setDate(dueAt2.getDate() + interval2);
1930
+ return {
1931
+ stability: s,
1932
+ difficulty: d,
1933
+ elapsedDays: 0,
1934
+ scheduledDays: interval2,
1935
+ reps: rating >= 2 ? 1 : 0,
1936
+ lapses: rating === 1 ? 1 : 0,
1937
+ state: "learning",
1938
+ dueAt: dueAt2,
1939
+ lastReviewAt: reviewTime
1940
+ };
1941
+ }
1942
+ const r = retrievability(elapsed, card.stability);
1943
+ let newStability;
1944
+ let newDifficulty;
1945
+ let newReps;
1946
+ let newLapses;
1947
+ let newState;
1948
+ if (rating === 1) {
1949
+ newStability = stabilityAfterForgetting(
1950
+ w,
1951
+ card.stability,
1952
+ card.difficulty,
1953
+ r
1954
+ );
1955
+ newDifficulty = nextDifficulty(w, card.difficulty, rating);
1956
+ newReps = 0;
1957
+ newLapses = card.lapses + 1;
1958
+ newState = "relearning";
1959
+ } else {
1960
+ newStability = stabilityAfterSuccess(
1961
+ w,
1962
+ card.stability,
1963
+ card.difficulty,
1964
+ r,
1965
+ rating
1966
+ );
1967
+ newDifficulty = nextDifficulty(w, card.difficulty, rating);
1968
+ newReps = card.reps + 1;
1969
+ newLapses = card.lapses;
1970
+ newState = "review";
1971
+ }
1972
+ const interval = nextInterval(
1973
+ newStability,
1974
+ resolvedParams.requestRetention
1975
+ );
1976
+ const dueAt = new Date(reviewTime);
1977
+ dueAt.setDate(dueAt.getDate() + interval);
1978
+ return {
1979
+ stability: newStability,
1980
+ difficulty: newDifficulty,
1981
+ elapsedDays: elapsed,
1982
+ scheduledDays: interval,
1983
+ reps: newReps,
1984
+ lapses: newLapses,
1985
+ state: newState,
1986
+ dueAt,
1987
+ lastReviewAt: reviewTime
1320
1988
  };
1321
1989
  }
1322
- return { ratings, unmatchedCommands, timeSpan };
1990
+ return {
1991
+ schedule,
1992
+ params: Object.freeze(resolvedParams)
1993
+ };
1323
1994
  }
1324
- function inferRating(signals) {
1325
- const { helpSeeking, errorCount, selfCorrections, medianGapMs, thinkingGapMs } = signals;
1326
- let negatives = 0;
1327
- if (helpSeeking) negatives += 2;
1328
- if (errorCount >= 3) negatives += 3;
1329
- else if (errorCount >= 1) negatives += 1;
1330
- if (selfCorrections >= 2) negatives += 2;
1331
- else if (selfCorrections >= 1) negatives += 1;
1332
- if (medianGapMs != null && medianGapMs > 3e4) negatives += 2;
1333
- else if (medianGapMs != null && medianGapMs > 1e4) negatives += 1;
1334
- if (thinkingGapMs != null && thinkingGapMs > 3e4) negatives += 1;
1335
- if (negatives >= 5) return 1;
1336
- if (negatives >= 3) return 2;
1337
- if (negatives >= 1) return 3;
1338
- return 4;
1995
+
1996
+ // src/kernel/recall/evaluator.ts
1997
+ function evaluateRating(db, input) {
1998
+ const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
1999
+ if (!card) {
2000
+ throw new Error(`Card not found: ${input.cardId}`);
2001
+ }
2002
+ const now = /* @__PURE__ */ new Date();
2003
+ const fsrs = createFSRS();
2004
+ const schedulingCard = {
2005
+ stability: card.stability,
2006
+ difficulty: card.difficulty,
2007
+ elapsedDays: card.elapsed_days,
2008
+ scheduledDays: card.scheduled_days,
2009
+ reps: card.reps,
2010
+ lapses: card.lapses,
2011
+ state: card.state,
2012
+ dueAt: new Date(card.due_at),
2013
+ lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
2014
+ };
2015
+ const updated = fsrs.schedule(schedulingCard, input.rating, now);
2016
+ updateCard(db, input.cardId, {
2017
+ stability: updated.stability,
2018
+ difficulty: updated.difficulty,
2019
+ elapsed_days: updated.elapsedDays,
2020
+ scheduled_days: updated.scheduledDays,
2021
+ reps: updated.reps,
2022
+ lapses: updated.lapses,
2023
+ state: updated.state,
2024
+ due_at: updated.dueAt.toISOString(),
2025
+ last_review_at: now.toISOString()
2026
+ });
2027
+ db.prepare(
2028
+ `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
2029
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
2030
+ ).run(
2031
+ ulid6(),
2032
+ input.cardId,
2033
+ input.tokenId,
2034
+ input.userId,
2035
+ input.rating,
2036
+ input.responseTimeMs ?? null,
2037
+ now.toISOString(),
2038
+ card.due_at,
2039
+ input.sessionId ?? null
2040
+ );
2041
+ return {
2042
+ nextDueAt: updated.dueAt.toISOString(),
2043
+ stability: updated.stability,
2044
+ difficulty: updated.difficulty,
2045
+ state: updated.state,
2046
+ scheduledDays: updated.scheduledDays,
2047
+ reps: updated.reps,
2048
+ lapses: updated.lapses
2049
+ };
1339
2050
  }
1340
2051
 
1341
- // src/kernel/observation/monitor-io.ts
1342
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, appendFileSync, statSync } from "fs";
1343
- import { homedir as homedir2 } from "os";
1344
- import { join as join2 } from "path";
1345
- var MONITOR_DIR = join2(homedir2(), ".zam", "monitor");
1346
- function getMonitorDir() {
1347
- return MONITOR_DIR;
1348
- }
1349
- function getMonitorPath(sessionId) {
1350
- return join2(MONITOR_DIR, `${sessionId}.jsonl`);
1351
- }
1352
- function ensureMonitorDir() {
1353
- if (!existsSync2(MONITOR_DIR)) {
1354
- mkdirSync2(MONITOR_DIR, { recursive: true, mode: 448 });
2052
+ // src/kernel/recall/actions.ts
2053
+ function getReviewTarget(db, cardId, userId) {
2054
+ const card = getCardById(db, cardId);
2055
+ if (!card) {
2056
+ throw new Error(`Card not found: ${cardId}`);
1355
2057
  }
1356
- }
1357
- function writeMonitorEvent(sessionId, event) {
1358
- ensureMonitorDir();
1359
- const path = getMonitorPath(sessionId);
1360
- appendFileSync(path, JSON.stringify(event) + "\n");
1361
- }
1362
- function readMonitorLog(sessionId) {
1363
- const path = getMonitorPath(sessionId);
1364
- if (!existsSync2(path)) {
1365
- return [];
2058
+ if (card.user_id !== userId) {
2059
+ throw new Error(`Card ${cardId} does not belong to user ${userId}`);
1366
2060
  }
1367
- const content = readFileSync(path, "utf-8");
1368
- return parseMonitorLog(content);
1369
- }
1370
- function monitorLogExists(sessionId) {
1371
- return existsSync2(getMonitorPath(sessionId));
1372
- }
1373
- function getMonitorLogStats(sessionId) {
1374
- const path = getMonitorPath(sessionId);
1375
- if (!existsSync2(path)) {
1376
- return { exists: false, sizeBytes: 0, lineCount: 0 };
2061
+ const token = getTokenById(db, card.token_id);
2062
+ if (!token) {
2063
+ throw new Error(`Token not found for card ${cardId}`);
1377
2064
  }
1378
- const stat = statSync(path);
1379
- const content = readFileSync(path, "utf-8");
1380
- const lineCount = content.split("\n").filter((l) => l.trim()).length;
1381
- return { exists: true, sizeBytes: stat.size, lineCount };
1382
- }
1383
-
1384
- // src/kernel/observation/shell-hooks.ts
1385
- function generateZshHooks(monitorFile, sessionId) {
1386
- return `
1387
- # ZAM monitor hooks for session ${sessionId}
1388
- export __ZAM_MONITOR_FILE="${monitorFile}"
1389
- export __ZAM_MONITOR_SEQ=0
1390
- export __ZAM_MONITOR_SESSION="${sessionId}"
1391
-
1392
- __zam_ts() {
1393
- if [[ -n "\${EPOCHREALTIME:-}" ]]; then
1394
- local sec="\${EPOCHREALTIME%%.*}"
1395
- local frac="\${EPOCHREALTIME##*.}"
1396
- frac="\${frac:0:3}"
1397
- printf '%s.%sZ' "$(date -u -r "$sec" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || date -u '+%Y-%m-%dT%H:%M:%S')" "$frac"
1398
- else
1399
- date -u '+%Y-%m-%dT%H:%M:%SZ'
1400
- fi
1401
- }
1402
-
1403
- __zam_preexec() {
1404
- (( __ZAM_MONITOR_SEQ++ ))
1405
- local cmd="\${1//\\"/\\\\\\"}"
1406
- local cwd="\${PWD//\\"/\\\\\\"}"
1407
- local ts="$(__zam_ts)"
1408
- printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
1409
- "$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
1410
- >> "$__ZAM_MONITOR_FILE"
1411
- }
1412
-
1413
- __zam_precmd() {
1414
- local exit_code=$?
1415
- [[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
1416
- local ts="$(__zam_ts)"
1417
- printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
1418
- "$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
1419
- >> "$__ZAM_MONITOR_FILE"
1420
- }
1421
-
1422
- autoload -Uz add-zsh-hook
1423
- add-zsh-hook preexec __zam_preexec
1424
- add-zsh-hook precmd __zam_precmd
1425
-
1426
- echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
1427
- `.trim();
2065
+ return { cardId: card.id, token };
1428
2066
  }
1429
- function generateBashHooks(monitorFile, sessionId) {
1430
- return `
1431
- # ZAM monitor hooks for session ${sessionId}
1432
- export __ZAM_MONITOR_FILE="${monitorFile}"
1433
- export __ZAM_MONITOR_SEQ=0
1434
- export __ZAM_MONITOR_SESSION="${sessionId}"
1435
- export __ZAM_MONITOR_CMD_ACTIVE=0
1436
-
1437
- __zam_ts() {
1438
- date -u '+%Y-%m-%dT%H:%M:%SZ'
2067
+ function executeReviewAction(db, input) {
2068
+ const target = getReviewTarget(db, input.cardId, input.userId);
2069
+ switch (input.action) {
2070
+ case "rate": {
2071
+ if (input.rating == null) {
2072
+ throw new Error("rating is required for action=rate");
2073
+ }
2074
+ const evaluation = evaluateRating(db, {
2075
+ cardId: target.cardId,
2076
+ tokenId: target.token.id,
2077
+ userId: input.userId,
2078
+ rating: input.rating
2079
+ });
2080
+ let blocked;
2081
+ if (input.rating === 1) {
2082
+ const prereqs = getPrerequisites(db, target.token.id);
2083
+ if (prereqs.length > 0) {
2084
+ blocked = cascadeBlock(db, input.userId, target.token.slug);
2085
+ }
2086
+ }
2087
+ return {
2088
+ action: input.action,
2089
+ token: target.token,
2090
+ evaluation,
2091
+ blocked
2092
+ };
2093
+ }
2094
+ case "skip":
2095
+ return { action: input.action, token: target.token, skipped: true };
2096
+ case "stop":
2097
+ return { action: input.action, token: target.token, stopped: true };
2098
+ case "edit-token": {
2099
+ const updatedToken = updateToken(
2100
+ db,
2101
+ target.token.slug,
2102
+ input.tokenUpdates ?? {}
2103
+ );
2104
+ return {
2105
+ action: input.action,
2106
+ token: target.token,
2107
+ updatedToken
2108
+ };
2109
+ }
2110
+ case "deprecate-token": {
2111
+ const updatedToken = deprecateToken(db, target.token.slug);
2112
+ return {
2113
+ action: input.action,
2114
+ token: target.token,
2115
+ updatedToken
2116
+ };
2117
+ }
2118
+ case "delete-token": {
2119
+ const deletedToken = deleteToken(db, target.token.slug);
2120
+ return {
2121
+ action: input.action,
2122
+ token: target.token,
2123
+ deletedToken
2124
+ };
2125
+ }
2126
+ case "delete-card": {
2127
+ const deletedCard = deleteCardForUser(db, target.token.id, input.userId);
2128
+ return {
2129
+ action: input.action,
2130
+ token: target.token,
2131
+ deletedCard
2132
+ };
2133
+ }
2134
+ default: {
2135
+ const exhaustive = input.action;
2136
+ throw new Error(`Unsupported review action: ${exhaustive}`);
2137
+ }
2138
+ }
1439
2139
  }
1440
2140
 
1441
- __zam_debug_trap() {
1442
- [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]] && return
1443
- __ZAM_MONITOR_CMD_ACTIVE=1
1444
- (( __ZAM_MONITOR_SEQ++ ))
1445
- local cmd="\${BASH_COMMAND//\\"/\\\\\\"}"
1446
- local cwd="\${PWD//\\"/\\\\\\"}"
1447
- local ts="$(__zam_ts)"
1448
- printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
1449
- "$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
1450
- >> "$__ZAM_MONITOR_FILE"
2141
+ // src/kernel/recall/prompter.ts
2142
+ var BLOOM_VERBS = {
2143
+ 1: "Remember",
2144
+ 2: "Understand",
2145
+ 3: "Apply",
2146
+ 4: "Analyze",
2147
+ 5: "Synthesize"
2148
+ };
2149
+ function formatSlugForCue(slug) {
2150
+ return slug.replace(/[-_]/g, " ");
2151
+ }
2152
+ var BLOOM_CUES = {
2153
+ 1: (slug) => `Recall the definition and core concept of: ${formatSlugForCue(slug)}`,
2154
+ 2: (slug) => `Explain the concept and how ${formatSlugForCue(slug)} works.`,
2155
+ 3: (slug) => `Describe how or where you would apply the concept of ${formatSlugForCue(slug)}.`,
2156
+ 4: (slug) => `Analyze the trade-offs, advantages, or alternatives of ${formatSlugForCue(slug)}.`,
2157
+ 5: (slug) => `How would you design a solution using the concept of ${formatSlugForCue(slug)}?`
2158
+ };
2159
+ function generateConceptFreeCue(bloomLevel, slug, _domain) {
2160
+ const bloom = bloomLevel >= 1 && bloomLevel <= 5 ? bloomLevel : 1;
2161
+ return BLOOM_CUES[bloom](slug);
1451
2162
  }
1452
-
1453
- __zam_prompt_cmd() {
1454
- local exit_code=$?
1455
- if [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]]; then
1456
- __ZAM_MONITOR_CMD_ACTIVE=0
1457
- local ts="$(__zam_ts)"
1458
- printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
1459
- "$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
1460
- >> "$__ZAM_MONITOR_FILE"
1461
- fi
2163
+ function generatePrompt(input) {
2164
+ const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
2165
+ const question = input.question?.trim() ? input.question.trim() : generateConceptFreeCue(bloom, input.slug, input.domain);
2166
+ return {
2167
+ cardId: input.cardId,
2168
+ tokenId: input.tokenId,
2169
+ slug: input.slug,
2170
+ question,
2171
+ concept: input.concept,
2172
+ domain: input.domain,
2173
+ bloomLevel: bloom,
2174
+ bloomVerb: BLOOM_VERBS[bloom],
2175
+ hints: [],
2176
+ sourceLink: input.sourceLink ?? null
2177
+ };
1462
2178
  }
1463
2179
 
1464
- trap '__zam_debug_trap' DEBUG
1465
- PROMPT_COMMAND="__zam_prompt_cmd;\${PROMPT_COMMAND:-}"
1466
-
1467
- echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
1468
- `.trim();
2180
+ // src/kernel/recall/reference-resolver.ts
2181
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
2182
+ import { dirname as dirname3, join as join5, resolve } from "path";
2183
+ var DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6e3;
2184
+ function htmlToText(html) {
2185
+ let content = html;
2186
+ const bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(html);
2187
+ if (bodyMatch) {
2188
+ content = bodyMatch[1];
2189
+ }
2190
+ content = content.replace(/<(script|style|head)[^>]*>([\s\S]*?)<\/\1>/gi, "");
2191
+ content = content.replace(
2192
+ /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi,
2193
+ "\n\n# $1\n"
2194
+ );
2195
+ content = content.replace(/<(p|div|li)[^>]*>/gi, "\n");
2196
+ content = content.replace(/<\/(p|div|li)>/gi, "\n");
2197
+ content = content.replace(/<br\s*\/?>/gi, "\n");
2198
+ content = content.replace(/<[^>]+>/g, "");
2199
+ content = content.replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
2200
+ content = content.replace(/\n{3,}/g, "\n\n").trim();
2201
+ return content;
2202
+ }
2203
+ function extractLines(content, anchor) {
2204
+ const lines = content.split(/\r?\n/);
2205
+ const match = /#L(\d+)(?:-L(\d+))?$/i.exec(anchor);
2206
+ if (!match) return content;
2207
+ const start = Number.parseInt(match[1], 10) - 1;
2208
+ const end = match[2] ? Number.parseInt(match[2], 10) - 1 : start;
2209
+ if (start < 0 || start >= lines.length) return content;
2210
+ const slice = lines.slice(start, Math.min(end + 1, lines.length));
2211
+ return slice.join("\n");
2212
+ }
2213
+ async function resolveReference(sourceLink) {
2214
+ const cleaned = sourceLink.trim();
2215
+ if (cleaned.startsWith("search://")) {
2216
+ try {
2217
+ const url = new URL(cleaned);
2218
+ const query = url.searchParams.get("q") || "";
2219
+ return {
2220
+ sourceType: "dynamic_search",
2221
+ content: `QUERY_DIRECTIVE: Run web search for "${query}"`,
2222
+ url: cleaned
2223
+ };
2224
+ } catch {
2225
+ const query = cleaned.replace(/^search:\/\/(\??q=)?/, "");
2226
+ return {
2227
+ sourceType: "dynamic_search",
2228
+ content: `QUERY_DIRECTIVE: Run web search for "${decodeURIComponent(query)}"`,
2229
+ url: cleaned
2230
+ };
2231
+ }
2232
+ }
2233
+ if (cleaned.startsWith("http://") || cleaned.startsWith("https://")) {
2234
+ const gitHubMatch = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/i.exec(
2235
+ cleaned
2236
+ );
2237
+ if (gitHubMatch) {
2238
+ const [_, owner, repo, branch, fullPathWithAnchor] = gitHubMatch;
2239
+ const anchorIndex2 = fullPathWithAnchor.indexOf("#");
2240
+ const filePath = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(0, anchorIndex2) : fullPathWithAnchor;
2241
+ const anchor2 = anchorIndex2 !== -1 ? fullPathWithAnchor.slice(anchorIndex2) : "";
2242
+ const parentDir = dirname3(process.cwd());
2243
+ const localRepoPath = join5(parentDir, repo);
2244
+ const localFilePath = join5(localRepoPath, filePath);
2245
+ if (existsSync5(localFilePath)) {
2246
+ try {
2247
+ let fileContent = readFileSync5(localFilePath, "utf-8");
2248
+ if (anchor2) {
2249
+ fileContent = extractLines(fileContent, anchor2);
2250
+ }
2251
+ return {
2252
+ sourceType: "local",
2253
+ content: fileContent,
2254
+ filePath: localFilePath
2255
+ };
2256
+ } catch (_e) {
2257
+ }
2258
+ }
2259
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filePath}`;
2260
+ try {
2261
+ const response = await fetch(rawUrl);
2262
+ if (response.ok) {
2263
+ let rawText = await response.text();
2264
+ if (anchor2) {
2265
+ rawText = extractLines(rawText, anchor2);
2266
+ }
2267
+ return {
2268
+ sourceType: "remote_web",
2269
+ content: rawText,
2270
+ url: cleaned
2271
+ };
2272
+ }
2273
+ } catch (_e) {
2274
+ }
2275
+ }
2276
+ try {
2277
+ const response = await fetch(cleaned);
2278
+ if (response.ok) {
2279
+ const text = await response.text();
2280
+ const cleanText = htmlToText(text);
2281
+ return {
2282
+ sourceType: "remote_web",
2283
+ content: cleanText,
2284
+ url: cleaned
2285
+ };
2286
+ }
2287
+ throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
2288
+ } catch (err) {
2289
+ return {
2290
+ sourceType: "remote_web",
2291
+ content: `Error fetching URL reference: ${err.message}
2292
+ Link: ${cleaned}`,
2293
+ url: cleaned
2294
+ };
2295
+ }
2296
+ }
2297
+ const anchorIndex = cleaned.indexOf("#");
2298
+ const relativePath = anchorIndex !== -1 ? cleaned.slice(0, anchorIndex) : cleaned;
2299
+ const anchor = anchorIndex !== -1 ? cleaned.slice(anchorIndex) : "";
2300
+ const absolutePath = resolve(process.cwd(), relativePath);
2301
+ if (existsSync5(absolutePath)) {
2302
+ try {
2303
+ let fileContent = readFileSync5(absolutePath, "utf-8");
2304
+ if (anchor) {
2305
+ fileContent = extractLines(fileContent, anchor);
2306
+ }
2307
+ return {
2308
+ sourceType: "local",
2309
+ content: fileContent,
2310
+ filePath: absolutePath
2311
+ };
2312
+ } catch (_e) {
2313
+ }
2314
+ }
2315
+ return {
2316
+ sourceType: "local",
2317
+ content: `Local reference file not found or unreadable.
2318
+ Reference: ${cleaned}`,
2319
+ filePath: absolutePath
2320
+ };
1469
2321
  }
1470
- function generateZshUnhooks() {
1471
- return `
1472
- # Remove ZAM monitor hooks
1473
- add-zsh-hook -d preexec __zam_preexec 2>/dev/null
1474
- add-zsh-hook -d precmd __zam_precmd 2>/dev/null
1475
- unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
1476
- unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
1477
- echo "ZAM monitor stopped."
1478
- `.trim();
2322
+ async function resolveReviewContext(sourceLink, opts = {}) {
2323
+ const cleaned = sourceLink?.trim();
2324
+ if (!cleaned) return null;
2325
+ const maxChars = opts.maxChars ?? DEFAULT_REVIEW_CONTEXT_MAX_CHARS;
2326
+ const resolved = await resolveReference(cleaned);
2327
+ let content = resolved.content;
2328
+ let truncated = false;
2329
+ if (content.length > maxChars) {
2330
+ content = content.slice(0, maxChars);
2331
+ truncated = true;
2332
+ }
2333
+ return {
2334
+ sourceLink: cleaned,
2335
+ sourceType: resolved.sourceType,
2336
+ content,
2337
+ filePath: resolved.filePath,
2338
+ url: resolved.url,
2339
+ truncated
2340
+ };
1479
2341
  }
1480
- function generateBashUnhooks() {
1481
- return `
1482
- # Remove ZAM monitor hooks
1483
- trap - DEBUG
1484
- PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
1485
- unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
1486
- unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
1487
- echo "ZAM monitor stopped."
1488
- `.trim();
2342
+ function normalizePath(p) {
2343
+ const base = p.split("#")[0].trim();
2344
+ return base.replace(/\\/g, "/").toLowerCase();
2345
+ }
2346
+ function matchesFilePath(sourceLink, changedFile) {
2347
+ if (!sourceLink) return false;
2348
+ const normSource = normalizePath(sourceLink);
2349
+ const normChanged = normalizePath(changedFile);
2350
+ if (!normSource || !normChanged) return false;
2351
+ const gitHubMatch = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/i.exec(
2352
+ normSource
2353
+ );
2354
+ if (gitHubMatch) {
2355
+ const filePath = gitHubMatch[4];
2356
+ return filePath === normChanged;
2357
+ }
2358
+ if (normSource.startsWith("http://") || normSource.startsWith("https://")) {
2359
+ return false;
2360
+ }
2361
+ return normSource.endsWith(normChanged) || normChanged.endsWith(normSource);
1489
2362
  }
1490
2363
 
1491
- // src/kernel/observation/skill-discovery.ts
1492
- function discoverSkills(sessionCommands, options = {}) {
1493
- const minSessions = options.minSessions ?? 2;
1494
- const minLen = options.minSequenceLength ?? 2;
1495
- const maxLen = options.maxSequenceLength ?? 5;
1496
- const existing = new Set(options.existingSkillSlugs ?? []);
1497
- const sessionSequences = /* @__PURE__ */ new Map();
1498
- for (const [sessionId, commands] of sessionCommands) {
1499
- const sequences = extractSequences(commands, minLen, maxLen);
1500
- if (sequences.length > 0) {
1501
- sessionSequences.set(sessionId, sequences);
2364
+ // src/kernel/scheduler/interleaver.ts
2365
+ function interleave(items, maxConsecutive = 2) {
2366
+ if (items.length <= 1) return [...items];
2367
+ const byDomain = /* @__PURE__ */ new Map();
2368
+ for (const item of items) {
2369
+ const group = byDomain.get(item.domain);
2370
+ if (group) {
2371
+ group.push(item);
2372
+ } else {
2373
+ byDomain.set(item.domain, [item]);
1502
2374
  }
1503
2375
  }
1504
- const sequenceIndex = /* @__PURE__ */ new Map();
1505
- for (const [, sequences] of sessionSequences) {
1506
- const seen = /* @__PURE__ */ new Set();
1507
- for (const seq of sequences) {
1508
- const key = seq.join(" \u2192 ");
1509
- if (seen.has(key)) continue;
1510
- seen.add(key);
1511
- const entry = sequenceIndex.get(key);
1512
- if (entry) {
1513
- entry.sessionCount++;
1514
- entry.totalOccurrences++;
2376
+ if (byDomain.size === 1) return [...items];
2377
+ const result = [];
2378
+ let consecutiveCount = 0;
2379
+ let lastDomain = null;
2380
+ const cursors = /* @__PURE__ */ new Map();
2381
+ for (const domain of byDomain.keys()) {
2382
+ cursors.set(domain, 0);
2383
+ }
2384
+ while (result.length < items.length) {
2385
+ const activeDomains = [...byDomain.entries()].filter(
2386
+ ([domain]) => (cursors.get(domain) ?? 0) < (byDomain.get(domain)?.length ?? 0)
2387
+ ).sort((a, b) => {
2388
+ const remainA = a[1].length - (cursors.get(a[0]) ?? 0);
2389
+ const remainB = b[1].length - (cursors.get(b[0]) ?? 0);
2390
+ return remainB - remainA;
2391
+ });
2392
+ if (activeDomains.length === 0) break;
2393
+ let pickedThisRound = false;
2394
+ for (const [domain, group] of activeDomains) {
2395
+ const cursor = cursors.get(domain) ?? 0;
2396
+ if (cursor >= group.length) continue;
2397
+ if (domain === lastDomain && consecutiveCount >= maxConsecutive) {
2398
+ continue;
2399
+ }
2400
+ result.push(group[cursor]);
2401
+ cursors.set(domain, cursor + 1);
2402
+ pickedThisRound = true;
2403
+ if (domain === lastDomain) {
2404
+ consecutiveCount++;
1515
2405
  } else {
1516
- sequenceIndex.set(key, {
1517
- steps: seq,
1518
- sessionCount: 1,
1519
- totalOccurrences: 1,
1520
- examples: []
1521
- });
2406
+ lastDomain = domain;
2407
+ consecutiveCount = 1;
1522
2408
  }
2409
+ break;
1523
2410
  }
1524
- }
1525
- const lastSessionId = [...sessionCommands.keys()].pop();
1526
- if (lastSessionId) {
1527
- const lastCommands = sessionCommands.get(lastSessionId);
1528
- for (const [key, entry] of sequenceIndex) {
1529
- if (entry.sessionCount >= minSessions) {
1530
- entry.examples = findExamplesForSequence(lastCommands, entry.steps);
2411
+ if (!pickedThisRound) {
2412
+ for (const [domain, group] of activeDomains) {
2413
+ const cursor = cursors.get(domain) ?? 0;
2414
+ if (cursor >= group.length) continue;
2415
+ result.push(group[cursor]);
2416
+ cursors.set(domain, cursor + 1);
2417
+ if (domain === lastDomain) {
2418
+ consecutiveCount++;
2419
+ } else {
2420
+ lastDomain = domain;
2421
+ consecutiveCount = 1;
2422
+ }
2423
+ break;
1531
2424
  }
1532
2425
  }
1533
2426
  }
1534
- const candidates = [...sequenceIndex.values()].filter(
1535
- (s) => s.sessionCount >= minSessions
2427
+ return result;
2428
+ }
2429
+
2430
+ // src/kernel/scheduler/queue.ts
2431
+ function buildReviewQueue(db, options) {
2432
+ const maxNew = options.maxNew ?? 10;
2433
+ const maxReviews = options.maxReviews ?? 50;
2434
+ const now = options.now ?? /* @__PURE__ */ new Date();
2435
+ const nowISO = now.toISOString();
2436
+ const dueRows = db.prepare(
2437
+ `SELECT
2438
+ c.id AS card_id,
2439
+ c.token_id AS token_id,
2440
+ t.slug AS slug,
2441
+ t.concept AS concept,
2442
+ t.domain AS domain,
2443
+ t.bloom_level AS bloom_level,
2444
+ c.state AS state,
2445
+ c.due_at AS due_at,
2446
+ t.source_link AS source_link,
2447
+ t.question AS question
2448
+ FROM cards c
2449
+ JOIN tokens t ON t.id = c.token_id
2450
+ WHERE c.user_id = ?
2451
+ AND c.blocked = 0
2452
+ AND c.due_at <= ?
2453
+ AND c.state IN ('review', 'relearning', 'learning')
2454
+ AND t.deprecated_at IS NULL
2455
+ ORDER BY c.due_at ASC`
2456
+ ).all(options.userId, nowISO);
2457
+ const newRows = db.prepare(
2458
+ `SELECT
2459
+ c.id AS card_id,
2460
+ c.token_id AS token_id,
2461
+ t.slug AS slug,
2462
+ t.concept AS concept,
2463
+ t.domain AS domain,
2464
+ t.bloom_level AS bloom_level,
2465
+ c.state AS state,
2466
+ c.due_at AS due_at,
2467
+ t.source_link AS source_link,
2468
+ t.question AS question
2469
+ FROM cards c
2470
+ JOIN tokens t ON t.id = c.token_id
2471
+ WHERE c.user_id = ?
2472
+ AND c.blocked = 0
2473
+ AND c.state = 'new'
2474
+ AND t.deprecated_at IS NULL
2475
+ ORDER BY t.bloom_level ASC, t.slug ASC
2476
+ LIMIT ?`
2477
+ ).all(options.userId, maxNew);
2478
+ const nowMs = now.getTime();
2479
+ const sortedDue = [...dueRows].sort((a, b) => {
2480
+ const overdueA = nowMs - new Date(a.due_at).getTime();
2481
+ const overdueB = nowMs - new Date(b.due_at).getTime();
2482
+ return overdueB - overdueA;
2483
+ });
2484
+ const interleavedDue = interleave(
2485
+ sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
1536
2486
  );
1537
- const pruned = removeSubsequences(candidates);
1538
- const proposals = [];
1539
- for (const seq of pruned) {
1540
- const slug = generateSlug(seq.steps);
1541
- if (existing.has(slug)) continue;
1542
- proposals.push({
1543
- slug,
1544
- description: describeSequence(seq.steps),
1545
- steps: seq.steps,
1546
- sessionCount: seq.sessionCount,
1547
- confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
1548
- examples: seq.examples
1549
- });
2487
+ const newItems = newRows.map(rowToItem);
2488
+ const merged = intersperseNew(interleavedDue, newItems, 5);
2489
+ const capped = merged.slice(0, maxReviews);
2490
+ let newCount = 0;
2491
+ let reviewCount = 0;
2492
+ let relearnCount = 0;
2493
+ const domainSet = /* @__PURE__ */ new Set();
2494
+ for (const item of capped) {
2495
+ domainSet.add(item.domain);
2496
+ switch (item.state) {
2497
+ case "new":
2498
+ newCount++;
2499
+ break;
2500
+ case "relearning":
2501
+ relearnCount++;
2502
+ break;
2503
+ default:
2504
+ reviewCount++;
2505
+ break;
2506
+ }
1550
2507
  }
1551
- const confidenceOrder = { high: 0, medium: 1, low: 2 };
1552
- proposals.sort((a, b) => {
1553
- const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
1554
- if (confDiff !== 0) return confDiff;
1555
- return b.sessionCount - a.sessionCount;
1556
- });
1557
- return proposals;
2508
+ return {
2509
+ items: capped,
2510
+ newCount,
2511
+ reviewCount,
2512
+ relearnCount,
2513
+ totalDomains: [...domainSet].sort()
2514
+ };
1558
2515
  }
1559
- function normalizeCommand(command) {
1560
- const parts = command.trim().split(/\s+/);
1561
- const multiWord = ["docker compose", "npm run", "npx", "git"];
1562
- const lower = command.toLowerCase();
1563
- for (const mw of multiWord) {
1564
- if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
1565
- return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
2516
+ function rowToItem(row) {
2517
+ return {
2518
+ cardId: row.card_id,
2519
+ tokenId: row.token_id,
2520
+ slug: row.slug,
2521
+ concept: row.concept,
2522
+ domain: row.domain,
2523
+ bloomLevel: row.bloom_level,
2524
+ state: row.state,
2525
+ dueAt: row.due_at,
2526
+ sourceLink: row.source_link,
2527
+ question: row.question
2528
+ };
2529
+ }
2530
+ function intersperseNew(reviews, newCards, interval) {
2531
+ if (newCards.length === 0) return [...reviews];
2532
+ if (reviews.length === 0) return [...newCards];
2533
+ const result = [];
2534
+ let reviewIdx = 0;
2535
+ let newIdx = 0;
2536
+ let position = 0;
2537
+ while (reviewIdx < reviews.length || newIdx < newCards.length) {
2538
+ if (newIdx < newCards.length && position > 0 && position % interval === interval - 1) {
2539
+ result.push(newCards[newIdx]);
2540
+ newIdx++;
2541
+ } else if (reviewIdx < reviews.length) {
2542
+ result.push(reviews[reviewIdx]);
2543
+ reviewIdx++;
2544
+ } else if (newIdx < newCards.length) {
2545
+ result.push(newCards[newIdx]);
2546
+ newIdx++;
1566
2547
  }
2548
+ position++;
1567
2549
  }
1568
- return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
2550
+ return result;
1569
2551
  }
1570
- function extractSequences(commands, minLen, maxLen) {
1571
- const filtered = commands.filter((c) => {
1572
- const lower = c.command.toLowerCase().trim();
1573
- return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
1574
- });
1575
- const normalized = filtered.map((c) => normalizeCommand(c.command));
1576
- const sequences = [];
1577
- for (let len = minLen; len <= maxLen; len++) {
1578
- for (let i = 0; i <= normalized.length - len; i++) {
1579
- const seq = normalized.slice(i, i + len);
1580
- if (new Set(seq).size >= 2) {
1581
- sequences.push(seq);
1582
- }
2552
+
2553
+ // src/kernel/system/hooks.ts
2554
+ import {
2555
+ appendFileSync as appendFileSync2,
2556
+ copyFileSync,
2557
+ existsSync as existsSync6,
2558
+ mkdirSync as mkdirSync4,
2559
+ readFileSync as readFileSync6
2560
+ } from "fs";
2561
+ import { homedir as homedir4 } from "os";
2562
+ import { join as join6 } from "path";
2563
+ import { fileURLToPath } from "url";
2564
+ var HOME = homedir4();
2565
+ function getPackageSkillPath(agent = "default") {
2566
+ const packageRoot = [
2567
+ fileURLToPath(new URL("../../..", import.meta.url)),
2568
+ fileURLToPath(new URL("../..", import.meta.url)),
2569
+ fileURLToPath(new URL("..", import.meta.url))
2570
+ ].find((candidate) => existsSync6(join6(candidate, "package.json"))) ?? "";
2571
+ if (!packageRoot) return "";
2572
+ if (agent === "codex") {
2573
+ const codexPath = join6(packageRoot, ".agents", "skills", "zam", "SKILL.md");
2574
+ if (existsSync6(codexPath)) return codexPath;
2575
+ return "";
2576
+ }
2577
+ if (agent === "claude") {
2578
+ const claudePath = join6(
2579
+ packageRoot,
2580
+ ".claude",
2581
+ "skills",
2582
+ "zam",
2583
+ "SKILL.md"
2584
+ );
2585
+ if (existsSync6(claudePath)) return claudePath;
2586
+ return "";
2587
+ }
2588
+ let path = join6(packageRoot, ".agent", "skills", "zam", "SKILL.md");
2589
+ if (existsSync6(path)) return path;
2590
+ path = join6(packageRoot, ".claude", "skills", "zam", "SKILL.md");
2591
+ if (existsSync6(path)) return path;
2592
+ return "";
2593
+ }
2594
+ function distributeGlobalSkills(home = HOME) {
2595
+ const sourceSkill = getPackageSkillPath();
2596
+ const claudeSourceSkill = getPackageSkillPath("claude");
2597
+ const codexSourceSkill = getPackageSkillPath("codex");
2598
+ const results = [];
2599
+ if (!sourceSkill) {
2600
+ console.warn("Could not find ZAM source SKILL.md in the package folder.");
2601
+ return results;
2602
+ }
2603
+ const claudeSkillsDir = join6(home, ".claude", "skills", "zam");
2604
+ try {
2605
+ if (!claudeSourceSkill) {
2606
+ throw new Error("Claude skill source not found");
1583
2607
  }
2608
+ mkdirSync4(claudeSkillsDir, { recursive: true });
2609
+ copyFileSync(claudeSourceSkill, join6(claudeSkillsDir, "SKILL.md"));
2610
+ results.push({
2611
+ name: "Claude Code Global",
2612
+ path: join6(claudeSkillsDir, "SKILL.md"),
2613
+ success: true
2614
+ });
2615
+ } catch (_err) {
2616
+ results.push({
2617
+ name: "Claude Code Global",
2618
+ path: claudeSkillsDir,
2619
+ success: false
2620
+ });
1584
2621
  }
1585
- return sequences;
2622
+ const geminiSkillsDir = join6(home, ".gemini", "skills", "zam");
2623
+ try {
2624
+ mkdirSync4(geminiSkillsDir, { recursive: true });
2625
+ copyFileSync(sourceSkill, join6(geminiSkillsDir, "SKILL.md"));
2626
+ results.push({
2627
+ name: "Gemini CLI Global",
2628
+ path: join6(geminiSkillsDir, "SKILL.md"),
2629
+ success: true
2630
+ });
2631
+ } catch (_err) {
2632
+ results.push({
2633
+ name: "Gemini CLI Global",
2634
+ path: geminiSkillsDir,
2635
+ success: false
2636
+ });
2637
+ }
2638
+ const codexSkillsDir = join6(home, ".agents", "skills", "zam");
2639
+ try {
2640
+ if (!codexSourceSkill) {
2641
+ throw new Error("Codex skill source not found");
2642
+ }
2643
+ mkdirSync4(codexSkillsDir, { recursive: true });
2644
+ copyFileSync(codexSourceSkill, join6(codexSkillsDir, "SKILL.md"));
2645
+ results.push({
2646
+ name: "Codex Global",
2647
+ path: join6(codexSkillsDir, "SKILL.md"),
2648
+ success: true
2649
+ });
2650
+ } catch (_err) {
2651
+ results.push({
2652
+ name: "Codex Global",
2653
+ path: codexSkillsDir,
2654
+ success: false
2655
+ });
2656
+ }
2657
+ const gooseSkillsDir = join6(home, ".goose", "skills", "zam");
2658
+ try {
2659
+ mkdirSync4(gooseSkillsDir, { recursive: true });
2660
+ copyFileSync(sourceSkill, join6(gooseSkillsDir, "SKILL.md"));
2661
+ results.push({
2662
+ name: "Goose Global",
2663
+ path: join6(gooseSkillsDir, "SKILL.md"),
2664
+ success: true
2665
+ });
2666
+ } catch (_err) {
2667
+ results.push({
2668
+ name: "Goose Global",
2669
+ path: gooseSkillsDir,
2670
+ success: false
2671
+ });
2672
+ }
2673
+ return results;
1586
2674
  }
1587
- function findExamplesForSequence(commands, steps) {
1588
- const normalized = commands.map((c) => ({
1589
- norm: normalizeCommand(c.command),
1590
- full: c.command
1591
- }));
1592
- for (let i = 0; i <= normalized.length - steps.length; i++) {
1593
- let match = true;
1594
- for (let j = 0; j < steps.length; j++) {
1595
- if (normalized[i + j].norm !== steps[j]) {
1596
- match = false;
1597
- break;
2675
+ function injectShellHooks() {
2676
+ const results = [];
2677
+ const hookLine = `
2678
+ # ZAM Shell Observation Hooks
2679
+ if (command -v zam >/dev/null 2>&1); then eval "$(zam monitor start --quiet)"; fi
2680
+ `;
2681
+ const pwshHookLine = `
2682
+ # ZAM Shell Observation Hooks
2683
+ if (Get-Command zam -ErrorAction SilentlyContinue) { Invoke-Expression (& zam monitor start --quiet pwsh) }
2684
+ `;
2685
+ const zshrc = join6(HOME, ".zshrc");
2686
+ if (existsSync6(zshrc)) {
2687
+ try {
2688
+ const content = readFileSync6(zshrc, "utf8");
2689
+ if (content.includes("zam monitor start")) {
2690
+ results.push({
2691
+ shell: "zsh",
2692
+ file: zshrc,
2693
+ success: true,
2694
+ alreadyHooked: true
2695
+ });
2696
+ } else {
2697
+ appendFileSync2(zshrc, hookLine);
2698
+ results.push({
2699
+ shell: "zsh",
2700
+ file: zshrc,
2701
+ success: true,
2702
+ alreadyHooked: false
2703
+ });
1598
2704
  }
1599
- }
1600
- if (match) {
1601
- return normalized.slice(i, i + steps.length).map((n) => n.full);
2705
+ } catch {
2706
+ results.push({
2707
+ shell: "zsh",
2708
+ file: zshrc,
2709
+ success: false,
2710
+ alreadyHooked: false
2711
+ });
1602
2712
  }
1603
2713
  }
1604
- return [];
1605
- }
1606
- function removeSubsequences(candidates) {
1607
- const sorted = [...candidates].sort((a, b) => b.steps.length - a.steps.length);
1608
- const result = [];
1609
- for (const candidate of sorted) {
1610
- const key = candidate.steps.join(" \u2192 ");
1611
- const isSubsequence = result.some((longer) => {
1612
- const longerKey = longer.steps.join(" \u2192 ");
1613
- return longerKey.includes(key) && longerKey !== key;
1614
- });
1615
- if (!isSubsequence) {
1616
- result.push(candidate);
2714
+ const bashrc = join6(HOME, ".bashrc");
2715
+ if (existsSync6(bashrc)) {
2716
+ try {
2717
+ const content = readFileSync6(bashrc, "utf8");
2718
+ if (content.includes("zam monitor start")) {
2719
+ results.push({
2720
+ shell: "bash",
2721
+ file: bashrc,
2722
+ success: true,
2723
+ alreadyHooked: true
2724
+ });
2725
+ } else {
2726
+ appendFileSync2(bashrc, hookLine);
2727
+ results.push({
2728
+ shell: "bash",
2729
+ file: bashrc,
2730
+ success: true,
2731
+ alreadyHooked: false
2732
+ });
2733
+ }
2734
+ } catch {
2735
+ results.push({
2736
+ shell: "bash",
2737
+ file: bashrc,
2738
+ success: false,
2739
+ alreadyHooked: false
2740
+ });
2741
+ }
2742
+ }
2743
+ const pwshDirs = [
2744
+ join6(HOME, "Documents", "PowerShell"),
2745
+ join6(HOME, "Documents", "WindowsPowerShell")
2746
+ ];
2747
+ for (const dir of pwshDirs) {
2748
+ const profileFile = join6(dir, "Microsoft.PowerShell_profile.ps1");
2749
+ try {
2750
+ mkdirSync4(dir, { recursive: true });
2751
+ let content = "";
2752
+ if (existsSync6(profileFile)) {
2753
+ content = readFileSync6(profileFile, "utf8");
2754
+ }
2755
+ if (content.includes("zam monitor start")) {
2756
+ results.push({
2757
+ shell: "powershell",
2758
+ file: profileFile,
2759
+ success: true,
2760
+ alreadyHooked: true
2761
+ });
2762
+ } else {
2763
+ appendFileSync2(profileFile, pwshHookLine);
2764
+ results.push({
2765
+ shell: "powershell",
2766
+ file: profileFile,
2767
+ success: true,
2768
+ alreadyHooked: false
2769
+ });
2770
+ }
2771
+ } catch {
2772
+ results.push({
2773
+ shell: "powershell",
2774
+ file: profileFile,
2775
+ success: false,
2776
+ alreadyHooked: false
2777
+ });
1617
2778
  }
1618
2779
  }
1619
- return result;
1620
- }
1621
- function generateSlug(steps) {
1622
- return steps.map((s) => {
1623
- const parts = s.split(/\s+/);
1624
- return parts[parts.length - 1];
1625
- }).join("-");
1626
- }
1627
- function describeSequence(steps) {
1628
- return `Recurring pattern: ${steps.join(" \u2192 ")}`;
2780
+ return results;
1629
2781
  }
1630
2782
 
1631
- // src/kernel/goals/engine.ts
1632
- import { readdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
1633
- import { join as join3, basename } from "path";
1634
-
1635
- // src/kernel/goals/parser.ts
1636
- function parseGoalFile(content, slug, filePath) {
1637
- const { frontmatter, body } = splitFrontmatter(content);
1638
- const validStatuses = ["active", "completed", "paused", "abandoned"];
1639
- const status = validStatuses.includes(frontmatter.status) ? frontmatter.status : "active";
1640
- const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1641
- return {
1642
- slug,
1643
- title: frontmatter.title || slug,
1644
- status,
1645
- parent: frontmatter.parent || null,
1646
- created: frontmatter.created || now,
1647
- updated: frontmatter.updated || now,
1648
- body,
1649
- filePath
1650
- };
1651
- }
1652
- function serializeGoal(goal) {
1653
- const lines = [
1654
- "---",
1655
- `title: ${goal.title}`,
1656
- `status: ${goal.status}`
1657
- ];
1658
- if (goal.parent) {
1659
- lines.push(`parent: ${goal.parent}`);
2783
+ // src/kernel/system/i18n.ts
2784
+ var TRANSLATIONS = {
2785
+ en: {
2786
+ welcome: "Learning session: {count} card(s)",
2787
+ new_review_relearn: " New: {newC} Review: {reviewC} Relearn: {relearnC}",
2788
+ domains: " Domains: {domains}",
2789
+ instruction: "\nRecall each answer first, reveal it, then rate yourself honestly.",
2790
+ quit_hint: "Type 'q' at the answer prompt (or press Ctrl+C) to stop anytime.",
2791
+ offline_warning: "\n\x1B[33m\u26A0 LLM-Feedback & automatic translation are disabled.\x1B[0m",
2792
+ offline_instruction: " Enable with: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
2793
+ nothing_due: "Nothing due to learn. You're all caught up!",
2794
+ evaluating: "Evaluating answer via local AI...",
2795
+ generating_question: "Generating dynamic question...",
2796
+ translating: "Translating question dynamically...",
2797
+ prompt_answer: "Your answer (Enter to reveal \xB7 'q' to stop):",
2798
+ session_ended: "Learning session ended.",
2799
+ session_complete: "Learning session complete!",
2800
+ cards_rated: " Cards rated: {count}",
2801
+ avg_rating: " Average rating: {avg}",
2802
+ forgot: " Forgot: {count} card(s)",
2803
+ feedback_title: "\u2500\u2500 ZAM Feedback {line}",
2804
+ answer_title: "\u2500\u2500 Answer {line}",
2805
+ keep_waiting: "Would you like to keep waiting?",
2806
+ local_ai_working: "The local AI is still generating the response.",
2807
+ wait_warning: "\u26A0 The LLM server is taking a while to load the model.",
2808
+ wait_info: "(This is expected when transitioning between models or starting up from cold.)",
2809
+ keep_waiting_llm: "Would you like to keep waiting for the model?",
2810
+ proceeding_offline: "\u26A0 Proceeding in offline-mode (without active LLM evaluations for this session).",
2811
+ eval_skipped: " [LLM Evaluation skipped: {reason}]"
2812
+ },
2813
+ de: {
2814
+ welcome: "Lern-Session: {count} Karte(n)",
2815
+ new_review_relearn: " Neu: {newC} Wiederholen: {reviewC} Lernen: {relearnC}",
2816
+ domains: " Dom\xE4nen: {domains}",
2817
+ instruction: "\nRufe jede Antwort zuerst ab, decke sie auf und bewerte dich dann ehrlich selbst.",
2818
+ quit_hint: "Gib 'q' bei der Antwortaufforderung ein (oder dr\xFCcke Strg+C), um jederzeit zu beenden.",
2819
+ offline_warning: "\n\x1B[33m\u26A0 LLM-Feedback & automatische \xDCbersetzung sind deaktiviert.\x1B[0m",
2820
+ offline_instruction: " Aktivieren mit: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
2821
+ nothing_due: "Nichts f\xE4llig zu lernen. Du bist komplett auf dem Laufenden!",
2822
+ evaluating: "Bewerte Antwort via lokaler KI...",
2823
+ generating_question: "Generiere dynamische Frage...",
2824
+ translating: "\xDCbersetze Frage dynamisch...",
2825
+ prompt_answer: "Deine Antwort (Eingabe zum Aufdecken \xB7 'q' zum Beenden):",
2826
+ session_ended: "Lern-Session beendet.",
2827
+ session_complete: "Lern-Session abgeschlossen!",
2828
+ cards_rated: " Bewertete Karten: {count}",
2829
+ avg_rating: " Durchschnittliche Bewertung: {avg}",
2830
+ forgot: " Vergessen: {count} Karte(n)",
2831
+ feedback_title: "\u2500\u2500 ZAM Feedback {line}",
2832
+ answer_title: "\u2500\u2500 Antwort {line}",
2833
+ keep_waiting: "M\xF6chtest du weiter auf die Bewertung warten?",
2834
+ local_ai_working: "Die lokale KI arbeitet noch an der Antwort.",
2835
+ wait_warning: "\u26A0 Der LLM-Server braucht ungew\xF6hnlich lange, um das Modell zu laden.",
2836
+ wait_info: "(Das ist normal, wenn das Modell gewechselt wird oder kalt startet.)",
2837
+ keep_waiting_llm: "M\xF6chtest du weiter auf das Modell warten?",
2838
+ proceeding_offline: "\u26A0 Fahre im Offline-Modus fort (ohne aktive LLM-Bewertungen in dieser Runde).",
2839
+ eval_skipped: " [LLM-Bewertung \xFCbersprungen: {reason}]"
2840
+ },
2841
+ es: {
2842
+ welcome: "Sesi\xF3n de aprendizaje: {count} tarjeta(s)",
2843
+ new_review_relearn: " Nuevas: {newC} Repasar: {reviewC} Reaprender: {relearnC}",
2844
+ domains: " Dominios: {domains}",
2845
+ instruction: "\nRecuerda cada respuesta primero, rev\xE9lala y calif\xEDcate honestamente.",
2846
+ quit_hint: "Escribe 'q' en la respuesta (o presiona Ctrl+C) para salir en cualquier momento.",
2847
+ offline_warning: "\n\x1B[33m\u26A0 Los comentarios de LLM y la traducci\xF3n autom\xE1tica est\xE1n desactivados.\x1B[0m",
2848
+ offline_instruction: " Activar con: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
2849
+ nothing_due: "No hay nada pendiente para aprender. \xA1Est\xE1s al d\xEDa!",
2850
+ evaluating: "Evaluando respuesta con IA local...",
2851
+ generating_question: "Generando pregunta din\xE1mica...",
2852
+ translating: "Traduciendo pregunta din\xE1micamente...",
2853
+ prompt_answer: "Tu respuesta (Intro para revelar \xB7 'q' para salir):",
2854
+ session_ended: "Sesi\xF3n de aprendizaje finalizada.",
2855
+ session_complete: "\xA1Sesi\xF3n de aprendizaje completada!",
2856
+ cards_rated: " Tarjetas calificadas: {count}",
2857
+ avg_rating: " Calificaci\xF3n promedio: {avg}",
2858
+ forgot: " Olvidadas: {count} tarjeta(s)",
2859
+ feedback_title: "\u2500\u2500 Comentarios de ZAM {line}",
2860
+ answer_title: "\u2500\u2500 Respuesta {line}",
2861
+ keep_waiting: "\xBFDeseas seguir esperando la evaluaci\xF3n?",
2862
+ local_ai_working: "La IA local todav\xEDa est\xE1 generando la respuesta.",
2863
+ wait_warning: "\u26A0 El servidor LLM est\xE1 tardando en cargar el modelo.",
2864
+ wait_info: "(Esto es normal al cambiar de modelo o iniciar en fr\xEDo.)",
2865
+ keep_waiting_llm: "\xBFDeseas seguir esperando el modelo?",
2866
+ proceeding_offline: "\u26A0 Continuando en modo fuera de l\xEDnea (sin evaluaciones de LLM en esta sesi\xF3n).",
2867
+ eval_skipped: " [Evaluaci\xF3n de LLM omitida: {reason}]"
2868
+ },
2869
+ fr: {
2870
+ welcome: "Session d'apprentissage : {count} carte(s)",
2871
+ new_review_relearn: " Nouveau: {newC} R\xE9vision: {reviewC} Relever: {relearnC}",
2872
+ domains: " Domaines: {domains}",
2873
+ instruction: "\nRappelez-vous chaque r\xE9ponse d'abord, r\xE9v\xE9lez-la, puis \xE9valuez-vous honn\xEAtement.",
2874
+ quit_hint: "Tapez 'q' (ou Ctrl+C) pour quitter \xE0 tout moment.",
2875
+ offline_warning: "\n\x1B[33m\u26A0 Les commentaires LLM et la traduction automatique sont d\xE9sactiv\xE9s.\x1B[0m",
2876
+ offline_instruction: " Activer avec : \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
2877
+ nothing_due: "Rien \xE0 apprendre. Vous \xEAtes \xE0 jour !",
2878
+ evaluating: "\xC9valuation de la r\xE9ponse via l'IA locale...",
2879
+ generating_question: "G\xE9n\xE9ration d'une question dynamique...",
2880
+ translating: "Traduction dynamique de la question...",
2881
+ prompt_answer: "Votre r\xE9ponse (Entr\xE9e pour r\xE9v\xE9ler \xB7 'q' pour quitter) :",
2882
+ session_ended: "Session d'apprentissage arr\xEAt\xE9e.",
2883
+ session_complete: "Session d'apprentissage termin\xE9e !",
2884
+ cards_rated: " Cartes \xE9valu\xE9es: {count}",
2885
+ avg_rating: " Note moyenne: {avg}",
2886
+ forgot: " Oubli\xE9es: {count} carte(s)",
2887
+ feedback_title: "\u2500\u2500 Commentaires ZAM {line}",
2888
+ answer_title: "\u2500\u2500 R\xE9ponse {line}",
2889
+ keep_waiting: "Voulez-vous continuer \xE0 attendre l'\xE9valuation ?",
2890
+ local_ai_working: "L'IA locale est toujours en train de g\xE9n\xE9rer la r\xE9ponse.",
2891
+ wait_warning: "\u26A0 Le serveur LLM prend du temps pour charger le mod\xE8le.",
2892
+ wait_info: "(Ceci est normal lors de la transition entre mod\xE8les ou du d\xE9marrage \xE0 froid.)",
2893
+ keep_waiting_llm: "Voulez-vous continuer \xE0 attendre le mod\xE8le ?",
2894
+ proceeding_offline: "\u26A0 Poursuite en mode hors ligne (sans \xE9valuation active de l'IA pour cette session).",
2895
+ eval_skipped: " [\xC9valuation LLM ignor\xE9e : {reason}]"
2896
+ },
2897
+ pt: {
2898
+ welcome: "Sess\xE3o de aprendizado: {count} cart\xE3o(\xF5es)",
2899
+ new_review_relearn: " Novos: {newC} Revisar: {reviewC} Reaprender: {relearnC}",
2900
+ domains: " Dom\xEDnios: {domains}",
2901
+ instruction: "\nLembre-se de cada resposta primeiro, revele-a e avalie-se honestamente.",
2902
+ quit_hint: "Digite 'q' (ou Ctrl+C) para parar a qualquer momento.",
2903
+ offline_warning: "\n\x1B[33m\u26A0 O feedback do LLM e a tradu\xE7\xE3o autom\xE1tica est\xE3o desativados.\x1B[0m",
2904
+ offline_instruction: " Ativar com: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
2905
+ nothing_due: "Nada faturado para aprender. Voc\xEA est\xE1 atualizado!",
2906
+ evaluating: "Avaliando a resposta via IA local...",
2907
+ generating_question: "Gerando pergunta din\xE2mica...",
2908
+ translating: "Traduzindo pergunta dinamicamente...",
2909
+ prompt_answer: "Sua resposta (Enter para revelar \xB7 'q' para parar):",
2910
+ session_ended: "Sess\xE3o de aprendizado encerrada.",
2911
+ session_complete: "Sess\xE3o de aprendizado conclu\xEDda!",
2912
+ cards_rated: " Cart\xF5es avaliados: {count}",
2913
+ avg_rating: " Nota m\xE9dia: {avg}",
2914
+ forgot: " Esquecidos: {count} cart\xE3o(\xF5es)",
2915
+ feedback_title: "\u2500\u2500 Feedback ZAM {line}",
2916
+ answer_title: "\u2500\u2500 Resposta {line}",
2917
+ keep_waiting: "Deseja continuar esperando pela avalia\xE7\xE3o?",
2918
+ local_ai_working: "A IA local ainda est\xE1 gerando a resposta.",
2919
+ wait_warning: "\u26A0 O servidor LLM est\xE1 demorando para carregar o modelo.",
2920
+ wait_info: "(Isso \xE9 esperado ao alternar modelos ou iniciar do zero.)",
2921
+ keep_waiting_llm: "Deseja continuar esperando o modelo?",
2922
+ proceeding_offline: "\u26A0 Continuando no modo offline (sem avalia\xE7\xF5es de LLM ativas nesta sess\xE3o).",
2923
+ eval_skipped: " [Avalia\xE7\xE3o LLM omitida: {reason}]"
2924
+ },
2925
+ zh: {
2926
+ welcome: "\u5B66\u4E60\u8BFE: {count} \u5F20\u5361\u7247",
2927
+ new_review_relearn: " \u65B0\u5361: {newC} \u590D\u4E60: {reviewC} \u91CD\u5B66: {relearnC}",
2928
+ domains: " \u77E5\u8BC6\u9886\u57DF: {domains}",
2929
+ instruction: "\n\u9996\u5148\u5728\u8111\u4E2D\u56DE\u5FC6\u7B54\u6848\uFF0C\u7136\u540E\u63ED\u6653\u5E76\u8BDA\u5B9E\u81EA\u6211\u8BC4\u5206\u3002",
2930
+ quit_hint: "\u5728\u56DE\u7B54\u63D0\u793A\u5904\u8F93\u5165 'q' (\u6216\u6309 Ctrl+C) \u53EF\u968F\u65F6\u9000\u51FA\u3002",
2931
+ offline_warning: "\n\x1B[33m\u26A0 LLM \u53CD\u9988\u4E0E\u81EA\u52A8\u7FFB\u8BD1\u5DF2\u7981\u7528\u3002\x1B[0m",
2932
+ offline_instruction: " \u5F00\u542F\u547D\u4EE4: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
2933
+ nothing_due: "\u76EE\u524D\u6CA1\u6709\u9700\u8981\u5B66\u4E60\u7684\u5185\u5BB9\u3002\u60A8\u5DF2\u5168\u90E8\u638C\u63E1\uFF01",
2934
+ evaluating: "\u6B63\u5728\u901A\u8FC7\u672C\u5730 AI \u8BC4\u4F30\u56DE\u7B54...",
2935
+ generating_question: "\u6B63\u5728\u52A8\u6001\u751F\u6210\u95EE\u9898...",
2936
+ translating: "\u6B63\u5728\u52A8\u6001\u7FFB\u8BD1\u95EE\u9898...",
2937
+ prompt_answer: "\u60A8\u7684\u56DE\u7B54 (\u6309\u56DE\u8F66\u63ED\u6653 \xB7 \u8F93\u5165 'q' \u9000\u51FA):",
2938
+ session_ended: "\u5B66\u4E60\u8BFE\u5DF2\u7ED3\u675F\u3002",
2939
+ session_complete: "\u5B66\u4E60\u8BFE\u5DF2\u5B8C\u6210\uFF01",
2940
+ cards_rated: " \u5DF2\u8BC4\u5206\u5361\u7247: {count}",
2941
+ avg_rating: " \u5E73\u5747\u5206: {avg}",
2942
+ forgot: " \u9057\u5FD8: {count} \u5F20\u5361\u7247",
2943
+ feedback_title: "\u2500\u2500 ZAM \u53CD\u9988 {line}",
2944
+ answer_title: "\u2500\u2500 \u53C2\u8003\u7B54\u6848 {line}",
2945
+ keep_waiting: "\u662F\u5426\u7EE7\u7EED\u7B49\u5F85\u8BC4\u5206\uFF1F",
2946
+ local_ai_working: "\u672C\u5730 AI \u4ECD\u5728\u751F\u6210\u56DE\u7B54\u3002",
2947
+ wait_warning: "\u26A0 LLM \u670D\u52A1\u5668\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\uFF0C\u8FD9\u53EF\u80FD\u9700\u8981\u4E00\u4E9B\u65F6\u95F4\u3002",
2948
+ wait_info: "(\u8FD9\u5728\u5207\u6362\u6A21\u578B\u6216\u51B7\u542F\u52A8\u65F6\u662F\u6B63\u5E38\u73B0\u8C61\u3002)",
2949
+ keep_waiting_llm: "\u662F\u5426\u7EE7\u7EED\u7B49\u5F85\u6A21\u578B\u52A0\u8F7D\uFF1F",
2950
+ proceeding_offline: "\u26A0 \u6B63\u5728\u4EE5\u79BB\u7EBF\u6A21\u5F0F\u7EE7\u7EED\uFF08\u672C\u6B21\u5B66\u4E60\u8BFE\u5C06\u4E0D\u5305\u542B\u6D3B\u8DC3\u7684 AI \u8BC4\u4F30\uFF09\u3002",
2951
+ eval_skipped: " [\u5DF2\u8DF3\u8FC7 LLM \u8BC4\u4F30: {reason}]"
2952
+ },
2953
+ ja: {
2954
+ welcome: "\u5B66\u7FD2\u30BB\u30C3\u30B7\u30E7\u30F3: {count} \u679A\u306E\u30AB\u30FC\u30C9",
2955
+ new_review_relearn: " \u65B0\u898F: {newC} \u5FA9\u7FD2: {reviewC} \u518D\u5B66\u7FD2: {relearnC}",
2956
+ domains: " \u30C9\u30E1\u30A4\u30F3: {domains}",
2957
+ instruction: "\n\u6700\u521D\u306B\u56DE\u7B54\u3092\u601D\u3044\u51FA\u3057\u3001\u6B21\u306B\u56DE\u7B54\u3092\u8868\u793A\u3057\u3066\u3001\u6B63\u76F4\u306B\u81EA\u5DF1\u8A55\u4FA1\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
2958
+ quit_hint: "\u56DE\u7B54\u30D7\u30ED\u30F3\u30D7\u30C8\u3067\u300Cq\u300D\u3092\u5165\u529B\u3059\u308B\uFF08\u307E\u305F\u306F Ctrl+C \u3092\u62BC\u3059\uFF09\u3068\u3001\u3044\u3064\u3067\u3082\u7D42\u4E86\u3067\u304D\u307E\u3059\u3002",
2959
+ offline_warning: "\n\x1B[33m\u26A0 LLM \u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u3068\u81EA\u52D5\u7FFB\u8A33\u306F\u7121\u52B9\u3067\u3059\u3002\x1B[0m",
2960
+ offline_instruction: " \u6709\u52B9\u5316\u3059\u308B\u306B\u306F: \x1B[36mnpm run dev -- settings llm on\x1B[0m\n",
2961
+ nothing_due: "\u5B66\u7FD2\u4E88\u5B9A\u306E\u30AB\u30FC\u30C9\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u3059\u3079\u3066\u5B8C\u4E86\u3057\u3066\u3044\u307E\u3059\uFF01",
2962
+ evaluating: "\u30ED\u30FC\u30AB\u30EBAI\u306B\u3088\u308B\u56DE\u7B54\u306E\u8A55\u4FA1\u4E2D...",
2963
+ generating_question: "\u8CEA\u554F\u3092\u52D5\u7684\u306B\u751F\u6210\u4E2D...",
2964
+ translating: "\u8CEA\u554F\u3092\u52D5\u7684\u306B\u7FFB\u8A33\u4E2D...",
2965
+ prompt_answer: "\u3042\u306A\u305F\u306E\u56DE\u7B54 (Enter\u3067\u8868\u793A \xB7 'q'\u3067\u7D42\u4E86):",
2966
+ session_ended: "\u5B66\u7FD2\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u7D42\u4E86\u3057\u307E\u3057\u305F\u3002",
2967
+ session_complete: "\u5B66\u7FD2\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u5B8C\u4E86\u3057\u307E\u3057\u305F\uFF01",
2968
+ cards_rated: " \u8A55\u4FA1\u6E08\u307F\u30AB\u30FC\u30C9\u6570: {count}",
2969
+ avg_rating: " \u5E73\u5747\u8A55\u4FA1: {avg}",
2970
+ forgot: " \u5FD8\u308C\u305F\u30AB\u30FC\u30C9\u6570: {count} \u679A",
2971
+ feedback_title: "\u2500\u2500 ZAM \u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF {line}",
2972
+ answer_title: "\u2500\u2500 \u89E3\u7B54 {line}",
2973
+ keep_waiting: "\u8A55\u4FA1\u306E\u751F\u6210\u3092\u5F85\u3061\u307E\u3059\u304B\uFF1F",
2974
+ local_ai_working: "\u30ED\u30FC\u30AB\u30EBAI\u304C\u56DE\u7B54\u3092\u751F\u6210\u3057\u3066\u3044\u307E\u3059\u3002",
2975
+ wait_warning: "\u26A0 LLM \u30B5\u30FC\u30D0\u30FC\u304C\u30E2\u30C7\u30EB\u3092\u30ED\u30FC\u30C9\u3059\u308B\u306E\u306B\u6642\u9593\u304C\u304B\u304B\u3063\u3066\u3044\u307E\u3059\u3002",
2976
+ wait_info: "(\u30E2\u30C7\u30EB\u306E\u79FB\u884C\u4E2D\u3084\u30B3\u30FC\u30EB\u30C9\u30B9\u30BF\u30FC\u30C8\u6642\u306B\u306F\u3001\u3053\u308C\u304C\u4E88\u60F3\u3055\u308C\u307E\u3059\u3002)",
2977
+ keep_waiting_llm: "\u30E2\u30C7\u30EB\u306E\u30ED\u30FC\u30C9\u3092\u5F85\u3061\u7D9A\u3051\u307E\u3059\u304B\uFF1F",
2978
+ proceeding_offline: "\u26A0 \u30AA\u30D5\u30E9\u30A4\u30F3\u30E2\u30FC\u30C9\u3067\u7D9A\u884C\u3057\u307E\u3059\uFF08\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u3067\u306F\u30A2\u30AF\u30C6\u30A3\u30D6\u306A AI \u8A55\u4FA1\u306F\u884C\u308F\u308C\u307E\u305B\u3093\uFF09\u3002",
2979
+ eval_skipped: " [LLM \u8A55\u4FA1\u304C\u30B9\u30AD\u30C3\u30D7\u3055\u308C\u307E\u3057\u305F: {reason}]"
1660
2980
  }
1661
- lines.push(`created: ${goal.created}`);
1662
- lines.push(`updated: ${goal.updated}`);
1663
- lines.push("---");
1664
- lines.push("");
1665
- if (goal.body.trim()) {
1666
- lines.push(goal.body.trim());
1667
- lines.push("");
2981
+ };
2982
+ function t(locale, key, params = {}) {
2983
+ const dict = TRANSLATIONS[locale] || TRANSLATIONS.en;
2984
+ let str = dict[key] || TRANSLATIONS.en[key] || "";
2985
+ for (const [k, v] of Object.entries(params)) {
2986
+ str = str.replace(new RegExp(`{${k}}`, "g"), String(v));
1668
2987
  }
1669
- return lines.join("\n");
2988
+ return str;
1670
2989
  }
1671
- function extractTasks(body) {
1672
- const tasks = [];
1673
- const taskRegex = /^[-*]\s+\[([ xX])\]\s+(.+)$/gm;
1674
- let match;
1675
- while ((match = taskRegex.exec(body)) !== null) {
1676
- tasks.push({
1677
- done: match[1] !== " ",
1678
- text: match[2].trim()
1679
- });
2990
+
2991
+ // src/kernel/system/installer.ts
2992
+ import { execSync } from "child_process";
2993
+ import { existsSync as existsSync7 } from "fs";
2994
+ import { homedir as homedir5 } from "os";
2995
+ import { join as join7 } from "path";
2996
+ function hasCommand(cmd) {
2997
+ try {
2998
+ const checkCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
2999
+ execSync(checkCmd, { stdio: "ignore" });
3000
+ return true;
3001
+ } catch {
3002
+ return false;
1680
3003
  }
1681
- return tasks;
1682
3004
  }
1683
- function extractTokenRefs(body) {
1684
- const tokensSection = body.match(/## Tokens\n([\s\S]*?)(?=\n## |\n*$)/);
1685
- if (!tokensSection) return [];
1686
- const refs = [];
1687
- const lines = tokensSection[1].split("\n");
1688
- for (const line of lines) {
1689
- const match = line.match(/^[-*]\s+(\S+)/);
1690
- if (match) {
1691
- refs.push(match[1]);
1692
- }
3005
+ function installFastFlowLM() {
3006
+ if (process.platform !== "win32") {
3007
+ return {
3008
+ success: false,
3009
+ message: "FastFlowLM is only supported on Windows."
3010
+ };
3011
+ }
3012
+ const hasFlm = hasCommand("flm") || existsSync7("C:\\Program Files\\flm\\flm.exe");
3013
+ if (hasFlm) {
3014
+ return { success: true, message: "FastFlowLM is already installed." };
3015
+ }
3016
+ if (!hasCommand("winget")) {
3017
+ return {
3018
+ success: false,
3019
+ message: "winget package manager was not found on this system."
3020
+ };
3021
+ }
3022
+ console.log("Installing FastFlowLM via winget...");
3023
+ try {
3024
+ execSync(
3025
+ "winget install -e --id FastFlowLM --accept-source-agreements --accept-package-agreements",
3026
+ { stdio: "inherit" }
3027
+ );
3028
+ return { success: true, message: "FastFlowLM installed successfully." };
3029
+ } catch (err) {
3030
+ return {
3031
+ success: false,
3032
+ message: `Failed to install FastFlowLM: ${err.message}`
3033
+ };
1693
3034
  }
1694
- return refs;
1695
3035
  }
1696
- function splitFrontmatter(content) {
1697
- const trimmed = content.trim();
1698
- if (!trimmed.startsWith("---")) {
1699
- return { frontmatter: {}, body: trimmed };
3036
+ function installOllama() {
3037
+ const isMac = process.platform === "darwin";
3038
+ const isWin = process.platform === "win32";
3039
+ const hasOllama = hasCommand("ollama") || isMac && existsSync7("/Applications/Ollama.app") || isWin && existsSync7(
3040
+ join7(homedir5(), "AppData", "Local", "Programs", "Ollama", "ollama.exe")
3041
+ );
3042
+ if (hasOllama) {
3043
+ return { success: true, message: "Ollama is already installed." };
1700
3044
  }
1701
- const endIndex = trimmed.indexOf("---", 3);
1702
- if (endIndex === -1) {
1703
- return { frontmatter: {}, body: trimmed };
3045
+ if (process.platform === "darwin") {
3046
+ if (!hasCommand("brew")) {
3047
+ return {
3048
+ success: false,
3049
+ message: "Homebrew was not found. Please install Homebrew from brew.sh first."
3050
+ };
3051
+ }
3052
+ console.log("Installing Ollama via Homebrew Cask...");
3053
+ try {
3054
+ execSync("brew install --cask ollama", { stdio: "inherit" });
3055
+ return { success: true, message: "Ollama installed successfully." };
3056
+ } catch (err) {
3057
+ return {
3058
+ success: false,
3059
+ message: `Failed to install Ollama: ${err.message}`
3060
+ };
3061
+ }
3062
+ } else if (process.platform === "win32") {
3063
+ if (!hasCommand("winget")) {
3064
+ return {
3065
+ success: false,
3066
+ message: "winget was not found. Please install winget first."
3067
+ };
3068
+ }
3069
+ console.log("Installing Ollama via winget...");
3070
+ try {
3071
+ execSync(
3072
+ "winget install -e --id Ollama.Ollama --accept-source-agreements --accept-package-agreements",
3073
+ { stdio: "inherit" }
3074
+ );
3075
+ return { success: true, message: "Ollama installed successfully." };
3076
+ } catch (err) {
3077
+ return {
3078
+ success: false,
3079
+ message: `Failed to install Ollama: ${err.message}`
3080
+ };
3081
+ }
3082
+ } else {
3083
+ console.log("Installing Ollama via official installer script...");
3084
+ try {
3085
+ execSync("curl -fsSL https://ollama.com/install.sh | sh", {
3086
+ stdio: "inherit"
3087
+ });
3088
+ return { success: true, message: "Ollama installed successfully." };
3089
+ } catch (err) {
3090
+ return {
3091
+ success: false,
3092
+ message: `Failed to install Ollama: ${err.message}`
3093
+ };
3094
+ }
1704
3095
  }
1705
- const fmBlock = trimmed.slice(3, endIndex).trim();
1706
- const body = trimmed.slice(endIndex + 3).trim();
1707
- const frontmatter = {};
1708
- for (const line of fmBlock.split("\n")) {
1709
- const colonIndex = line.indexOf(":");
1710
- if (colonIndex === -1) continue;
1711
- const key = line.slice(0, colonIndex).trim();
1712
- const value = line.slice(colonIndex + 1).trim();
1713
- if (key && value) {
1714
- frontmatter[key] = value;
3096
+ }
3097
+
3098
+ // src/kernel/system/locale.ts
3099
+ import { execSync as execSync2 } from "child_process";
3100
+ var SUPPORTED_LOCALES = /* @__PURE__ */ new Set([
3101
+ "en",
3102
+ "de",
3103
+ "es",
3104
+ "fr",
3105
+ "pt",
3106
+ "zh",
3107
+ "ja"
3108
+ ]);
3109
+ function normalizeLocale(raw) {
3110
+ const clean = raw.trim().toLowerCase().split(/[_-]/)[0];
3111
+ if (SUPPORTED_LOCALES.has(clean)) {
3112
+ return clean;
3113
+ }
3114
+ return "en";
3115
+ }
3116
+ function detectSystemLocale() {
3117
+ try {
3118
+ const envVars = [
3119
+ process.env.LANG,
3120
+ process.env.LANGUAGE,
3121
+ process.env.LC_ALL,
3122
+ process.env.LC_MESSAGES
3123
+ ];
3124
+ for (const val of envVars) {
3125
+ if (val && val.trim().length > 0) {
3126
+ return normalizeLocale(val);
3127
+ }
3128
+ }
3129
+ if (process.platform === "win32") {
3130
+ const output = execSync2(
3131
+ 'powershell -NoProfile -Command "[System.Globalization.CultureInfo]::CurrentCulture.Name"',
3132
+ { stdio: "pipe", encoding: "utf8", timeout: 2e3 }
3133
+ ).trim();
3134
+ if (output && output.length > 0) {
3135
+ return normalizeLocale(output);
3136
+ }
1715
3137
  }
3138
+ } catch {
1716
3139
  }
1717
- return { frontmatter, body };
3140
+ return "en";
1718
3141
  }
1719
3142
 
1720
- // src/kernel/goals/engine.ts
1721
- function listGoals(goalsDir) {
1722
- if (!existsSync3(goalsDir)) return [];
1723
- const files = readdirSync(goalsDir).filter(
1724
- (f) => f.endsWith(".md") && f !== "README.md"
1725
- );
1726
- const summaries = [];
1727
- for (const file of files) {
1728
- const filePath = join3(goalsDir, file);
1729
- const content = readFileSync2(filePath, "utf-8");
1730
- const slug = basename(file, ".md");
1731
- const goal = parseGoalFile(content, slug, filePath);
1732
- const tasks = extractTasks(goal.body);
1733
- const tokens = extractTokenRefs(goal.body);
1734
- summaries.push({
1735
- slug: goal.slug,
1736
- title: goal.title,
1737
- status: goal.status,
1738
- parent: goal.parent,
1739
- taskCount: tasks.length,
1740
- tasksDone: tasks.filter((t) => t.done).length,
1741
- tokenCount: tokens.length
1742
- });
3143
+ // src/kernel/system/profiler.ts
3144
+ import { execSync as execSync3 } from "child_process";
3145
+ function runCommand(cmd) {
3146
+ try {
3147
+ return execSync3(cmd, { stdio: "pipe", encoding: "utf8" }).trim();
3148
+ } catch {
3149
+ return "";
1743
3150
  }
1744
- const statusOrder = {
1745
- active: 0,
1746
- paused: 1,
1747
- completed: 2,
1748
- abandoned: 3
1749
- };
1750
- summaries.sort((a, b) => {
1751
- const statusDiff = statusOrder[a.status] - statusOrder[b.status];
1752
- if (statusDiff !== 0) return statusDiff;
1753
- return a.title.localeCompare(b.title);
1754
- });
1755
- return summaries;
1756
3151
  }
1757
- function getGoal(goalsDir, slug) {
1758
- const filePath = join3(goalsDir, `${slug}.md`);
1759
- if (!existsSync3(filePath)) return void 0;
1760
- const content = readFileSync2(filePath, "utf-8");
1761
- return parseGoalFile(content, slug, filePath);
3152
+ function detectWindowsAMDIPU() {
3153
+ if (process.platform !== "win32") return false;
3154
+ const cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*AMD IPU*' -or $_.Name -like '*AMD NPU*' -or $_.Name -like '*NPU Compute*' -or $_.Name -like '*Ryzen AI*' -or $_.HardwareID -like '*VEN_1022&DEV_1502*' -or $_.HardwareID -like '*VEN_1022&DEV_17F0*' } | Select-Object -First 1 -ExpandProperty Name"`;
3155
+ const output = runCommand(cmd);
3156
+ return Boolean(
3157
+ output && (output.toLowerCase().includes("amd") || output.toLowerCase().includes("ipu") || output.toLowerCase().includes("npu") || output.toLowerCase().includes("ryzen"))
3158
+ );
1762
3159
  }
1763
- function createGoal(goalsDir, input) {
1764
- const filePath = join3(goalsDir, `${input.slug}.md`);
1765
- if (existsSync3(filePath)) {
1766
- throw new Error(`Goal already exists: ${input.slug}`);
3160
+ function getSystemProfile() {
3161
+ const platform = process.platform;
3162
+ const archStr = process.arch;
3163
+ let os = "unknown";
3164
+ if (platform === "win32") os = "windows";
3165
+ else if (platform === "darwin") os = "macos";
3166
+ else if (platform === "linux") os = "linux";
3167
+ let arch = "unknown";
3168
+ if (archStr === "x64") arch = "x64";
3169
+ else if (archStr === "arm64") arch = "arm64";
3170
+ const hasRyzenNPU = os === "windows" && detectWindowsAMDIPU();
3171
+ const hasAppleSilicon = os === "macos" && arch === "arm64";
3172
+ let recommendedRunner = "generic";
3173
+ let recommendedModel = "qwen3.5:4b";
3174
+ if (hasRyzenNPU) {
3175
+ recommendedRunner = "fastflowlm";
3176
+ recommendedModel = "qwen3.5:4b";
3177
+ } else if (hasAppleSilicon) {
3178
+ recommendedRunner = "ollama";
3179
+ recommendedModel = "llama3.2:3b";
3180
+ } else if (os === "macos" || os === "linux" || os === "windows") {
3181
+ recommendedRunner = "ollama";
3182
+ recommendedModel = "llama3.2:3b";
1767
3183
  }
1768
- const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1769
- const goal = {
1770
- slug: input.slug,
1771
- title: input.title,
1772
- status: input.status ?? "active",
1773
- parent: input.parent ?? null,
1774
- created: now,
1775
- updated: now,
1776
- body: input.description ? `## Description
1777
- ${input.description}
1778
-
1779
- ## Tasks
3184
+ return {
3185
+ os,
3186
+ arch,
3187
+ hasRyzenNPU,
3188
+ hasAppleSilicon,
3189
+ recommendedRunner,
3190
+ recommendedModel
3191
+ };
3192
+ }
1780
3193
 
1781
- ## Tokens` : "## Description\n\n## Tasks\n\n## Tokens",
1782
- filePath
3194
+ // src/kernel/system/repos.ts
3195
+ import { existsSync as existsSync8 } from "fs";
3196
+ import { resolve as resolve2 } from "path";
3197
+ function getRepoPaths(db) {
3198
+ const personalSetting = getSetting(db, "repo.personal") || getSetting(db, "personal.workspace_dir");
3199
+ const teamSetting = getSetting(db, "repo.team");
3200
+ const orgSetting = getSetting(db, "repo.org");
3201
+ return {
3202
+ personal: personalSetting ? resolve2(personalSetting) : null,
3203
+ team: teamSetting ? resolve2(teamSetting) : null,
3204
+ org: orgSetting ? resolve2(orgSetting) : null
1783
3205
  };
1784
- writeFileSync(filePath, serializeGoal(goal), "utf-8");
1785
- return goal;
1786
3206
  }
1787
- function updateGoalStatus(goalsDir, slug, status) {
1788
- const goal = getGoal(goalsDir, slug);
1789
- if (!goal) throw new Error(`Goal not found: ${slug}`);
1790
- goal.status = status;
1791
- goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1792
- writeFileSync(goal.filePath, serializeGoal(goal), "utf-8");
1793
- return goal;
3207
+ function resolveRepoPath(db, type) {
3208
+ const paths = getRepoPaths(db);
3209
+ return paths[type];
1794
3210
  }
1795
- function getGoalTree(goalsDir) {
1796
- const all = listGoals(goalsDir);
1797
- const bySlug = new Map(all.map((g) => [g.slug, g]));
1798
- const roots = [];
1799
- const children = /* @__PURE__ */ new Map();
1800
- for (const g of all) {
1801
- if (g.parent && bySlug.has(g.parent)) {
1802
- const list = children.get(g.parent) ?? [];
1803
- list.push(g);
1804
- children.set(g.parent, list);
1805
- }
3211
+ function resolveAllBeliefPaths(db) {
3212
+ const paths = getRepoPaths(db);
3213
+ const dirs = [];
3214
+ if (paths.personal) {
3215
+ const personalDir = resolve2(paths.personal, "beliefs");
3216
+ if (existsSync8(personalDir)) dirs.push(personalDir);
1806
3217
  }
1807
- for (const g of all) {
1808
- if (!g.parent || !bySlug.has(g.parent)) {
1809
- roots.push({ ...g, children: children.get(g.slug) ?? [] });
1810
- }
3218
+ if (paths.team) {
3219
+ const teamDir = resolve2(paths.team, "beliefs");
3220
+ if (existsSync8(teamDir)) dirs.push(teamDir);
1811
3221
  }
1812
- return roots;
1813
- }
1814
-
1815
- // src/kernel/connectors/azure-devops.ts
1816
- function loadADOConfig(db) {
1817
- const orgUrl = getSetting(db, "ado.org_url");
1818
- const project = getSetting(db, "ado.project");
1819
- const pat = getSetting(db, "ado.pat");
1820
- if (!orgUrl || !project || !pat) return null;
1821
- return { orgUrl: orgUrl.replace(/\/+$/, ""), project, pat };
1822
- }
1823
- function authHeader(pat) {
1824
- return `Basic ${Buffer.from(`:${pat}`).toString("base64")}`;
3222
+ if (paths.org) {
3223
+ const orgDir = resolve2(paths.org, "beliefs");
3224
+ if (existsSync8(orgDir)) dirs.push(orgDir);
3225
+ }
3226
+ return dirs;
1825
3227
  }
1826
- async function fetchActiveWorkItems(config) {
1827
- const { orgUrl, project, pat } = config;
1828
- const wiqlUrl = `${orgUrl}/${project}/_apis/wit/wiql?api-version=7.1`;
1829
- const wiqlBody = {
1830
- query: `SELECT [System.Id] FROM WorkItems WHERE [System.AssignedTo] = @me AND [System.State] NOT IN ('Closed', 'Completed', 'Done', 'Removed') ORDER BY [Microsoft.VSTS.Common.Priority] ASC, [System.ChangedDate] DESC`
1831
- };
1832
- const wiqlRes = await fetch(wiqlUrl, {
1833
- method: "POST",
1834
- headers: {
1835
- Authorization: authHeader(pat),
1836
- "Content-Type": "application/json"
1837
- },
1838
- body: JSON.stringify(wiqlBody)
1839
- });
1840
- if (!wiqlRes.ok) {
1841
- const text = await wiqlRes.text();
1842
- throw new Error(`ADO WIQL query failed (${wiqlRes.status}): ${text}`);
3228
+ function resolveAllGoalPaths(db) {
3229
+ const paths = getRepoPaths(db);
3230
+ const dirs = [];
3231
+ if (paths.personal) {
3232
+ const personalDir = resolve2(paths.personal, "goals");
3233
+ if (existsSync8(personalDir)) dirs.push(personalDir);
1843
3234
  }
1844
- const wiqlData = await wiqlRes.json();
1845
- const ids = wiqlData.workItems.map((wi) => wi.id);
1846
- if (ids.length === 0) return [];
1847
- const batchIds = ids.slice(0, 200);
1848
- const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
1849
- const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
1850
- const detailRes = await fetch(detailUrl, {
1851
- headers: { Authorization: authHeader(pat) }
1852
- });
1853
- if (!detailRes.ok) {
1854
- const text = await detailRes.text();
1855
- throw new Error(`ADO work items fetch failed (${detailRes.status}): ${text}`);
3235
+ if (paths.team) {
3236
+ const teamDir = resolve2(paths.team, "goals");
3237
+ if (existsSync8(teamDir)) dirs.push(teamDir);
1856
3238
  }
1857
- const detailData = await detailRes.json();
1858
- return detailData.value.map((wi) => ({
1859
- id: wi.id,
1860
- title: wi.fields["System.Title"],
1861
- state: wi.fields["System.State"],
1862
- type: wi.fields["System.WorkItemType"],
1863
- assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
1864
- }));
3239
+ if (paths.org) {
3240
+ const orgDir = resolve2(paths.org, "goals");
3241
+ if (existsSync8(orgDir)) dirs.push(orgDir);
3242
+ }
3243
+ return dirs;
1865
3244
  }
1866
3245
  export {
3246
+ DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
1867
3247
  addPrerequisite,
1868
3248
  analyzeObservation,
1869
3249
  buildReviewQueue,
1870
3250
  cascadeBlock,
3251
+ clearADOCredentials,
3252
+ clearTursoCredentials,
1871
3253
  createAgentSkill,
1872
3254
  createFSRS,
1873
3255
  createGoal,
1874
3256
  createToken,
3257
+ deleteCardForUser,
1875
3258
  deleteSetting,
3259
+ deleteToken,
1876
3260
  deprecateToken,
3261
+ detectSystemLocale,
1877
3262
  discoverSkills,
3263
+ distributeGlobalSkills,
1878
3264
  endSession,
1879
3265
  ensureCard,
1880
3266
  ensureMonitorDir,
1881
3267
  evaluateRating,
3268
+ executeReviewAction,
1882
3269
  extractTasks,
1883
3270
  extractTokenRefs,
1884
3271
  fetchActiveWorkItems,
1885
3272
  findTokens,
1886
3273
  generateBashHooks,
1887
3274
  generateBashUnhooks,
3275
+ generateConceptFreeCue,
3276
+ generatePowerShellHooks,
3277
+ generatePowerShellUnhooks,
1888
3278
  generatePrompt,
1889
3279
  generateZshHooks,
1890
3280
  generateZshUnhooks,
3281
+ getADOCredentials,
1891
3282
  getAgentSkill,
1892
3283
  getAllSettings,
1893
3284
  getAllSettingsDetailed,
1894
3285
  getBlockedCards,
1895
3286
  getCard,
3287
+ getCardById,
3288
+ getCardDeletionImpact,
1896
3289
  getDefaultDbPath,
1897
3290
  getDependents,
1898
3291
  getDomainCompetence,
@@ -1902,34 +3295,57 @@ export {
1902
3295
  getMonitorDir,
1903
3296
  getMonitorLogStats,
1904
3297
  getMonitorPath,
3298
+ getPackageSkillPath,
1905
3299
  getPrerequisites,
3300
+ getRepoPaths,
1906
3301
  getReviewsForCard,
1907
3302
  getReviewsForUser,
1908
3303
  getSessionSummary,
1909
3304
  getSetting,
3305
+ getSystemProfile,
1910
3306
  getTokenById,
1911
3307
  getTokenBySlug,
3308
+ getTokenDeleteImpact,
3309
+ getTursoCredentials,
1912
3310
  getUserStats,
3311
+ hasCommand,
3312
+ injectShellHooks,
3313
+ installFastFlowLM,
3314
+ installOllama,
1913
3315
  interleave,
1914
3316
  listAgentSkills,
1915
3317
  listGoals,
1916
3318
  listTokens,
1917
3319
  loadADOConfig,
3320
+ loadCredentials,
1918
3321
  logReview,
1919
3322
  logStep,
3323
+ matchesFilePath,
1920
3324
  monitorLogExists,
3325
+ normalizeLocale,
3326
+ normalizePath,
1921
3327
  openDatabase,
1922
3328
  openDatabaseWithSync,
1923
3329
  pairCommands,
1924
3330
  parseGoalFile,
1925
3331
  parseMonitorLog,
1926
3332
  readMonitorLog,
3333
+ resolveAllBeliefPaths,
3334
+ resolveAllGoalPaths,
3335
+ resolveReference,
3336
+ resolveRepoPath,
3337
+ resolveReviewContext,
3338
+ saveCredentials,
1927
3339
  serializeGoal,
3340
+ setADOCredentials,
1928
3341
  setSetting,
3342
+ setTursoCredentials,
1929
3343
  startSession,
3344
+ t,
1930
3345
  unblockReady,
1931
3346
  updateCard,
1932
3347
  updateGoalStatus,
3348
+ updateToken,
1933
3349
  writeMonitorEvent
1934
3350
  };
1935
3351
  //# sourceMappingURL=index.js.map