zam-core 0.3.2 → 0.3.5

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,52 +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;
131
373
  const metaPath = `${dbPath}.meta`;
132
- if (existsSync(dbPath) && !existsSync(metaPath)) {
374
+ const infoPath = `${dbPath}-info`;
375
+ if (existsSync2(dbPath) && !existsSync2(metaPath) && !existsSync2(infoPath)) {
133
376
  for (const suffix of ["", "-wal", "-shm"]) {
134
377
  const f = `${dbPath}${suffix}`;
135
- if (existsSync(f)) rmSync(f);
378
+ if (existsSync2(f)) rmSync(f, { force: true });
136
379
  }
380
+ } else if (!existsSync2(dbPath) && (existsSync2(metaPath) || existsSync2(infoPath))) {
381
+ if (existsSync2(metaPath)) rmSync(metaPath);
382
+ if (existsSync2(infoPath)) rmSync(infoPath);
383
+ }
384
+ }
385
+ const authToken = configuredCloud?.token ?? options.authToken;
386
+ if (authToken) {
387
+ dbOpts.authToken = authToken;
388
+ }
389
+ let db;
390
+ try {
391
+ db = new Database(dbPath, dbOpts);
392
+ } catch (err) {
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);
399
+ db = new Database(dbPath, dbOpts);
400
+ } else {
401
+ throw err;
137
402
  }
138
403
  }
139
- if (options.authToken) {
140
- dbOpts.authToken = options.authToken;
404
+ if (!isRemote && !isEmbeddedReplica) {
405
+ db.pragma("journal_mode = WAL");
141
406
  }
142
- const db = new Database(dbPath, dbOpts);
143
- db.pragma("journal_mode = WAL");
144
407
  db.pragma("foreign_keys = ON");
145
- db.pragma("busy_timeout = 5000");
408
+ if (!isRemote) {
409
+ db.pragma("busy_timeout = 5000");
410
+ }
411
+ if (isEmbeddedReplica) {
412
+ db.sync();
413
+ }
146
414
  if (options.initialize) {
147
415
  db.exec(SCHEMA);
148
416
  }
149
417
  runMigrations(db);
150
- if (options.syncUrl) {
151
- db.sync();
152
- }
153
418
  return db;
154
419
  }
155
420
  function openDatabaseWithSync(options = {}) {
156
- const db = openDatabase(options);
157
- const syncUrl = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.url");
158
- const authToken = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.token");
159
- if (!syncUrl || !authToken) return db;
160
- db.pragma("wal_checkpoint(TRUNCATE)");
161
- db.pragma("journal_mode = DELETE");
162
- db.close();
163
- return openDatabase({ ...options, syncUrl: syncUrl.value, authToken: authToken.value });
421
+ return openDatabase(options);
164
422
  }
165
423
  function getDefaultDbPath() {
166
424
  return DEFAULT_DB_PATH;
@@ -176,6 +434,12 @@ function runMigrations(db) {
176
434
  if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
177
435
  db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
178
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
+ }
179
443
  db.exec(`
180
444
  CREATE TABLE IF NOT EXISTS agent_skills (
181
445
  id TEXT PRIMARY KEY,
@@ -190,111 +454,232 @@ function runMigrations(db) {
190
454
  `);
191
455
  }
192
456
 
193
- // src/kernel/models/token.ts
194
- import { ulid } from "ulid";
195
- function createToken(db, input) {
196
- const id = ulid();
197
- const now = (/* @__PURE__ */ new Date()).toISOString();
198
- const bloom = input.bloom_level ?? 1;
199
- if (bloom < 1 || bloom > 5) {
200
- throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
201
- }
202
- db.prepare(`
203
- INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, created_at, updated_at)
204
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
205
- `).run(
206
- id,
207
- input.slug,
208
- input.concept,
209
- input.domain ?? "",
210
- bloom,
211
- input.context ?? "",
212
- input.symbiosis_mode ?? null,
213
- now,
214
- now
215
- );
216
- return getTokenById(db, id);
217
- }
218
- function getTokenBySlug(db, slug) {
219
- return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
220
- }
221
- function getTokenById(db, id) {
222
- 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
+ };
223
482
  }
224
- function deprecateToken(db, slug) {
225
- const token = getTokenBySlug(db, slug);
226
- if (!token) {
227
- 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}`);
228
487
  }
229
- if (token.deprecated_at) {
230
- 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("");
231
495
  }
232
- const now = (/* @__PURE__ */ new Date()).toISOString();
233
- db.prepare("UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?").run(
234
- now,
235
- now,
236
- slug
237
- );
238
- return getTokenBySlug(db, slug);
496
+ return lines.join("\n");
239
497
  }
240
- function findTokens(db, query) {
241
- const normalised = query.toLowerCase();
242
- const qTokens = new Set(
243
- normalised.split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter((t) => t.length > 2)
244
- );
245
- const tokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
246
- const scored = [];
247
- for (const t of tokens) {
248
- const words = (t.slug + " " + t.concept + " " + t.domain).toLowerCase().split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter(Boolean);
249
- let score = 0;
250
- for (const w of words) {
251
- if (qTokens.has(w)) score++;
252
- }
253
- if (t.concept.toLowerCase().includes(normalised.slice(0, 25))) {
254
- score += 3;
255
- }
256
- if (score > 0) {
257
- scored.push({ score, ...t });
258
- }
259
- }
260
- scored.sort((a, b) => b.score - a.score);
261
- return scored;
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;
262
510
  }
263
- function listTokens(db, options) {
264
- if (options?.domain) {
265
- return db.prepare(
266
- "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
267
- ).all(options.domain);
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]);
520
+ }
268
521
  }
269
- return db.prepare(
270
- "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
271
- ).all();
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;
543
+ }
544
+ }
545
+ return { frontmatter, body };
272
546
  }
273
547
 
274
- // src/kernel/models/prerequisite.ts
275
- function addPrerequisite(db, tokenId, requiresId) {
276
- if (tokenId === requiresId) {
277
- throw new Error("A token cannot be a prerequisite of itself");
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
+ });
571
+ }
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);
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}
606
+
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}`);
278
656
  }
657
+ const id = ulid();
658
+ const now = (/* @__PURE__ */ new Date()).toISOString();
279
659
  db.prepare(
280
- "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
281
- ).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
+ );
282
675
  }
283
- function getPrerequisites(db, tokenId) {
284
- return db.prepare(
285
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
286
- FROM prerequisites p
287
- JOIN tokens t ON t.id = p.requires_id
288
- WHERE p.token_id = ?`
289
- ).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;
290
679
  }
291
- function getDependents(db, tokenId) {
292
- return db.prepare(
293
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
294
- FROM prerequisites p
295
- JOIN tokens t ON t.id = p.token_id
296
- WHERE p.requires_id = ?`
297
- ).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);
298
683
  }
299
684
 
300
685
  // src/kernel/models/card.ts
@@ -313,6 +698,9 @@ function ensureCard(db, tokenId, userId) {
313
698
  function getCard(db, tokenId, userId) {
314
699
  return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
315
700
  }
701
+ function getCardById(db, cardId) {
702
+ return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
703
+ }
316
704
  function updateCard(db, cardId, updates) {
317
705
  const fields = [];
318
706
  const values = [];
@@ -366,6 +754,23 @@ function updateCard(db, cardId, updates) {
366
754
  }
367
755
  return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
368
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
+ }
369
774
  function getDueCards(db, userId, now) {
370
775
  const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
371
776
  return db.prepare(
@@ -386,6 +791,32 @@ function getBlockedCards(db, userId) {
386
791
  ).all(userId);
387
792
  }
388
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
+
389
820
  // src/kernel/models/review.ts
390
821
  import { ulid as ulid3 } from "ulid";
391
822
  function logReview(db, input) {
@@ -455,12 +886,17 @@ function endSession(db, sessionId) {
455
886
  throw new Error(`Session already completed: ${sessionId}`);
456
887
  }
457
888
  const now = (/* @__PURE__ */ new Date()).toISOString();
458
- 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
+ );
459
893
  return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
460
894
  }
461
895
  function logStep(db, input) {
462
896
  if (input.done_by !== "user" && input.done_by !== "agent") {
463
- 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
+ );
464
900
  }
465
901
  if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
466
902
  throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
@@ -500,58 +936,16 @@ function getSessionSummary(db, sessionId) {
500
936
  return { session, steps };
501
937
  }
502
938
 
503
- // src/kernel/models/agent-skill.ts
504
- import { ulid as ulid5 } from "ulid";
505
- function parseRow(row) {
506
- return {
507
- ...row,
508
- steps: JSON.parse(row.steps),
509
- token_slugs: JSON.parse(row.token_slugs)
510
- };
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;
511
943
  }
512
- function createAgentSkill(db, input) {
513
- const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
514
- if (existing) {
515
- throw new Error(`Agent skill already exists: ${input.slug}`);
516
- }
517
- const id = ulid5();
518
- const now = (/* @__PURE__ */ new Date()).toISOString();
519
- db.prepare(
520
- `INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
521
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
522
- ).run(
523
- id,
524
- input.slug,
525
- input.description,
526
- JSON.stringify(input.steps),
527
- JSON.stringify(input.token_slugs ?? []),
528
- input.source ?? "learned",
529
- now,
530
- now
531
- );
532
- return parseRow(
533
- db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
534
- );
535
- }
536
- function getAgentSkill(db, slug) {
537
- const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
538
- return row ? parseRow(row) : void 0;
539
- }
540
- function listAgentSkills(db) {
541
- const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
542
- return rows.map(parseRow);
543
- }
544
-
545
- // src/kernel/models/settings.ts
546
- function getSetting(db, key) {
547
- const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
548
- return row?.value;
549
- }
550
- function getAllSettings(db) {
551
- const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
552
- const map = {};
553
- for (const row of rows) {
554
- map[row.key] = row.value;
944
+ function getAllSettings(db) {
945
+ const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
946
+ const map = {};
947
+ for (const row of rows) {
948
+ map[row.key] = row.value;
555
949
  }
556
950
  return map;
557
951
  }
@@ -570,1323 +964,2328 @@ function deleteSetting(db, key) {
570
964
  return result.changes > 0;
571
965
  }
572
966
 
573
- // src/kernel/scheduler/fsrs.ts
574
- var DEFAULT_W = [
575
- 0.4072,
576
- 1.1829,
577
- 3.1262,
578
- 15.4722,
579
- // w0–w3: initial stability per rating
580
- 7.2102,
581
- 0.5316,
582
- 1.0651,
583
- // w4–w6: difficulty
584
- 92e-4,
585
- 1.5988,
586
- 0.1176,
587
- 1.0014,
588
- // w7–w10: stability after forgetting
589
- 2.0032,
590
- 0.0266,
591
- 0.3077,
592
- 0.15,
593
- // w11–w14: stability increase
594
- 0,
595
- 2.7849,
596
- 0.3477,
597
- 0.6831
598
- // w15–w18: additional parameters
599
- ];
600
- var DEFAULT_REQUEST_RETENTION = 0.9;
601
- function clamp(value, lo, hi) {
602
- return Math.min(hi, Math.max(lo, value));
603
- }
604
- function daysBetween(a, b) {
605
- return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
606
- }
607
- function initialStability(w, rating) {
608
- return w[rating - 1];
609
- }
610
- function initialDifficulty(w, rating) {
611
- return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
612
- }
613
- function nextDifficulty(w, d, rating) {
614
- const d0ForGood = initialDifficulty(w, 3);
615
- const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
616
- return clamp(updated, 1, 10);
617
- }
618
- function retrievability(elapsed, stability) {
619
- if (stability <= 0) return 0;
620
- return Math.pow(1 + elapsed / (9 * stability), -1);
621
- }
622
- function stabilityAfterSuccess(w, s, d, r, rating) {
623
- const hardPenalty = rating === 2 ? w[15] : 1;
624
- const easyBonus = rating === 4 ? w[16] : 1;
625
- const inner = Math.exp(w[8]) * (11 - d) * Math.pow(s, -w[9]) * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
626
- 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);
627
993
  }
628
- function stabilityAfterForgetting(w, s, d, r) {
629
- 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);
630
996
  }
631
- function nextInterval(stability, requestRetention) {
632
- const interval = 9 * stability * (1 / requestRetention - 1);
633
- return Math.max(1, Math.round(interval));
997
+ function getTokenById(db, id) {
998
+ return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
634
999
  }
635
- function createFSRS(params) {
636
- const resolvedParams = {
637
- w: params?.w ?? [...DEFAULT_W],
638
- requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
639
- };
640
- function schedule(card, rating, now) {
641
- const reviewTime = now ?? /* @__PURE__ */ new Date();
642
- const w = resolvedParams.w;
643
- const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
644
- if (card.state === "new") {
645
- const s = initialStability(w, rating);
646
- const d = initialDifficulty(w, rating);
647
- const interval2 = nextInterval(s, resolvedParams.requestRetention);
648
- const dueAt2 = new Date(reviewTime);
649
- dueAt2.setDate(dueAt2.getDate() + interval2);
650
- return {
651
- stability: s,
652
- difficulty: d,
653
- elapsedDays: 0,
654
- scheduledDays: interval2,
655
- reps: rating >= 2 ? 1 : 0,
656
- lapses: rating === 1 ? 1 : 0,
657
- state: "learning",
658
- dueAt: dueAt2,
659
- lastReviewAt: reviewTime
660
- };
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
+ );
661
1020
  }
662
- const r = retrievability(elapsed, card.stability);
663
- let newStability;
664
- let newDifficulty;
665
- let newReps;
666
- let newLapses;
667
- let newState;
668
- if (rating === 1) {
669
- newStability = stabilityAfterForgetting(w, card.stability, card.difficulty, r);
670
- newDifficulty = nextDifficulty(w, card.difficulty, rating);
671
- newReps = 0;
672
- newLapses = card.lapses + 1;
673
- newState = "relearning";
674
- } else {
675
- newStability = stabilityAfterSuccess(w, card.stability, card.difficulty, r, rating);
676
- newDifficulty = nextDifficulty(w, card.difficulty, rating);
677
- newReps = card.reps + 1;
678
- newLapses = card.lapses;
679
- 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}`);
680
1032
  }
681
- const interval = nextInterval(newStability, resolvedParams.requestRetention);
682
- const dueAt = new Date(reviewTime);
683
- dueAt.setDate(dueAt.getDate() + interval);
684
- return {
685
- stability: newStability,
686
- difficulty: newDifficulty,
687
- elapsedDays: elapsed,
688
- scheduledDays: interval,
689
- reps: newReps,
690
- lapses: newLapses,
691
- state: newState,
692
- dueAt,
693
- lastReviewAt: reviewTime
694
- };
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}`);
695
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;
696
1087
  return {
697
- schedule,
698
- 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
699
1095
  };
700
1096
  }
701
-
702
- // src/kernel/scheduler/blocker.ts
703
- function cascadeBlock(db, userId, tokenSlug) {
704
- const token = getTokenBySlug(db, tokenSlug);
1097
+ function deleteToken(db, slug) {
1098
+ const token = getTokenBySlug(db, slug);
705
1099
  if (!token) {
706
- throw new Error(`Unknown token slug: ${tokenSlug}`);
1100
+ throw new Error(`Token not found: ${slug}`);
707
1101
  }
708
- ensureCard(db, token.id, userId);
709
- db.prepare(
710
- "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
711
- ).run(token.id, userId);
712
- const prereqs = getPrerequisites(db, token.id);
713
- const surfaced = [];
714
- for (const prereq of prereqs) {
715
- const card = ensureCard(db, prereq.requires_id, userId);
716
- if (card.blocked === 1) {
717
- const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
718
- if (prereqOfPrereq.n === 0) {
719
- 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) {
720
1111
  db.prepare(
721
- "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
722
- ).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);
723
1114
  }
724
1115
  }
725
- surfaced.push({
726
- slug: prereq.slug,
727
- concept: prereq.concept,
728
- bloomLevel: prereq.bloom_level
729
- });
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;
730
1121
  }
731
- return {
732
- blockedSlug: tokenSlug,
733
- prerequisites: surfaced
734
- };
1122
+ return { token, impact };
735
1123
  }
736
- function unblockReady(db, userId) {
737
- const blockedCards = db.prepare(
738
- `SELECT c.token_id, t.slug, t.concept
739
- FROM cards c
740
- JOIN tokens t ON t.id = c.token_id
741
- WHERE c.user_id = ? AND c.blocked = 1`
742
- ).all(userId);
743
- const unblocked = [];
744
- for (const card of blockedCards) {
745
- const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
746
- const metPrereqs = db.prepare(
747
- `SELECT COUNT(*) as n FROM cards c
748
- JOIN prerequisites p ON p.requires_id = c.token_id
749
- WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
750
- ).get(card.token_id, userId);
751
- if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
752
- const now = (/* @__PURE__ */ new Date()).toISOString();
753
- db.prepare(
754
- "UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
755
- ).run(now, card.token_id, userId);
756
- 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 });
757
1142
  }
758
1143
  }
759
- 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();
760
1156
  }
761
1157
 
762
- // src/kernel/scheduler/interleaver.ts
763
- function interleave(items, maxConsecutive = 2) {
764
- if (items.length <= 1) return [...items];
765
- const byDomain = /* @__PURE__ */ new Map();
766
- for (const item of items) {
767
- const group = byDomain.get(item.domain);
768
- if (group) {
769
- group.push(item);
770
- } else {
771
- 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 {
772
1167
  }
773
1168
  }
774
- if (byDomain.size === 1) return [...items];
775
- const result = [];
776
- let consecutiveCount = 0;
777
- let lastDomain = null;
778
- const cursors = /* @__PURE__ */ new Map();
779
- for (const domain of byDomain.keys()) {
780
- 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
+ }
781
1197
  }
782
- while (result.length < items.length) {
783
- const activeDomains = [...byDomain.entries()].filter(([domain]) => cursors.get(domain) < byDomain.get(domain).length).sort((a, b) => {
784
- const remainA = a[1].length - cursors.get(a[0]);
785
- const remainB = b[1].length - cursors.get(b[0]);
786
- 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
787
1208
  });
788
- if (activeDomains.length === 0) break;
789
- let pickedThisRound = false;
790
- for (const [domain, group] of activeDomains) {
791
- const cursor = cursors.get(domain);
792
- if (cursor >= group.length) continue;
793
- if (domain === lastDomain && consecutiveCount >= maxConsecutive) {
794
- continue;
795
- }
796
- result.push(group[cursor]);
797
- cursors.set(domain, cursor + 1);
798
- pickedThisRound = true;
799
- if (domain === lastDomain) {
800
- consecutiveCount++;
801
- } else {
802
- lastDomain = domain;
803
- 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);
804
1245
  }
805
- break;
806
1246
  }
807
- if (!pickedThisRound) {
808
- for (const [domain, group] of activeDomains) {
809
- const cursor = cursors.get(domain);
810
- if (cursor >= group.length) continue;
811
- result.push(group[cursor]);
812
- cursors.set(domain, cursor + 1);
813
- if (domain === lastDomain) {
814
- consecutiveCount++;
815
- } else {
816
- lastDomain = domain;
817
- 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
+ }
818
1275
  }
819
- 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++;
820
1283
  }
821
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();
1309
+ }
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
+ });
822
1335
  }
823
- return result;
824
- }
825
-
826
- // src/kernel/scheduler/queue.ts
827
- function buildReviewQueue(db, options) {
828
- const maxNew = options.maxNew ?? 10;
829
- const maxReviews = options.maxReviews ?? 50;
830
- const now = options.now ?? /* @__PURE__ */ new Date();
831
- const nowISO = now.toISOString();
832
- const dueRows = db.prepare(
833
- `SELECT
834
- c.id AS card_id,
835
- c.token_id AS token_id,
836
- t.slug AS slug,
837
- t.concept AS concept,
838
- t.domain AS domain,
839
- t.bloom_level AS bloom_level,
840
- c.state AS state,
841
- c.due_at AS due_at
842
- FROM cards c
843
- JOIN tokens t ON t.id = c.token_id
844
- WHERE c.user_id = ?
845
- AND c.blocked = 0
846
- AND c.due_at <= ?
847
- AND c.state IN ('review', 'relearning', 'learning')
848
- AND t.deprecated_at IS NULL
849
- ORDER BY c.due_at ASC`
850
- ).all(options.userId, nowISO);
851
- const newRows = db.prepare(
852
- `SELECT
853
- c.id AS card_id,
854
- c.token_id AS token_id,
855
- t.slug AS slug,
856
- t.concept AS concept,
857
- t.domain AS domain,
858
- t.bloom_level AS bloom_level,
859
- c.state AS state,
860
- c.due_at AS due_at
861
- FROM cards c
862
- JOIN tokens t ON t.id = c.token_id
863
- WHERE c.user_id = ?
864
- AND c.blocked = 0
865
- AND c.state = 'new'
866
- AND t.deprecated_at IS NULL
867
- ORDER BY t.bloom_level ASC, t.slug ASC
868
- LIMIT ?`
869
- ).all(options.userId, maxNew);
870
- const nowMs = now.getTime();
871
- const sortedDue = [...dueRows].sort((a, b) => {
872
- const overdueA = nowMs - new Date(a.due_at).getTime();
873
- const overdueB = nowMs - new Date(b.due_at).getTime();
874
- return overdueB - overdueA;
875
- });
876
- const interleavedDue = interleave(
877
- sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
878
- );
879
- const newItems = newRows.map(rowToItem);
880
- const merged = intersperseNew(interleavedDue, newItems, 5);
881
- const capped = merged.slice(0, maxReviews);
882
- let newCount = 0;
883
- let reviewCount = 0;
884
- let relearnCount = 0;
885
- const domainSet = /* @__PURE__ */ new Set();
886
- for (const item of capped) {
887
- domainSet.add(item.domain);
888
- switch (item.state) {
889
- case "new":
890
- newCount++;
891
- break;
892
- case "relearning":
893
- relearnCount++;
894
- break;
895
- default:
896
- reviewCount++;
897
- 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);
898
1340
  }
899
1341
  }
900
- return {
901
- items: capped,
902
- newCount,
903
- reviewCount,
904
- relearnCount,
905
- totalDomains: [...domainSet].sort()
906
- };
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 };
907
1354
  }
908
- function rowToItem(row) {
909
- return {
910
- cardId: row.card_id,
911
- tokenId: row.token_id,
912
- slug: row.slug,
913
- concept: row.concept,
914
- domain: row.domain,
915
- bloomLevel: row.bloom_level,
916
- state: row.state,
917
- dueAt: row.due_at
918
- };
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;
919
1376
  }
920
- function intersperseNew(reviews, newCards, interval) {
921
- if (newCards.length === 0) return [...reviews];
922
- if (reviews.length === 0) return [...newCards];
923
- const result = [];
924
- let reviewIdx = 0;
925
- let newIdx = 0;
926
- let position = 0;
927
- while (reviewIdx < reviews.length || newIdx < newCards.length) {
928
- if (newIdx < newCards.length && position > 0 && position % interval === interval - 1) {
929
- result.push(newCards[newIdx]);
930
- newIdx++;
931
- } else if (reviewIdx < reviews.length) {
932
- result.push(reviews[reviewIdx]);
933
- reviewIdx++;
934
- } else if (newIdx < newCards.length) {
935
- result.push(newCards[newIdx]);
936
- newIdx++;
937
- }
938
- 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 });
939
1398
  }
940
- return result;
1399
+ }
1400
+ function writeMonitorEvent(sessionId, event) {
1401
+ ensureMonitorDir();
1402
+ const path = getMonitorPath(sessionId);
1403
+ appendFileSync(path, `${JSON.stringify(event)}
1404
+ `);
1405
+ }
1406
+ function readMonitorLog(sessionId) {
1407
+ const path = getMonitorPath(sessionId);
1408
+ if (!existsSync4(path)) {
1409
+ return [];
1410
+ }
1411
+ const content = readFileSync4(path, "utf-8");
1412
+ return parseMonitorLog(content);
1413
+ }
1414
+ function monitorLogExists(sessionId) {
1415
+ return existsSync4(getMonitorPath(sessionId));
1416
+ }
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 };
941
1426
  }
942
1427
 
943
- // src/kernel/recall/prompter.ts
944
- var BLOOM_VERBS = {
945
- 1: "Remember",
946
- 2: "Understand",
947
- 3: "Apply",
948
- 4: "Analyze",
949
- 5: "Synthesize"
950
- };
951
- var BLOOM_PROMPTS = {
952
- 1: (c) => `What is: ${c}?`,
953
- 2: (c) => `Explain how this works: ${c}`,
954
- 3: (c) => `Apply this concept: ${c}`,
955
- 4: (c) => `Analyze the trade-offs: ${c}`,
956
- 5: (c) => `Design a solution using: ${c}`
957
- };
958
- function generatePrompt(input) {
959
- const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
960
- return {
961
- cardId: input.cardId,
962
- tokenId: input.tokenId,
963
- slug: input.slug,
964
- question: BLOOM_PROMPTS[bloom](input.concept),
965
- concept: input.concept,
966
- domain: input.domain,
967
- bloomLevel: bloom,
968
- bloomVerb: BLOOM_VERBS[bloom],
969
- hints: []
970
- };
1428
+ // src/kernel/observation/shell-hooks.ts
1429
+ function psSingleQuoted(value) {
1430
+ return `'${value.replace(/'/g, "''")}'`;
971
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}"
972
1438
 
973
- // src/kernel/recall/evaluator.ts
974
- import { ulid as ulid6 } from "ulid";
975
- function evaluateRating(db, input) {
976
- const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
977
- if (!card) {
978
- throw new Error(`Card not found: ${input.cardId}`);
979
- }
980
- const now = /* @__PURE__ */ new Date();
981
- const fsrs = createFSRS();
982
- const schedulingCard = {
983
- stability: card.stability,
984
- difficulty: card.difficulty,
985
- elapsedDays: card.elapsed_days,
986
- scheduledDays: card.scheduled_days,
987
- reps: card.reps,
988
- lapses: card.lapses,
989
- state: card.state,
990
- dueAt: new Date(card.due_at),
991
- lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
992
- };
993
- const updated = fsrs.schedule(schedulingCard, input.rating, now);
994
- updateCard(db, input.cardId, {
995
- stability: updated.stability,
996
- difficulty: updated.difficulty,
997
- elapsed_days: updated.elapsedDays,
998
- scheduled_days: updated.scheduledDays,
999
- reps: updated.reps,
1000
- lapses: updated.lapses,
1001
- state: updated.state,
1002
- due_at: updated.dueAt.toISOString(),
1003
- last_review_at: now.toISOString()
1004
- });
1005
- db.prepare(
1006
- `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
1007
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
1008
- ).run(
1009
- ulid6(),
1010
- input.cardId,
1011
- input.tokenId,
1012
- input.userId,
1013
- input.rating,
1014
- input.responseTimeMs ?? null,
1015
- now.toISOString(),
1016
- card.due_at,
1017
- input.sessionId ?? null
1018
- );
1019
- return {
1020
- nextDueAt: updated.dueAt.toISOString(),
1021
- stability: updated.stability,
1022
- difficulty: updated.difficulty,
1023
- state: updated.state,
1024
- scheduledDays: updated.scheduledDays,
1025
- reps: updated.reps,
1026
- lapses: updated.lapses
1027
- };
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
1028
1448
  }
1029
1449
 
1030
- // src/kernel/analytics/stats.ts
1031
- function q(db, sql, ...params) {
1032
- return db.prepare(sql).get(...params);
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"
1033
1458
  }
1034
- function getUserStats(db, userId) {
1035
- return {
1036
- userId,
1037
- totalTokens: q(db, "SELECT COUNT(*) as n FROM tokens").n,
1038
- cardsInDeck: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ?", userId).n,
1039
- dueToday: q(
1040
- db,
1041
- "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
1042
- userId
1043
- ).n,
1044
- blocked: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1", userId).n,
1045
- mature: q(
1046
- db,
1047
- "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
1048
- userId
1049
- ).n,
1050
- avgStability: (() => {
1051
- const v = q(db, "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0", userId);
1052
- return v.v ? Math.round(v.v * 100) / 100 : null;
1053
- })(),
1054
- totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
1055
- lastSession: (() => {
1056
- const r = db.prepare(
1057
- "SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
1058
- ).get(userId);
1059
- return r?.started_at ?? null;
1060
- })()
1061
- };
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"
1062
1467
  }
1063
- function getDomainCompetence(db, userId) {
1064
- const domains = db.prepare(
1065
- `SELECT DISTINCT t.domain FROM cards c
1066
- JOIN tokens t ON t.id = c.token_id
1067
- WHERE c.user_id = ? AND t.domain != ''`
1068
- ).all(userId);
1069
- return domains.map((d) => {
1070
- const total = q(
1071
- db,
1072
- `SELECT COUNT(*) as n FROM cards c
1073
- JOIN tokens t ON t.id = c.token_id
1074
- WHERE c.user_id = ? AND t.domain = ?`,
1075
- userId,
1076
- d.domain
1077
- ).n;
1078
- const mature = q(
1079
- db,
1080
- `SELECT COUNT(*) as n FROM cards c
1081
- JOIN tokens t ON t.id = c.token_id
1082
- WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
1083
- userId,
1084
- d.domain
1085
- ).n;
1086
- const avgStab = q(
1087
- db,
1088
- `SELECT AVG(c.stability) as v FROM cards c
1089
- JOIN tokens t ON t.id = c.token_id
1090
- WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
1091
- userId,
1092
- d.domain
1093
- ).v ?? 0;
1094
- const reviews = q(
1095
- db,
1096
- `SELECT COUNT(*) as total,
1097
- SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
1098
- FROM review_logs
1099
- WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
1100
- userId,
1101
- d.domain
1102
- );
1103
- const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
1104
- let suggestedMode;
1105
- if (retentionRate > 0.9 && avgStab > 30) {
1106
- suggestedMode = "autonomy";
1107
- } else if (retentionRate > 0.7 && avgStab > 7) {
1108
- suggestedMode = "copilot";
1109
- } else {
1110
- suggestedMode = "shadowing";
1111
- }
1112
- return {
1113
- domain: d.domain,
1114
- totalCards: total,
1115
- matureCards: mature,
1116
- avgStability: Math.round(avgStab * 100) / 100,
1117
- retentionRate: Math.round(retentionRate * 1e3) / 1e3,
1118
- suggestedMode
1119
- };
1120
- });
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();
1121
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
1122
1483
 
1123
- // src/kernel/observation/analyzer.ts
1124
- function parseMonitorLog(jsonl) {
1125
- const events = [];
1126
- for (const line of jsonl.split("\n")) {
1127
- const trimmed = line.trim();
1128
- if (!trimmed) continue;
1129
- try {
1130
- events.push(JSON.parse(trimmed));
1131
- } catch {
1132
- }
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")
1133
1536
  }
1134
- return events;
1537
+ return $Date.ToUniversalTime().ToString("o")
1135
1538
  }
1136
- function pairCommands(events) {
1137
- const starts = /* @__PURE__ */ new Map();
1138
- const records = [];
1139
- for (const e of events) {
1140
- if (e.type === "command_start" && e.seq != null) {
1141
- const key = `${e.pid ?? 0}:${e.seq}`;
1142
- starts.set(key, e);
1143
- } else if (e.type === "command_end" && e.seq != null) {
1144
- const key = `${e.pid ?? 0}:${e.seq}`;
1145
- const start = starts.get(key);
1146
- if (start) {
1147
- const startMs = new Date(start.ts).getTime();
1148
- const endMs = new Date(e.ts).getTime();
1149
- records.push({
1150
- seq: e.seq,
1151
- pid: e.pid ?? 0,
1152
- command: start.command ?? "",
1153
- cwd: start.cwd ?? "",
1154
- startedAt: start.ts,
1155
- endedAt: e.ts,
1156
- durationMs: endMs - startMs,
1157
- exitCode: e.exit_code ?? null
1158
- });
1159
- starts.delete(key);
1160
- }
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
1161
1568
  }
1162
1569
  }
1163
- for (const [, start] of starts) {
1164
- records.push({
1165
- seq: start.seq ?? 0,
1166
- pid: start.pid ?? 0,
1167
- command: start.command ?? "",
1168
- cwd: start.cwd ?? "",
1169
- startedAt: start.ts,
1170
- endedAt: null,
1171
- durationMs: null,
1172
- exitCode: null
1173
- });
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
1174
1586
  }
1175
- records.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
1176
- return records;
1177
1587
  }
1178
- var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
1179
- var HELP_WINDOW_MS = 6e4;
1180
- function matchesToken(command, patterns) {
1181
- const lower = command.toLowerCase();
1182
- 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
1183
1591
  }
1184
- function isHelpCommand(command) {
1185
- const lower = command.toLowerCase();
1186
- 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
+ }
1187
1610
  }
1188
- function commandPrefix(command) {
1189
- return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
1611
+
1612
+ Write-Host "ZAM monitor active for session $global:__ZAM_MONITOR_SESSION"
1613
+ `.trim();
1190
1614
  }
1191
- function computeMedian(values) {
1192
- if (values.length === 0) return null;
1193
- const sorted = [...values].sort((a, b) => a - b);
1194
- const mid = Math.floor(sorted.length / 2);
1195
- 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();
1196
1624
  }
1197
- function analyzeObservation(commands, tokenPatterns) {
1198
- const matchedSet = /* @__PURE__ */ new Set();
1199
- const ratings = [];
1200
- for (const tp of tokenPatterns) {
1201
- const matchIndices = [];
1202
- const matchedTexts = [];
1203
- for (let i = 0; i < commands.length; i++) {
1204
- if (matchesToken(commands[i].command, tp.patterns)) {
1205
- matchIndices.push(i);
1206
- matchedTexts.push(commands[i].command);
1207
- matchedSet.add(i);
1208
- }
1209
- }
1210
- if (matchIndices.length === 0) {
1211
- ratings.push({
1212
- tokenSlug: tp.slug,
1213
- rating: null,
1214
- confidence: "low",
1215
- evidence: {
1216
- matchedCommands: 0,
1217
- helpSeeking: false,
1218
- errorCount: 0,
1219
- selfCorrections: 0,
1220
- medianGapMs: null,
1221
- thinkingGapMs: null
1222
- },
1223
- matchedCommandTexts: []
1224
- });
1225
- 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);
1226
1659
  }
1227
- let helpSeeking = false;
1228
- for (const mi of matchIndices) {
1229
- const matchTime = new Date(commands[mi].startedAt).getTime();
1230
- for (let j = 0; j < commands.length; j++) {
1231
- if (j === mi) continue;
1232
- const cmdTime = new Date(commands[j].startedAt).getTime();
1233
- if (cmdTime >= matchTime - HELP_WINDOW_MS && cmdTime < matchTime) {
1234
- if (isHelpCommand(commands[j].command)) {
1235
- helpSeeking = true;
1236
- break;
1237
- }
1238
- }
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
+ });
1239
1679
  }
1240
- if (helpSeeking) break;
1241
1680
  }
1242
- let errorCount = 0;
1243
- for (const mi of matchIndices) {
1244
- if (commands[mi].exitCode != null && commands[mi].exitCode !== 0) {
1245
- 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);
1246
1688
  }
1247
1689
  }
1248
- let selfCorrections = 0;
1249
- const prefixGroups = /* @__PURE__ */ new Map();
1250
- for (const mi of matchIndices) {
1251
- const prefix = commandPrefix(commands[mi].command);
1252
- prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
1253
- }
1254
- for (const count of prefixGroups.values()) {
1255
- 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();
1256
1723
  }
1257
- const gaps = [];
1258
- for (let k = 1; k < matchIndices.length; k++) {
1259
- const prev = commands[matchIndices[k - 1]];
1260
- const curr = commands[matchIndices[k]];
1261
- if (prev.endedAt) {
1262
- const gap = new Date(curr.startedAt).getTime() - new Date(prev.endedAt).getTime();
1263
- 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);
1264
1739
  }
1265
1740
  }
1266
- let thinkingGapMs = null;
1267
- const firstMatchIdx = matchIndices[0];
1268
- if (firstMatchIdx > 0) {
1269
- const prev = commands[firstMatchIdx - 1];
1270
- if (prev.endedAt) {
1271
- 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;
1272
1755
  }
1273
1756
  }
1274
- const medianGapMs = computeMedian(gaps);
1275
- const rating = inferRating({
1276
- helpSeeking,
1277
- errorCount,
1278
- selfCorrections,
1279
- medianGapMs,
1280
- thinkingGapMs,
1281
- 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;
1282
1773
  });
1283
- const confidence = matchIndices.length >= 3 ? "high" : matchIndices.length >= 2 ? "medium" : "low";
1284
- ratings.push({
1285
- tokenSlug: tp.slug,
1286
- rating,
1287
- confidence,
1288
- evidence: {
1289
- matchedCommands: matchIndices.length,
1290
- helpSeeking,
1291
- errorCount,
1292
- selfCorrections,
1293
- medianGapMs,
1294
- thinkingGapMs
1295
- },
1296
- 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
1297
1817
  });
1298
1818
  }
1299
- const unmatchedCommands = [];
1300
- for (let i = 0; i < commands.length; i++) {
1301
- if (!matchedSet.has(i) && !isHelpCommand(commands[i].command)) {
1302
- 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 });
1303
1845
  }
1304
1846
  }
1305
- let timeSpan = null;
1306
- if (commands.length > 0) {
1307
- const first = commands[0];
1308
- const last = commands[commands.length - 1];
1309
- const endTs = last.endedAt ?? last.startedAt;
1310
- timeSpan = {
1311
- start: first.startedAt,
1312
- end: endTs,
1313
- 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
1314
1988
  };
1315
1989
  }
1316
- return { ratings, unmatchedCommands, timeSpan };
1990
+ return {
1991
+ schedule,
1992
+ params: Object.freeze(resolvedParams)
1993
+ };
1317
1994
  }
1318
- function inferRating(signals) {
1319
- const { helpSeeking, errorCount, selfCorrections, medianGapMs, thinkingGapMs } = signals;
1320
- let negatives = 0;
1321
- if (helpSeeking) negatives += 2;
1322
- if (errorCount >= 3) negatives += 3;
1323
- else if (errorCount >= 1) negatives += 1;
1324
- if (selfCorrections >= 2) negatives += 2;
1325
- else if (selfCorrections >= 1) negatives += 1;
1326
- if (medianGapMs != null && medianGapMs > 3e4) negatives += 2;
1327
- else if (medianGapMs != null && medianGapMs > 1e4) negatives += 1;
1328
- if (thinkingGapMs != null && thinkingGapMs > 3e4) negatives += 1;
1329
- if (negatives >= 5) return 1;
1330
- if (negatives >= 3) return 2;
1331
- if (negatives >= 1) return 3;
1332
- 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
+ };
1333
2050
  }
1334
2051
 
1335
- // src/kernel/observation/monitor-io.ts
1336
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, appendFileSync, statSync } from "fs";
1337
- import { homedir as homedir2 } from "os";
1338
- import { join as join2 } from "path";
1339
- var MONITOR_DIR = join2(homedir2(), ".zam", "monitor");
1340
- function getMonitorDir() {
1341
- return MONITOR_DIR;
1342
- }
1343
- function getMonitorPath(sessionId) {
1344
- return join2(MONITOR_DIR, `${sessionId}.jsonl`);
1345
- }
1346
- function ensureMonitorDir() {
1347
- if (!existsSync2(MONITOR_DIR)) {
1348
- 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}`);
1349
2057
  }
1350
- }
1351
- function writeMonitorEvent(sessionId, event) {
1352
- ensureMonitorDir();
1353
- const path = getMonitorPath(sessionId);
1354
- appendFileSync(path, JSON.stringify(event) + "\n");
1355
- }
1356
- function readMonitorLog(sessionId) {
1357
- const path = getMonitorPath(sessionId);
1358
- if (!existsSync2(path)) {
1359
- return [];
2058
+ if (card.user_id !== userId) {
2059
+ throw new Error(`Card ${cardId} does not belong to user ${userId}`);
1360
2060
  }
1361
- const content = readFileSync(path, "utf-8");
1362
- return parseMonitorLog(content);
1363
- }
1364
- function monitorLogExists(sessionId) {
1365
- return existsSync2(getMonitorPath(sessionId));
1366
- }
1367
- function getMonitorLogStats(sessionId) {
1368
- const path = getMonitorPath(sessionId);
1369
- if (!existsSync2(path)) {
1370
- 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}`);
1371
2064
  }
1372
- const stat = statSync(path);
1373
- const content = readFileSync(path, "utf-8");
1374
- const lineCount = content.split("\n").filter((l) => l.trim()).length;
1375
- return { exists: true, sizeBytes: stat.size, lineCount };
2065
+ return { cardId: card.id, token };
1376
2066
  }
1377
-
1378
- // src/kernel/observation/shell-hooks.ts
1379
- function generateZshHooks(monitorFile, sessionId) {
1380
- return `
1381
- # ZAM monitor hooks for session ${sessionId}
1382
- export __ZAM_MONITOR_FILE="${monitorFile}"
1383
- export __ZAM_MONITOR_SEQ=0
1384
- export __ZAM_MONITOR_SESSION="${sessionId}"
1385
-
1386
- __zam_ts() {
1387
- if [[ -n "\${EPOCHREALTIME:-}" ]]; then
1388
- local sec="\${EPOCHREALTIME%%.*}"
1389
- local frac="\${EPOCHREALTIME##*.}"
1390
- frac="\${frac:0:3}"
1391
- 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"
1392
- else
1393
- date -u '+%Y-%m-%dT%H:%M:%SZ'
1394
- fi
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
+ }
1395
2139
  }
1396
2140
 
1397
- __zam_preexec() {
1398
- (( __ZAM_MONITOR_SEQ++ ))
1399
- local cmd="\${1//\\"/\\\\\\"}"
1400
- local cwd="\${PWD//\\"/\\\\\\"}"
1401
- local ts="$(__zam_ts)"
1402
- printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
1403
- "$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
1404
- >> "$__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);
1405
2162
  }
1406
-
1407
- __zam_precmd() {
1408
- local exit_code=$?
1409
- [[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
1410
- local ts="$(__zam_ts)"
1411
- printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
1412
- "$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
1413
- >> "$__ZAM_MONITOR_FILE"
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
+ };
1414
2178
  }
1415
2179
 
1416
- autoload -Uz add-zsh-hook
1417
- add-zsh-hook preexec __zam_preexec
1418
- add-zsh-hook precmd __zam_precmd
1419
-
1420
- echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
1421
- `.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
+ };
1422
2321
  }
1423
- function generateBashHooks(monitorFile, sessionId) {
1424
- return `
1425
- # ZAM monitor hooks for session ${sessionId}
1426
- export __ZAM_MONITOR_FILE="${monitorFile}"
1427
- export __ZAM_MONITOR_SEQ=0
1428
- export __ZAM_MONITOR_SESSION="${sessionId}"
1429
- export __ZAM_MONITOR_CMD_ACTIVE=0
1430
-
1431
- __zam_ts() {
1432
- date -u '+%Y-%m-%dT%H:%M:%SZ'
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
+ };
1433
2341
  }
1434
-
1435
- __zam_debug_trap() {
1436
- [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]] && return
1437
- __ZAM_MONITOR_CMD_ACTIVE=1
1438
- (( __ZAM_MONITOR_SEQ++ ))
1439
- local cmd="\${BASH_COMMAND//\\"/\\\\\\"}"
1440
- local cwd="\${PWD//\\"/\\\\\\"}"
1441
- local ts="$(__zam_ts)"
1442
- printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
1443
- "$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
1444
- >> "$__ZAM_MONITOR_FILE"
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);
1445
2362
  }
1446
2363
 
1447
- __zam_prompt_cmd() {
1448
- local exit_code=$?
1449
- if [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]]; then
1450
- __ZAM_MONITOR_CMD_ACTIVE=0
1451
- local ts="$(__zam_ts)"
1452
- printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
1453
- "$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
1454
- >> "$__ZAM_MONITOR_FILE"
1455
- fi
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]);
2374
+ }
2375
+ }
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++;
2405
+ } else {
2406
+ lastDomain = domain;
2407
+ consecutiveCount = 1;
2408
+ }
2409
+ break;
2410
+ }
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;
2424
+ }
2425
+ }
2426
+ }
2427
+ return result;
1456
2428
  }
1457
2429
 
1458
- trap '__zam_debug_trap' DEBUG
1459
- PROMPT_COMMAND="__zam_prompt_cmd;\${PROMPT_COMMAND:-}"
1460
-
1461
- echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
1462
- `.trim();
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 }))
2486
+ );
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
+ }
2507
+ }
2508
+ return {
2509
+ items: capped,
2510
+ newCount,
2511
+ reviewCount,
2512
+ relearnCount,
2513
+ totalDomains: [...domainSet].sort()
2514
+ };
1463
2515
  }
1464
- function generateZshUnhooks() {
1465
- return `
1466
- # Remove ZAM monitor hooks
1467
- add-zsh-hook -d preexec __zam_preexec 2>/dev/null
1468
- add-zsh-hook -d precmd __zam_precmd 2>/dev/null
1469
- unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
1470
- unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
1471
- echo "ZAM monitor stopped."
1472
- `.trim();
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
+ };
1473
2529
  }
1474
- function generateBashUnhooks() {
1475
- return `
1476
- # Remove ZAM monitor hooks
1477
- trap - DEBUG
1478
- PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
1479
- unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
1480
- unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
1481
- echo "ZAM monitor stopped."
1482
- `.trim();
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++;
2547
+ }
2548
+ position++;
2549
+ }
2550
+ return result;
1483
2551
  }
1484
2552
 
1485
- // src/kernel/observation/skill-discovery.ts
1486
- function discoverSkills(sessionCommands, options = {}) {
1487
- const minSessions = options.minSessions ?? 2;
1488
- const minLen = options.minSequenceLength ?? 2;
1489
- const maxLen = options.maxSequenceLength ?? 5;
1490
- const existing = new Set(options.existingSkillSlugs ?? []);
1491
- const sessionSequences = /* @__PURE__ */ new Map();
1492
- for (const [sessionId, commands] of sessionCommands) {
1493
- const sequences = extractSequences(commands, minLen, maxLen);
1494
- if (sequences.length > 0) {
1495
- sessionSequences.set(sessionId, sequences);
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");
1496
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
+ });
1497
2621
  }
1498
- const sequenceIndex = /* @__PURE__ */ new Map();
1499
- for (const [, sequences] of sessionSequences) {
1500
- const seen = /* @__PURE__ */ new Set();
1501
- for (const seq of sequences) {
1502
- const key = seq.join(" \u2192 ");
1503
- if (seen.has(key)) continue;
1504
- seen.add(key);
1505
- const entry = sequenceIndex.get(key);
1506
- if (entry) {
1507
- entry.sessionCount++;
1508
- entry.totalOccurrences++;
1509
- } else {
1510
- sequenceIndex.set(key, {
1511
- steps: seq,
1512
- sessionCount: 1,
1513
- totalOccurrences: 1,
1514
- examples: []
1515
- });
1516
- }
1517
- }
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
+ });
1518
2637
  }
1519
- const lastSessionId = [...sessionCommands.keys()].pop();
1520
- if (lastSessionId) {
1521
- const lastCommands = sessionCommands.get(lastSessionId);
1522
- for (const [key, entry] of sequenceIndex) {
1523
- if (entry.sessionCount >= minSessions) {
1524
- entry.examples = findExamplesForSequence(lastCommands, entry.steps);
1525
- }
2638
+ const codexSkillsDir = join6(home, ".agents", "skills", "zam");
2639
+ try {
2640
+ if (!codexSourceSkill) {
2641
+ throw new Error("Codex skill source not found");
1526
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
+ });
1527
2656
  }
1528
- const candidates = [...sequenceIndex.values()].filter(
1529
- (s) => s.sessionCount >= minSessions
1530
- );
1531
- const pruned = removeSubsequences(candidates);
1532
- const proposals = [];
1533
- for (const seq of pruned) {
1534
- const slug = generateSlug(seq.steps);
1535
- if (existing.has(slug)) continue;
1536
- proposals.push({
1537
- slug,
1538
- description: describeSequence(seq.steps),
1539
- steps: seq.steps,
1540
- sessionCount: seq.sessionCount,
1541
- confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
1542
- examples: seq.examples
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
1543
2671
  });
1544
2672
  }
1545
- const confidenceOrder = { high: 0, medium: 1, low: 2 };
1546
- proposals.sort((a, b) => {
1547
- const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
1548
- if (confDiff !== 0) return confDiff;
1549
- return b.sessionCount - a.sessionCount;
1550
- });
1551
- return proposals;
2673
+ return results;
1552
2674
  }
1553
- function normalizeCommand(command) {
1554
- const parts = command.trim().split(/\s+/);
1555
- const multiWord = ["docker compose", "npm run", "npx", "git"];
1556
- const lower = command.toLowerCase();
1557
- for (const mw of multiWord) {
1558
- if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
1559
- return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
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
+ });
2704
+ }
2705
+ } catch {
2706
+ results.push({
2707
+ shell: "zsh",
2708
+ file: zshrc,
2709
+ success: false,
2710
+ alreadyHooked: false
2711
+ });
1560
2712
  }
1561
2713
  }
1562
- return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
1563
- }
1564
- function extractSequences(commands, minLen, maxLen) {
1565
- const filtered = commands.filter((c) => {
1566
- const lower = c.command.toLowerCase().trim();
1567
- return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
1568
- });
1569
- const normalized = filtered.map((c) => normalizeCommand(c.command));
1570
- const sequences = [];
1571
- for (let len = minLen; len <= maxLen; len++) {
1572
- for (let i = 0; i <= normalized.length - len; i++) {
1573
- const seq = normalized.slice(i, i + len);
1574
- if (new Set(seq).size >= 2) {
1575
- sequences.push(seq);
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
+ });
1576
2733
  }
2734
+ } catch {
2735
+ results.push({
2736
+ shell: "bash",
2737
+ file: bashrc,
2738
+ success: false,
2739
+ alreadyHooked: false
2740
+ });
1577
2741
  }
1578
2742
  }
1579
- return sequences;
1580
- }
1581
- function findExamplesForSequence(commands, steps) {
1582
- const normalized = commands.map((c) => ({
1583
- norm: normalizeCommand(c.command),
1584
- full: c.command
1585
- }));
1586
- for (let i = 0; i <= normalized.length - steps.length; i++) {
1587
- let match = true;
1588
- for (let j = 0; j < steps.length; j++) {
1589
- if (normalized[i + j].norm !== steps[j]) {
1590
- match = false;
1591
- break;
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
+ });
1592
2770
  }
1593
- }
1594
- if (match) {
1595
- return normalized.slice(i, i + steps.length).map((n) => n.full);
2771
+ } catch {
2772
+ results.push({
2773
+ shell: "powershell",
2774
+ file: profileFile,
2775
+ success: false,
2776
+ alreadyHooked: false
2777
+ });
1596
2778
  }
1597
2779
  }
1598
- return [];
2780
+ return results;
1599
2781
  }
1600
- function removeSubsequences(candidates) {
1601
- const sorted = [...candidates].sort((a, b) => b.steps.length - a.steps.length);
1602
- const result = [];
1603
- for (const candidate of sorted) {
1604
- const key = candidate.steps.join(" \u2192 ");
1605
- const isSubsequence = result.some((longer) => {
1606
- const longerKey = longer.steps.join(" \u2192 ");
1607
- return longerKey.includes(key) && longerKey !== key;
1608
- });
1609
- if (!isSubsequence) {
1610
- result.push(candidate);
1611
- }
2782
+
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}]"
1612
2980
  }
1613
- return result;
1614
- }
1615
- function generateSlug(steps) {
1616
- return steps.map((s) => {
1617
- const parts = s.split(/\s+/);
1618
- return parts[parts.length - 1];
1619
- }).join("-");
1620
- }
1621
- function describeSequence(steps) {
1622
- return `Recurring pattern: ${steps.join(" \u2192 ")}`;
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));
2987
+ }
2988
+ return str;
1623
2989
  }
1624
2990
 
1625
- // src/kernel/goals/engine.ts
1626
- import { readdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
1627
- import { join as join3, basename } from "path";
1628
-
1629
- // src/kernel/goals/parser.ts
1630
- function parseGoalFile(content, slug, filePath) {
1631
- const { frontmatter, body } = splitFrontmatter(content);
1632
- const validStatuses = ["active", "completed", "paused", "abandoned"];
1633
- const status = validStatuses.includes(frontmatter.status) ? frontmatter.status : "active";
1634
- const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1635
- return {
1636
- slug,
1637
- title: frontmatter.title || slug,
1638
- status,
1639
- parent: frontmatter.parent || null,
1640
- created: frontmatter.created || now,
1641
- updated: frontmatter.updated || now,
1642
- body,
1643
- filePath
1644
- };
1645
- }
1646
- function serializeGoal(goal) {
1647
- const lines = [
1648
- "---",
1649
- `title: ${goal.title}`,
1650
- `status: ${goal.status}`
1651
- ];
1652
- if (goal.parent) {
1653
- lines.push(`parent: ${goal.parent}`);
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;
3003
+ }
3004
+ }
3005
+ function installFastFlowLM() {
3006
+ if (process.platform !== "win32") {
3007
+ return {
3008
+ success: false,
3009
+ message: "FastFlowLM is only supported on Windows."
3010
+ };
1654
3011
  }
1655
- lines.push(`created: ${goal.created}`);
1656
- lines.push(`updated: ${goal.updated}`);
1657
- lines.push("---");
1658
- lines.push("");
1659
- if (goal.body.trim()) {
1660
- lines.push(goal.body.trim());
1661
- lines.push("");
3012
+ const hasFlm = hasCommand("flm") || existsSync7("C:\\Program Files\\flm\\flm.exe");
3013
+ if (hasFlm) {
3014
+ return { success: true, message: "FastFlowLM is already installed." };
1662
3015
  }
1663
- return lines.join("\n");
1664
- }
1665
- function extractTasks(body) {
1666
- const tasks = [];
1667
- const taskRegex = /^[-*]\s+\[([ xX])\]\s+(.+)$/gm;
1668
- let match;
1669
- while ((match = taskRegex.exec(body)) !== null) {
1670
- tasks.push({
1671
- done: match[1] !== " ",
1672
- text: match[2].trim()
1673
- });
3016
+ if (!hasCommand("winget")) {
3017
+ return {
3018
+ success: false,
3019
+ message: "winget package manager was not found on this system."
3020
+ };
1674
3021
  }
1675
- return tasks;
1676
- }
1677
- function extractTokenRefs(body) {
1678
- const tokensSection = body.match(/## Tokens\n([\s\S]*?)(?=\n## |\n*$)/);
1679
- if (!tokensSection) return [];
1680
- const refs = [];
1681
- const lines = tokensSection[1].split("\n");
1682
- for (const line of lines) {
1683
- const match = line.match(/^[-*]\s+(\S+)/);
1684
- if (match) {
1685
- refs.push(match[1]);
1686
- }
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
+ };
1687
3034
  }
1688
- return refs;
1689
3035
  }
1690
- function splitFrontmatter(content) {
1691
- const trimmed = content.trim();
1692
- if (!trimmed.startsWith("---")) {
1693
- 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." };
1694
3044
  }
1695
- const endIndex = trimmed.indexOf("---", 3);
1696
- if (endIndex === -1) {
1697
- 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
+ }
1698
3095
  }
1699
- const fmBlock = trimmed.slice(3, endIndex).trim();
1700
- const body = trimmed.slice(endIndex + 3).trim();
1701
- const frontmatter = {};
1702
- for (const line of fmBlock.split("\n")) {
1703
- const colonIndex = line.indexOf(":");
1704
- if (colonIndex === -1) continue;
1705
- const key = line.slice(0, colonIndex).trim();
1706
- const value = line.slice(colonIndex + 1).trim();
1707
- if (key && value) {
1708
- 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
+ }
1709
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
+ }
3137
+ }
3138
+ } catch {
1710
3139
  }
1711
- return { frontmatter, body };
3140
+ return "en";
1712
3141
  }
1713
3142
 
1714
- // src/kernel/goals/engine.ts
1715
- function listGoals(goalsDir) {
1716
- if (!existsSync3(goalsDir)) return [];
1717
- const files = readdirSync(goalsDir).filter(
1718
- (f) => f.endsWith(".md") && f !== "README.md"
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 "";
3150
+ }
3151
+ }
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"))
1719
3158
  );
1720
- const summaries = [];
1721
- for (const file of files) {
1722
- const filePath = join3(goalsDir, file);
1723
- const content = readFileSync2(filePath, "utf-8");
1724
- const slug = basename(file, ".md");
1725
- const goal = parseGoalFile(content, slug, filePath);
1726
- const tasks = extractTasks(goal.body);
1727
- const tokens = extractTokenRefs(goal.body);
1728
- summaries.push({
1729
- slug: goal.slug,
1730
- title: goal.title,
1731
- status: goal.status,
1732
- parent: goal.parent,
1733
- taskCount: tasks.length,
1734
- tasksDone: tasks.filter((t) => t.done).length,
1735
- tokenCount: tokens.length
1736
- });
3159
+ }
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";
1737
3183
  }
1738
- const statusOrder = {
1739
- active: 0,
1740
- paused: 1,
1741
- completed: 2,
1742
- abandoned: 3
3184
+ return {
3185
+ os,
3186
+ arch,
3187
+ hasRyzenNPU,
3188
+ hasAppleSilicon,
3189
+ recommendedRunner,
3190
+ recommendedModel
1743
3191
  };
1744
- summaries.sort((a, b) => {
1745
- const statusDiff = statusOrder[a.status] - statusOrder[b.status];
1746
- if (statusDiff !== 0) return statusDiff;
1747
- return a.title.localeCompare(b.title);
1748
- });
1749
- return summaries;
1750
- }
1751
- function getGoal(goalsDir, slug) {
1752
- const filePath = join3(goalsDir, `${slug}.md`);
1753
- if (!existsSync3(filePath)) return void 0;
1754
- const content = readFileSync2(filePath, "utf-8");
1755
- return parseGoalFile(content, slug, filePath);
1756
3192
  }
1757
- function createGoal(goalsDir, input) {
1758
- const filePath = join3(goalsDir, `${input.slug}.md`);
1759
- if (existsSync3(filePath)) {
1760
- throw new Error(`Goal already exists: ${input.slug}`);
1761
- }
1762
- const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1763
- const goal = {
1764
- slug: input.slug,
1765
- title: input.title,
1766
- status: input.status ?? "active",
1767
- parent: input.parent ?? null,
1768
- created: now,
1769
- updated: now,
1770
- body: input.description ? `## Description
1771
- ${input.description}
1772
3193
 
1773
- ## Tasks
1774
-
1775
- ## Tokens` : "## Description\n\n## Tasks\n\n## Tokens",
1776
- 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
1777
3205
  };
1778
- writeFileSync(filePath, serializeGoal(goal), "utf-8");
1779
- return goal;
1780
3206
  }
1781
- function updateGoalStatus(goalsDir, slug, status) {
1782
- const goal = getGoal(goalsDir, slug);
1783
- if (!goal) throw new Error(`Goal not found: ${slug}`);
1784
- goal.status = status;
1785
- goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1786
- writeFileSync(goal.filePath, serializeGoal(goal), "utf-8");
1787
- return goal;
3207
+ function resolveRepoPath(db, type) {
3208
+ const paths = getRepoPaths(db);
3209
+ return paths[type];
1788
3210
  }
1789
- function getGoalTree(goalsDir) {
1790
- const all = listGoals(goalsDir);
1791
- const bySlug = new Map(all.map((g) => [g.slug, g]));
1792
- const roots = [];
1793
- const children = /* @__PURE__ */ new Map();
1794
- for (const g of all) {
1795
- if (g.parent && bySlug.has(g.parent)) {
1796
- const list = children.get(g.parent) ?? [];
1797
- list.push(g);
1798
- children.set(g.parent, list);
1799
- }
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);
1800
3217
  }
1801
- for (const g of all) {
1802
- if (!g.parent || !bySlug.has(g.parent)) {
1803
- roots.push({ ...g, children: children.get(g.slug) ?? [] });
1804
- }
3218
+ if (paths.team) {
3219
+ const teamDir = resolve2(paths.team, "beliefs");
3220
+ if (existsSync8(teamDir)) dirs.push(teamDir);
1805
3221
  }
1806
- return roots;
1807
- }
1808
-
1809
- // src/kernel/connectors/azure-devops.ts
1810
- function loadADOConfig(db) {
1811
- const orgUrl = getSetting(db, "ado.org_url");
1812
- const project = getSetting(db, "ado.project");
1813
- const pat = getSetting(db, "ado.pat");
1814
- if (!orgUrl || !project || !pat) return null;
1815
- return { orgUrl: orgUrl.replace(/\/+$/, ""), project, pat };
1816
- }
1817
- function authHeader(pat) {
1818
- 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;
1819
3227
  }
1820
- async function fetchActiveWorkItems(config) {
1821
- const { orgUrl, project, pat } = config;
1822
- const wiqlUrl = `${orgUrl}/${project}/_apis/wit/wiql?api-version=7.1`;
1823
- const wiqlBody = {
1824
- 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`
1825
- };
1826
- const wiqlRes = await fetch(wiqlUrl, {
1827
- method: "POST",
1828
- headers: {
1829
- Authorization: authHeader(pat),
1830
- "Content-Type": "application/json"
1831
- },
1832
- body: JSON.stringify(wiqlBody)
1833
- });
1834
- if (!wiqlRes.ok) {
1835
- const text = await wiqlRes.text();
1836
- 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);
1837
3234
  }
1838
- const wiqlData = await wiqlRes.json();
1839
- const ids = wiqlData.workItems.map((wi) => wi.id);
1840
- if (ids.length === 0) return [];
1841
- const batchIds = ids.slice(0, 200);
1842
- const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
1843
- const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
1844
- const detailRes = await fetch(detailUrl, {
1845
- headers: { Authorization: authHeader(pat) }
1846
- });
1847
- if (!detailRes.ok) {
1848
- const text = await detailRes.text();
1849
- 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);
1850
3238
  }
1851
- const detailData = await detailRes.json();
1852
- return detailData.value.map((wi) => ({
1853
- id: wi.id,
1854
- title: wi.fields["System.Title"],
1855
- state: wi.fields["System.State"],
1856
- type: wi.fields["System.WorkItemType"],
1857
- assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
1858
- }));
3239
+ if (paths.org) {
3240
+ const orgDir = resolve2(paths.org, "goals");
3241
+ if (existsSync8(orgDir)) dirs.push(orgDir);
3242
+ }
3243
+ return dirs;
1859
3244
  }
1860
3245
  export {
3246
+ DEFAULT_REVIEW_CONTEXT_MAX_CHARS,
1861
3247
  addPrerequisite,
1862
3248
  analyzeObservation,
1863
3249
  buildReviewQueue,
1864
3250
  cascadeBlock,
3251
+ clearADOCredentials,
3252
+ clearTursoCredentials,
1865
3253
  createAgentSkill,
1866
3254
  createFSRS,
1867
3255
  createGoal,
1868
3256
  createToken,
3257
+ deleteCardForUser,
1869
3258
  deleteSetting,
3259
+ deleteToken,
1870
3260
  deprecateToken,
3261
+ detectSystemLocale,
1871
3262
  discoverSkills,
3263
+ distributeGlobalSkills,
1872
3264
  endSession,
1873
3265
  ensureCard,
1874
3266
  ensureMonitorDir,
1875
3267
  evaluateRating,
3268
+ executeReviewAction,
1876
3269
  extractTasks,
1877
3270
  extractTokenRefs,
1878
3271
  fetchActiveWorkItems,
1879
3272
  findTokens,
1880
3273
  generateBashHooks,
1881
3274
  generateBashUnhooks,
3275
+ generateConceptFreeCue,
3276
+ generatePowerShellHooks,
3277
+ generatePowerShellUnhooks,
1882
3278
  generatePrompt,
1883
3279
  generateZshHooks,
1884
3280
  generateZshUnhooks,
3281
+ getADOCredentials,
1885
3282
  getAgentSkill,
1886
3283
  getAllSettings,
1887
3284
  getAllSettingsDetailed,
1888
3285
  getBlockedCards,
1889
3286
  getCard,
3287
+ getCardById,
3288
+ getCardDeletionImpact,
1890
3289
  getDefaultDbPath,
1891
3290
  getDependents,
1892
3291
  getDomainCompetence,
@@ -1896,34 +3295,57 @@ export {
1896
3295
  getMonitorDir,
1897
3296
  getMonitorLogStats,
1898
3297
  getMonitorPath,
3298
+ getPackageSkillPath,
1899
3299
  getPrerequisites,
3300
+ getRepoPaths,
1900
3301
  getReviewsForCard,
1901
3302
  getReviewsForUser,
1902
3303
  getSessionSummary,
1903
3304
  getSetting,
3305
+ getSystemProfile,
1904
3306
  getTokenById,
1905
3307
  getTokenBySlug,
3308
+ getTokenDeleteImpact,
3309
+ getTursoCredentials,
1906
3310
  getUserStats,
3311
+ hasCommand,
3312
+ injectShellHooks,
3313
+ installFastFlowLM,
3314
+ installOllama,
1907
3315
  interleave,
1908
3316
  listAgentSkills,
1909
3317
  listGoals,
1910
3318
  listTokens,
1911
3319
  loadADOConfig,
3320
+ loadCredentials,
1912
3321
  logReview,
1913
3322
  logStep,
3323
+ matchesFilePath,
1914
3324
  monitorLogExists,
3325
+ normalizeLocale,
3326
+ normalizePath,
1915
3327
  openDatabase,
1916
3328
  openDatabaseWithSync,
1917
3329
  pairCommands,
1918
3330
  parseGoalFile,
1919
3331
  parseMonitorLog,
1920
3332
  readMonitorLog,
3333
+ resolveAllBeliefPaths,
3334
+ resolveAllGoalPaths,
3335
+ resolveReference,
3336
+ resolveRepoPath,
3337
+ resolveReviewContext,
3338
+ saveCredentials,
1921
3339
  serializeGoal,
3340
+ setADOCredentials,
1922
3341
  setSetting,
3342
+ setTursoCredentials,
1923
3343
  startSession,
3344
+ t,
1924
3345
  unblockReady,
1925
3346
  updateCard,
1926
3347
  updateGoalStatus,
3348
+ updateToken,
1927
3349
  writeMonitorEvent
1928
3350
  };
1929
3351
  //# sourceMappingURL=index.js.map