zam-core 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/zam/SKILL.md +331 -0
- package/.gemini/skills/zam/SKILL.md +335 -0
- package/LICENSE +199 -0
- package/README.de.md +86 -0
- package/README.md +86 -0
- package/dist/cli/index.js +3661 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +998 -0
- package/dist/index.js +1920 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1920 @@
|
|
|
1
|
+
// src/kernel/db/connection.ts
|
|
2
|
+
import Database from "libsql";
|
|
3
|
+
import { existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
|
|
7
|
+
// src/kernel/db/schema.ts
|
|
8
|
+
var SCHEMA = `
|
|
9
|
+
-- Use WAL mode for concurrent reads (AI CLI + user CLI)
|
|
10
|
+
PRAGMA journal_mode = WAL;
|
|
11
|
+
PRAGMA foreign_keys = ON;
|
|
12
|
+
|
|
13
|
+
-- Knowledge tokens: atomic concepts/facts with Bloom levels
|
|
14
|
+
CREATE TABLE IF NOT EXISTS tokens (
|
|
15
|
+
id TEXT PRIMARY KEY,
|
|
16
|
+
slug TEXT UNIQUE NOT NULL,
|
|
17
|
+
concept TEXT NOT NULL,
|
|
18
|
+
domain TEXT NOT NULL DEFAULT '',
|
|
19
|
+
bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
|
|
20
|
+
context TEXT NOT NULL DEFAULT '',
|
|
21
|
+
symbiosis_mode TEXT CHECK (symbiosis_mode IN ('shadowing', 'copilot', 'autonomy')),
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
deprecated_at TEXT
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
-- Prerequisite dependency graph: "to learn A, first know B"
|
|
28
|
+
CREATE TABLE IF NOT EXISTS prerequisites (
|
|
29
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
30
|
+
requires_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
31
|
+
PRIMARY KEY (token_id, requires_id)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
-- Per-user scheduling state for each token (FSRS fields)
|
|
35
|
+
CREATE TABLE IF NOT EXISTS cards (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
38
|
+
user_id TEXT NOT NULL,
|
|
39
|
+
stability REAL NOT NULL DEFAULT 0.0,
|
|
40
|
+
difficulty REAL NOT NULL DEFAULT 0.5,
|
|
41
|
+
elapsed_days REAL NOT NULL DEFAULT 0.0,
|
|
42
|
+
scheduled_days REAL NOT NULL DEFAULT 0.0,
|
|
43
|
+
reps INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
lapses INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
state TEXT NOT NULL DEFAULT 'new' CHECK (state IN ('new', 'learning', 'review', 'relearning')),
|
|
46
|
+
due_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
47
|
+
last_review_at TEXT,
|
|
48
|
+
blocked INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
UNIQUE(token_id, user_id)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- Immutable review log: every rating event
|
|
53
|
+
CREATE TABLE IF NOT EXISTS review_logs (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
card_id TEXT NOT NULL REFERENCES cards(id) ON DELETE CASCADE,
|
|
56
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
57
|
+
user_id TEXT NOT NULL,
|
|
58
|
+
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 4),
|
|
59
|
+
response_time_ms INTEGER,
|
|
60
|
+
reviewed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
61
|
+
scheduled_at TEXT NOT NULL,
|
|
62
|
+
session_id TEXT REFERENCES sessions(id)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- Work+learning sessions
|
|
66
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
user_id TEXT NOT NULL,
|
|
69
|
+
task TEXT NOT NULL,
|
|
70
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
71
|
+
completed_at TEXT
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
-- Steps within a session: who did what
|
|
75
|
+
CREATE TABLE IF NOT EXISTS session_steps (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
78
|
+
token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
|
|
79
|
+
done_by TEXT NOT NULL CHECK (done_by IN ('user', 'agent')),
|
|
80
|
+
rating INTEGER CHECK (rating BETWEEN 1 AND 4),
|
|
81
|
+
notes TEXT,
|
|
82
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
-- User configuration
|
|
86
|
+
CREATE TABLE IF NOT EXISTS user_config (
|
|
87
|
+
key TEXT PRIMARY KEY,
|
|
88
|
+
value TEXT NOT NULL,
|
|
89
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
-- Agent skills: task recipes the agent learns from user guidance
|
|
93
|
+
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
94
|
+
id TEXT PRIMARY KEY,
|
|
95
|
+
slug TEXT NOT NULL UNIQUE,
|
|
96
|
+
description TEXT NOT NULL,
|
|
97
|
+
steps TEXT NOT NULL DEFAULT '[]', -- JSON array of step strings
|
|
98
|
+
token_slugs TEXT NOT NULL DEFAULT '[]', -- JSON array of related token slugs
|
|
99
|
+
source TEXT NOT NULL DEFAULT 'learned'
|
|
100
|
+
CHECK(source IN ('learned', 'builtin')),
|
|
101
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
102
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
-- Performance indexes
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_tokens_slug ON tokens(slug);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_prereqs_token ON prerequisites(token_id);
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_prereqs_requires ON prerequisites(requires_id);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_cards_user_due ON cards(user_id, blocked, due_at);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
// src/kernel/db/connection.ts
|
|
118
|
+
var DEFAULT_DB_DIR = join(homedir(), ".zam");
|
|
119
|
+
var DEFAULT_DB_PATH = join(DEFAULT_DB_DIR, "zam.db");
|
|
120
|
+
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 });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const dbOpts = {};
|
|
129
|
+
if (options.syncUrl) {
|
|
130
|
+
dbOpts.syncUrl = options.syncUrl;
|
|
131
|
+
}
|
|
132
|
+
if (options.authToken) {
|
|
133
|
+
dbOpts.authToken = options.authToken;
|
|
134
|
+
}
|
|
135
|
+
const db = new Database(dbPath, dbOpts);
|
|
136
|
+
db.pragma("journal_mode = WAL");
|
|
137
|
+
db.pragma("foreign_keys = ON");
|
|
138
|
+
db.pragma("busy_timeout = 5000");
|
|
139
|
+
if (options.initialize) {
|
|
140
|
+
db.exec(SCHEMA);
|
|
141
|
+
}
|
|
142
|
+
runMigrations(db);
|
|
143
|
+
if (options.syncUrl) {
|
|
144
|
+
db.sync();
|
|
145
|
+
}
|
|
146
|
+
return db;
|
|
147
|
+
}
|
|
148
|
+
function openDatabaseWithSync(options = {}) {
|
|
149
|
+
const db = openDatabase(options);
|
|
150
|
+
const syncUrl = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.url");
|
|
151
|
+
const authToken = db.prepare("SELECT value FROM user_config WHERE key = ?").get("turso.token");
|
|
152
|
+
if (!syncUrl || !authToken) return db;
|
|
153
|
+
db.close();
|
|
154
|
+
return openDatabase({ ...options, syncUrl: syncUrl.value, authToken: authToken.value });
|
|
155
|
+
}
|
|
156
|
+
function getDefaultDbPath() {
|
|
157
|
+
return DEFAULT_DB_PATH;
|
|
158
|
+
}
|
|
159
|
+
function runMigrations(db) {
|
|
160
|
+
const sessionCols = db.pragma("table_info(sessions)");
|
|
161
|
+
if (sessionCols.length > 0 && !sessionCols.some((c) => c.name === "execution_context")) {
|
|
162
|
+
db.exec(
|
|
163
|
+
`ALTER TABLE sessions ADD COLUMN execution_context TEXT NOT NULL DEFAULT 'shell'`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const tokenCols = db.pragma("table_info(tokens)");
|
|
167
|
+
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "deprecated_at")) {
|
|
168
|
+
db.exec(`ALTER TABLE tokens ADD COLUMN deprecated_at TEXT`);
|
|
169
|
+
}
|
|
170
|
+
db.exec(`
|
|
171
|
+
CREATE TABLE IF NOT EXISTS agent_skills (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
slug TEXT NOT NULL UNIQUE,
|
|
174
|
+
description TEXT NOT NULL,
|
|
175
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
176
|
+
token_slugs TEXT NOT NULL DEFAULT '[]',
|
|
177
|
+
source TEXT NOT NULL DEFAULT 'learned',
|
|
178
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
179
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
180
|
+
)
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/kernel/models/token.ts
|
|
185
|
+
import { ulid } from "ulid";
|
|
186
|
+
function createToken(db, input) {
|
|
187
|
+
const id = ulid();
|
|
188
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
189
|
+
const bloom = input.bloom_level ?? 1;
|
|
190
|
+
if (bloom < 1 || bloom > 5) {
|
|
191
|
+
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
192
|
+
}
|
|
193
|
+
db.prepare(`
|
|
194
|
+
INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, created_at, updated_at)
|
|
195
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
196
|
+
`).run(
|
|
197
|
+
id,
|
|
198
|
+
input.slug,
|
|
199
|
+
input.concept,
|
|
200
|
+
input.domain ?? "",
|
|
201
|
+
bloom,
|
|
202
|
+
input.context ?? "",
|
|
203
|
+
input.symbiosis_mode ?? null,
|
|
204
|
+
now,
|
|
205
|
+
now
|
|
206
|
+
);
|
|
207
|
+
return getTokenById(db, id);
|
|
208
|
+
}
|
|
209
|
+
function getTokenBySlug(db, slug) {
|
|
210
|
+
return db.prepare("SELECT * FROM tokens WHERE slug = ?").get(slug);
|
|
211
|
+
}
|
|
212
|
+
function getTokenById(db, id) {
|
|
213
|
+
return db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
214
|
+
}
|
|
215
|
+
function deprecateToken(db, slug) {
|
|
216
|
+
const token = getTokenBySlug(db, slug);
|
|
217
|
+
if (!token) {
|
|
218
|
+
throw new Error(`Token not found: ${slug}`);
|
|
219
|
+
}
|
|
220
|
+
if (token.deprecated_at) {
|
|
221
|
+
throw new Error(`Token already deprecated: ${slug}`);
|
|
222
|
+
}
|
|
223
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
224
|
+
db.prepare("UPDATE tokens SET deprecated_at = ?, updated_at = ? WHERE slug = ?").run(
|
|
225
|
+
now,
|
|
226
|
+
now,
|
|
227
|
+
slug
|
|
228
|
+
);
|
|
229
|
+
return getTokenBySlug(db, slug);
|
|
230
|
+
}
|
|
231
|
+
function findTokens(db, query) {
|
|
232
|
+
const normalised = query.toLowerCase();
|
|
233
|
+
const qTokens = new Set(
|
|
234
|
+
normalised.split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter((t) => t.length > 2)
|
|
235
|
+
);
|
|
236
|
+
const tokens = db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
237
|
+
const scored = [];
|
|
238
|
+
for (const t of tokens) {
|
|
239
|
+
const words = (t.slug + " " + t.concept + " " + t.domain).toLowerCase().split(/[\s,.\-_/\\:;!?()\[\]{}]+/).filter(Boolean);
|
|
240
|
+
let score = 0;
|
|
241
|
+
for (const w of words) {
|
|
242
|
+
if (qTokens.has(w)) score++;
|
|
243
|
+
}
|
|
244
|
+
if (t.concept.toLowerCase().includes(normalised.slice(0, 25))) {
|
|
245
|
+
score += 3;
|
|
246
|
+
}
|
|
247
|
+
if (score > 0) {
|
|
248
|
+
scored.push({ score, ...t });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
scored.sort((a, b) => b.score - a.score);
|
|
252
|
+
return scored;
|
|
253
|
+
}
|
|
254
|
+
function listTokens(db, options) {
|
|
255
|
+
if (options?.domain) {
|
|
256
|
+
return db.prepare(
|
|
257
|
+
"SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
|
|
258
|
+
).all(options.domain);
|
|
259
|
+
}
|
|
260
|
+
return db.prepare(
|
|
261
|
+
"SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
|
|
262
|
+
).all();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/kernel/models/prerequisite.ts
|
|
266
|
+
function addPrerequisite(db, tokenId, requiresId) {
|
|
267
|
+
if (tokenId === requiresId) {
|
|
268
|
+
throw new Error("A token cannot be a prerequisite of itself");
|
|
269
|
+
}
|
|
270
|
+
db.prepare(
|
|
271
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
272
|
+
).run(tokenId, requiresId);
|
|
273
|
+
}
|
|
274
|
+
function getPrerequisites(db, tokenId) {
|
|
275
|
+
return db.prepare(
|
|
276
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
277
|
+
FROM prerequisites p
|
|
278
|
+
JOIN tokens t ON t.id = p.requires_id
|
|
279
|
+
WHERE p.token_id = ?`
|
|
280
|
+
).all(tokenId);
|
|
281
|
+
}
|
|
282
|
+
function getDependents(db, tokenId) {
|
|
283
|
+
return db.prepare(
|
|
284
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
285
|
+
FROM prerequisites p
|
|
286
|
+
JOIN tokens t ON t.id = p.token_id
|
|
287
|
+
WHERE p.requires_id = ?`
|
|
288
|
+
).all(tokenId);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/kernel/models/card.ts
|
|
292
|
+
import { ulid as ulid2 } from "ulid";
|
|
293
|
+
function ensureCard(db, tokenId, userId) {
|
|
294
|
+
const existing = db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
295
|
+
if (existing) return existing;
|
|
296
|
+
const id = ulid2();
|
|
297
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
298
|
+
db.prepare(
|
|
299
|
+
`INSERT INTO cards (id, token_id, user_id, due_at)
|
|
300
|
+
VALUES (?, ?, ?, ?)`
|
|
301
|
+
).run(id, tokenId, userId, now);
|
|
302
|
+
return db.prepare("SELECT * FROM cards WHERE id = ?").get(id);
|
|
303
|
+
}
|
|
304
|
+
function getCard(db, tokenId, userId) {
|
|
305
|
+
return db.prepare("SELECT * FROM cards WHERE token_id = ? AND user_id = ?").get(tokenId, userId);
|
|
306
|
+
}
|
|
307
|
+
function updateCard(db, cardId, updates) {
|
|
308
|
+
const fields = [];
|
|
309
|
+
const values = [];
|
|
310
|
+
if (updates.stability !== void 0) {
|
|
311
|
+
fields.push("stability = ?");
|
|
312
|
+
values.push(updates.stability);
|
|
313
|
+
}
|
|
314
|
+
if (updates.difficulty !== void 0) {
|
|
315
|
+
fields.push("difficulty = ?");
|
|
316
|
+
values.push(updates.difficulty);
|
|
317
|
+
}
|
|
318
|
+
if (updates.elapsed_days !== void 0) {
|
|
319
|
+
fields.push("elapsed_days = ?");
|
|
320
|
+
values.push(updates.elapsed_days);
|
|
321
|
+
}
|
|
322
|
+
if (updates.scheduled_days !== void 0) {
|
|
323
|
+
fields.push("scheduled_days = ?");
|
|
324
|
+
values.push(updates.scheduled_days);
|
|
325
|
+
}
|
|
326
|
+
if (updates.reps !== void 0) {
|
|
327
|
+
fields.push("reps = ?");
|
|
328
|
+
values.push(updates.reps);
|
|
329
|
+
}
|
|
330
|
+
if (updates.lapses !== void 0) {
|
|
331
|
+
fields.push("lapses = ?");
|
|
332
|
+
values.push(updates.lapses);
|
|
333
|
+
}
|
|
334
|
+
if (updates.state !== void 0) {
|
|
335
|
+
fields.push("state = ?");
|
|
336
|
+
values.push(updates.state);
|
|
337
|
+
}
|
|
338
|
+
if (updates.due_at !== void 0) {
|
|
339
|
+
fields.push("due_at = ?");
|
|
340
|
+
values.push(updates.due_at);
|
|
341
|
+
}
|
|
342
|
+
if (updates.last_review_at !== void 0) {
|
|
343
|
+
fields.push("last_review_at = ?");
|
|
344
|
+
values.push(updates.last_review_at);
|
|
345
|
+
}
|
|
346
|
+
if (updates.blocked !== void 0) {
|
|
347
|
+
fields.push("blocked = ?");
|
|
348
|
+
values.push(updates.blocked);
|
|
349
|
+
}
|
|
350
|
+
if (fields.length === 0) {
|
|
351
|
+
throw new Error("updateCard called with no fields to update");
|
|
352
|
+
}
|
|
353
|
+
values.push(cardId);
|
|
354
|
+
const result = db.prepare(`UPDATE cards SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
355
|
+
if (result.changes === 0) {
|
|
356
|
+
throw new Error(`Card not found: ${cardId}`);
|
|
357
|
+
}
|
|
358
|
+
return db.prepare("SELECT * FROM cards WHERE id = ?").get(cardId);
|
|
359
|
+
}
|
|
360
|
+
function getDueCards(db, userId, now) {
|
|
361
|
+
const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
362
|
+
return db.prepare(
|
|
363
|
+
`SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
364
|
+
FROM cards c
|
|
365
|
+
JOIN tokens t ON t.id = c.token_id
|
|
366
|
+
WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?
|
|
367
|
+
ORDER BY t.bloom_level ASC, c.due_at ASC`
|
|
368
|
+
).all(userId, cutoff);
|
|
369
|
+
}
|
|
370
|
+
function getBlockedCards(db, userId) {
|
|
371
|
+
return db.prepare(
|
|
372
|
+
`SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
373
|
+
FROM cards c
|
|
374
|
+
JOIN tokens t ON t.id = c.token_id
|
|
375
|
+
WHERE c.user_id = ? AND c.blocked = 1
|
|
376
|
+
ORDER BY t.bloom_level ASC, t.slug ASC`
|
|
377
|
+
).all(userId);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/kernel/models/review.ts
|
|
381
|
+
import { ulid as ulid3 } from "ulid";
|
|
382
|
+
function logReview(db, input) {
|
|
383
|
+
if (input.rating < 1 || input.rating > 4) {
|
|
384
|
+
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
385
|
+
}
|
|
386
|
+
const id = ulid3();
|
|
387
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
388
|
+
db.prepare(
|
|
389
|
+
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
390
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
391
|
+
).run(
|
|
392
|
+
id,
|
|
393
|
+
input.card_id,
|
|
394
|
+
input.token_id,
|
|
395
|
+
input.user_id,
|
|
396
|
+
input.rating,
|
|
397
|
+
input.response_time_ms ?? null,
|
|
398
|
+
now,
|
|
399
|
+
input.scheduled_at,
|
|
400
|
+
input.session_id ?? null
|
|
401
|
+
);
|
|
402
|
+
return db.prepare("SELECT * FROM review_logs WHERE id = ?").get(id);
|
|
403
|
+
}
|
|
404
|
+
function getReviewsForCard(db, cardId) {
|
|
405
|
+
return db.prepare(
|
|
406
|
+
"SELECT * FROM review_logs WHERE card_id = ? ORDER BY reviewed_at ASC"
|
|
407
|
+
).all(cardId);
|
|
408
|
+
}
|
|
409
|
+
function getReviewsForUser(db, userId, options) {
|
|
410
|
+
const conditions = ["user_id = ?"];
|
|
411
|
+
const params = [userId];
|
|
412
|
+
if (options?.after) {
|
|
413
|
+
conditions.push("reviewed_at > ?");
|
|
414
|
+
params.push(options.after);
|
|
415
|
+
}
|
|
416
|
+
if (options?.before) {
|
|
417
|
+
conditions.push("reviewed_at < ?");
|
|
418
|
+
params.push(options.before);
|
|
419
|
+
}
|
|
420
|
+
let sql = `SELECT * FROM review_logs WHERE ${conditions.join(" AND ")} ORDER BY reviewed_at DESC`;
|
|
421
|
+
if (options?.limit) {
|
|
422
|
+
sql += " LIMIT ?";
|
|
423
|
+
params.push(options.limit);
|
|
424
|
+
}
|
|
425
|
+
return db.prepare(sql).all(...params);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/kernel/models/session.ts
|
|
429
|
+
import { ulid as ulid4 } from "ulid";
|
|
430
|
+
function startSession(db, input) {
|
|
431
|
+
const id = ulid4();
|
|
432
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
433
|
+
const ctx = input.execution_context ?? "shell";
|
|
434
|
+
db.prepare(
|
|
435
|
+
`INSERT INTO sessions (id, user_id, task, execution_context, started_at)
|
|
436
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
437
|
+
).run(id, input.user_id, input.task, ctx, now);
|
|
438
|
+
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
439
|
+
}
|
|
440
|
+
function endSession(db, sessionId) {
|
|
441
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
442
|
+
if (!session) {
|
|
443
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
444
|
+
}
|
|
445
|
+
if (session.completed_at) {
|
|
446
|
+
throw new Error(`Session already completed: ${sessionId}`);
|
|
447
|
+
}
|
|
448
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
449
|
+
db.prepare("UPDATE sessions SET completed_at = ? WHERE id = ?").run(now, sessionId);
|
|
450
|
+
return db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
451
|
+
}
|
|
452
|
+
function logStep(db, input) {
|
|
453
|
+
if (input.done_by !== "user" && input.done_by !== "agent") {
|
|
454
|
+
throw new Error(`done_by must be 'user' or 'agent', got '${input.done_by}'`);
|
|
455
|
+
}
|
|
456
|
+
if (input.rating != null && (input.rating < 1 || input.rating > 4)) {
|
|
457
|
+
throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
|
|
458
|
+
}
|
|
459
|
+
const session = db.prepare("SELECT id FROM sessions WHERE id = ?").get(input.session_id);
|
|
460
|
+
if (!session) {
|
|
461
|
+
throw new Error(`Session not found: ${input.session_id}`);
|
|
462
|
+
}
|
|
463
|
+
const id = ulid4();
|
|
464
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
465
|
+
db.prepare(
|
|
466
|
+
`INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
|
|
467
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
468
|
+
).run(
|
|
469
|
+
id,
|
|
470
|
+
input.session_id,
|
|
471
|
+
input.token_id,
|
|
472
|
+
input.done_by,
|
|
473
|
+
input.rating ?? null,
|
|
474
|
+
input.notes ?? null,
|
|
475
|
+
now
|
|
476
|
+
);
|
|
477
|
+
return db.prepare("SELECT * FROM session_steps WHERE id = ?").get(id);
|
|
478
|
+
}
|
|
479
|
+
function getSessionSummary(db, sessionId) {
|
|
480
|
+
const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
|
|
481
|
+
if (!session) {
|
|
482
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
483
|
+
}
|
|
484
|
+
const steps = db.prepare(
|
|
485
|
+
`SELECT ss.*, t.slug, t.concept, t.domain, t.bloom_level
|
|
486
|
+
FROM session_steps ss
|
|
487
|
+
JOIN tokens t ON t.id = ss.token_id
|
|
488
|
+
WHERE ss.session_id = ?
|
|
489
|
+
ORDER BY ss.created_at ASC`
|
|
490
|
+
).all(sessionId);
|
|
491
|
+
return { session, steps };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/kernel/models/agent-skill.ts
|
|
495
|
+
import { ulid as ulid5 } from "ulid";
|
|
496
|
+
function parseRow(row) {
|
|
497
|
+
return {
|
|
498
|
+
...row,
|
|
499
|
+
steps: JSON.parse(row.steps),
|
|
500
|
+
token_slugs: JSON.parse(row.token_slugs)
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function createAgentSkill(db, input) {
|
|
504
|
+
const existing = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(input.slug);
|
|
505
|
+
if (existing) {
|
|
506
|
+
throw new Error(`Agent skill already exists: ${input.slug}`);
|
|
507
|
+
}
|
|
508
|
+
const id = ulid5();
|
|
509
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
510
|
+
db.prepare(
|
|
511
|
+
`INSERT INTO agent_skills (id, slug, description, steps, token_slugs, source, created_at, updated_at)
|
|
512
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
513
|
+
).run(
|
|
514
|
+
id,
|
|
515
|
+
input.slug,
|
|
516
|
+
input.description,
|
|
517
|
+
JSON.stringify(input.steps),
|
|
518
|
+
JSON.stringify(input.token_slugs ?? []),
|
|
519
|
+
input.source ?? "learned",
|
|
520
|
+
now,
|
|
521
|
+
now
|
|
522
|
+
);
|
|
523
|
+
return parseRow(
|
|
524
|
+
db.prepare("SELECT * FROM agent_skills WHERE id = ?").get(id)
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
function getAgentSkill(db, slug) {
|
|
528
|
+
const row = db.prepare("SELECT * FROM agent_skills WHERE slug = ?").get(slug);
|
|
529
|
+
return row ? parseRow(row) : void 0;
|
|
530
|
+
}
|
|
531
|
+
function listAgentSkills(db) {
|
|
532
|
+
const rows = db.prepare("SELECT * FROM agent_skills ORDER BY created_at ASC").all();
|
|
533
|
+
return rows.map(parseRow);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/kernel/models/settings.ts
|
|
537
|
+
function getSetting(db, key) {
|
|
538
|
+
const row = db.prepare("SELECT value FROM user_config WHERE key = ?").get(key);
|
|
539
|
+
return row?.value;
|
|
540
|
+
}
|
|
541
|
+
function getAllSettings(db) {
|
|
542
|
+
const rows = db.prepare("SELECT key, value FROM user_config ORDER BY key").all();
|
|
543
|
+
const map = {};
|
|
544
|
+
for (const row of rows) {
|
|
545
|
+
map[row.key] = row.value;
|
|
546
|
+
}
|
|
547
|
+
return map;
|
|
548
|
+
}
|
|
549
|
+
function getAllSettingsDetailed(db) {
|
|
550
|
+
return db.prepare("SELECT key, value, updated_at FROM user_config ORDER BY key").all();
|
|
551
|
+
}
|
|
552
|
+
function setSetting(db, key, value) {
|
|
553
|
+
db.prepare(
|
|
554
|
+
`INSERT INTO user_config (key, value, updated_at)
|
|
555
|
+
VALUES (?, ?, datetime('now'))
|
|
556
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
|
|
557
|
+
).run(key, value);
|
|
558
|
+
}
|
|
559
|
+
function deleteSetting(db, key) {
|
|
560
|
+
const result = db.prepare("DELETE FROM user_config WHERE key = ?").run(key);
|
|
561
|
+
return result.changes > 0;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/kernel/scheduler/fsrs.ts
|
|
565
|
+
var DEFAULT_W = [
|
|
566
|
+
0.4072,
|
|
567
|
+
1.1829,
|
|
568
|
+
3.1262,
|
|
569
|
+
15.4722,
|
|
570
|
+
// w0–w3: initial stability per rating
|
|
571
|
+
7.2102,
|
|
572
|
+
0.5316,
|
|
573
|
+
1.0651,
|
|
574
|
+
// w4–w6: difficulty
|
|
575
|
+
92e-4,
|
|
576
|
+
1.5988,
|
|
577
|
+
0.1176,
|
|
578
|
+
1.0014,
|
|
579
|
+
// w7–w10: stability after forgetting
|
|
580
|
+
2.0032,
|
|
581
|
+
0.0266,
|
|
582
|
+
0.3077,
|
|
583
|
+
0.15,
|
|
584
|
+
// w11–w14: stability increase
|
|
585
|
+
0,
|
|
586
|
+
2.7849,
|
|
587
|
+
0.3477,
|
|
588
|
+
0.6831
|
|
589
|
+
// w15–w18: additional parameters
|
|
590
|
+
];
|
|
591
|
+
var DEFAULT_REQUEST_RETENTION = 0.9;
|
|
592
|
+
function clamp(value, lo, hi) {
|
|
593
|
+
return Math.min(hi, Math.max(lo, value));
|
|
594
|
+
}
|
|
595
|
+
function daysBetween(a, b) {
|
|
596
|
+
return (b.getTime() - a.getTime()) / (1e3 * 60 * 60 * 24);
|
|
597
|
+
}
|
|
598
|
+
function initialStability(w, rating) {
|
|
599
|
+
return w[rating - 1];
|
|
600
|
+
}
|
|
601
|
+
function initialDifficulty(w, rating) {
|
|
602
|
+
return clamp(w[4] - Math.exp(w[5] * (rating - 1)) + 1, 1, 10);
|
|
603
|
+
}
|
|
604
|
+
function nextDifficulty(w, d, rating) {
|
|
605
|
+
const d0ForGood = initialDifficulty(w, 3);
|
|
606
|
+
const updated = w[7] * d0ForGood + (1 - w[7]) * (d - w[6] * (rating - 3));
|
|
607
|
+
return clamp(updated, 1, 10);
|
|
608
|
+
}
|
|
609
|
+
function retrievability(elapsed, stability) {
|
|
610
|
+
if (stability <= 0) return 0;
|
|
611
|
+
return Math.pow(1 + elapsed / (9 * stability), -1);
|
|
612
|
+
}
|
|
613
|
+
function stabilityAfterSuccess(w, s, d, r, rating) {
|
|
614
|
+
const hardPenalty = rating === 2 ? w[15] : 1;
|
|
615
|
+
const easyBonus = rating === 4 ? w[16] : 1;
|
|
616
|
+
const inner = Math.exp(w[8]) * (11 - d) * Math.pow(s, -w[9]) * (Math.exp(w[10] * (1 - r)) - 1) * hardPenalty * easyBonus;
|
|
617
|
+
return s * (inner + 1);
|
|
618
|
+
}
|
|
619
|
+
function stabilityAfterForgetting(w, s, d, r) {
|
|
620
|
+
return w[11] * Math.pow(d, -w[12]) * (Math.pow(s + 1, w[13]) - 1) * Math.exp(w[14] * (1 - r));
|
|
621
|
+
}
|
|
622
|
+
function nextInterval(stability, requestRetention) {
|
|
623
|
+
const interval = 9 * stability * (1 / requestRetention - 1);
|
|
624
|
+
return Math.max(1, Math.round(interval));
|
|
625
|
+
}
|
|
626
|
+
function createFSRS(params) {
|
|
627
|
+
const resolvedParams = {
|
|
628
|
+
w: params?.w ?? [...DEFAULT_W],
|
|
629
|
+
requestRetention: params?.requestRetention ?? DEFAULT_REQUEST_RETENTION
|
|
630
|
+
};
|
|
631
|
+
function schedule(card, rating, now) {
|
|
632
|
+
const reviewTime = now ?? /* @__PURE__ */ new Date();
|
|
633
|
+
const w = resolvedParams.w;
|
|
634
|
+
const elapsed = card.lastReviewAt !== null ? Math.max(0, daysBetween(card.lastReviewAt, reviewTime)) : 0;
|
|
635
|
+
if (card.state === "new") {
|
|
636
|
+
const s = initialStability(w, rating);
|
|
637
|
+
const d = initialDifficulty(w, rating);
|
|
638
|
+
const interval2 = nextInterval(s, resolvedParams.requestRetention);
|
|
639
|
+
const dueAt2 = new Date(reviewTime);
|
|
640
|
+
dueAt2.setDate(dueAt2.getDate() + interval2);
|
|
641
|
+
return {
|
|
642
|
+
stability: s,
|
|
643
|
+
difficulty: d,
|
|
644
|
+
elapsedDays: 0,
|
|
645
|
+
scheduledDays: interval2,
|
|
646
|
+
reps: rating >= 2 ? 1 : 0,
|
|
647
|
+
lapses: rating === 1 ? 1 : 0,
|
|
648
|
+
state: "learning",
|
|
649
|
+
dueAt: dueAt2,
|
|
650
|
+
lastReviewAt: reviewTime
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const r = retrievability(elapsed, card.stability);
|
|
654
|
+
let newStability;
|
|
655
|
+
let newDifficulty;
|
|
656
|
+
let newReps;
|
|
657
|
+
let newLapses;
|
|
658
|
+
let newState;
|
|
659
|
+
if (rating === 1) {
|
|
660
|
+
newStability = stabilityAfterForgetting(w, card.stability, card.difficulty, r);
|
|
661
|
+
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
662
|
+
newReps = 0;
|
|
663
|
+
newLapses = card.lapses + 1;
|
|
664
|
+
newState = "relearning";
|
|
665
|
+
} else {
|
|
666
|
+
newStability = stabilityAfterSuccess(w, card.stability, card.difficulty, r, rating);
|
|
667
|
+
newDifficulty = nextDifficulty(w, card.difficulty, rating);
|
|
668
|
+
newReps = card.reps + 1;
|
|
669
|
+
newLapses = card.lapses;
|
|
670
|
+
newState = "review";
|
|
671
|
+
}
|
|
672
|
+
const interval = nextInterval(newStability, resolvedParams.requestRetention);
|
|
673
|
+
const dueAt = new Date(reviewTime);
|
|
674
|
+
dueAt.setDate(dueAt.getDate() + interval);
|
|
675
|
+
return {
|
|
676
|
+
stability: newStability,
|
|
677
|
+
difficulty: newDifficulty,
|
|
678
|
+
elapsedDays: elapsed,
|
|
679
|
+
scheduledDays: interval,
|
|
680
|
+
reps: newReps,
|
|
681
|
+
lapses: newLapses,
|
|
682
|
+
state: newState,
|
|
683
|
+
dueAt,
|
|
684
|
+
lastReviewAt: reviewTime
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
schedule,
|
|
689
|
+
params: Object.freeze(resolvedParams)
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/kernel/scheduler/blocker.ts
|
|
694
|
+
function cascadeBlock(db, userId, tokenSlug) {
|
|
695
|
+
const token = getTokenBySlug(db, tokenSlug);
|
|
696
|
+
if (!token) {
|
|
697
|
+
throw new Error(`Unknown token slug: ${tokenSlug}`);
|
|
698
|
+
}
|
|
699
|
+
ensureCard(db, token.id, userId);
|
|
700
|
+
db.prepare(
|
|
701
|
+
"UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
|
|
702
|
+
).run(token.id, userId);
|
|
703
|
+
const prereqs = getPrerequisites(db, token.id);
|
|
704
|
+
const surfaced = [];
|
|
705
|
+
for (const prereq of prereqs) {
|
|
706
|
+
const card = ensureCard(db, prereq.requires_id, userId);
|
|
707
|
+
if (card.blocked === 1) {
|
|
708
|
+
const prereqOfPrereq = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(prereq.requires_id);
|
|
709
|
+
if (prereqOfPrereq.n === 0) {
|
|
710
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
711
|
+
db.prepare(
|
|
712
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
713
|
+
).run(now, prereq.requires_id, userId);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
surfaced.push({
|
|
717
|
+
slug: prereq.slug,
|
|
718
|
+
concept: prereq.concept,
|
|
719
|
+
bloomLevel: prereq.bloom_level
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
blockedSlug: tokenSlug,
|
|
724
|
+
prerequisites: surfaced
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function unblockReady(db, userId) {
|
|
728
|
+
const blockedCards = db.prepare(
|
|
729
|
+
`SELECT c.token_id, t.slug, t.concept
|
|
730
|
+
FROM cards c
|
|
731
|
+
JOIN tokens t ON t.id = c.token_id
|
|
732
|
+
WHERE c.user_id = ? AND c.blocked = 1`
|
|
733
|
+
).all(userId);
|
|
734
|
+
const unblocked = [];
|
|
735
|
+
for (const card of blockedCards) {
|
|
736
|
+
const totalPrereqs = db.prepare("SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?").get(card.token_id);
|
|
737
|
+
const metPrereqs = db.prepare(
|
|
738
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
739
|
+
JOIN prerequisites p ON p.requires_id = c.token_id
|
|
740
|
+
WHERE p.token_id = ? AND c.user_id = ? AND c.reps >= 1 AND c.blocked = 0`
|
|
741
|
+
).get(card.token_id, userId);
|
|
742
|
+
if (totalPrereqs.n === 0 || metPrereqs.n === totalPrereqs.n) {
|
|
743
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
744
|
+
db.prepare(
|
|
745
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE token_id = ? AND user_id = ?"
|
|
746
|
+
).run(now, card.token_id, userId);
|
|
747
|
+
unblocked.push({ slug: card.slug, concept: card.concept });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return { unblocked };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/kernel/scheduler/interleaver.ts
|
|
754
|
+
function interleave(items, maxConsecutive = 2) {
|
|
755
|
+
if (items.length <= 1) return [...items];
|
|
756
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
757
|
+
for (const item of items) {
|
|
758
|
+
const group = byDomain.get(item.domain);
|
|
759
|
+
if (group) {
|
|
760
|
+
group.push(item);
|
|
761
|
+
} else {
|
|
762
|
+
byDomain.set(item.domain, [item]);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (byDomain.size === 1) return [...items];
|
|
766
|
+
const result = [];
|
|
767
|
+
let consecutiveCount = 0;
|
|
768
|
+
let lastDomain = null;
|
|
769
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
770
|
+
for (const domain of byDomain.keys()) {
|
|
771
|
+
cursors.set(domain, 0);
|
|
772
|
+
}
|
|
773
|
+
while (result.length < items.length) {
|
|
774
|
+
const activeDomains = [...byDomain.entries()].filter(([domain]) => cursors.get(domain) < byDomain.get(domain).length).sort((a, b) => {
|
|
775
|
+
const remainA = a[1].length - cursors.get(a[0]);
|
|
776
|
+
const remainB = b[1].length - cursors.get(b[0]);
|
|
777
|
+
return remainB - remainA;
|
|
778
|
+
});
|
|
779
|
+
if (activeDomains.length === 0) break;
|
|
780
|
+
let pickedThisRound = false;
|
|
781
|
+
for (const [domain, group] of activeDomains) {
|
|
782
|
+
const cursor = cursors.get(domain);
|
|
783
|
+
if (cursor >= group.length) continue;
|
|
784
|
+
if (domain === lastDomain && consecutiveCount >= maxConsecutive) {
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
result.push(group[cursor]);
|
|
788
|
+
cursors.set(domain, cursor + 1);
|
|
789
|
+
pickedThisRound = true;
|
|
790
|
+
if (domain === lastDomain) {
|
|
791
|
+
consecutiveCount++;
|
|
792
|
+
} else {
|
|
793
|
+
lastDomain = domain;
|
|
794
|
+
consecutiveCount = 1;
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
}
|
|
798
|
+
if (!pickedThisRound) {
|
|
799
|
+
for (const [domain, group] of activeDomains) {
|
|
800
|
+
const cursor = cursors.get(domain);
|
|
801
|
+
if (cursor >= group.length) continue;
|
|
802
|
+
result.push(group[cursor]);
|
|
803
|
+
cursors.set(domain, cursor + 1);
|
|
804
|
+
if (domain === lastDomain) {
|
|
805
|
+
consecutiveCount++;
|
|
806
|
+
} else {
|
|
807
|
+
lastDomain = domain;
|
|
808
|
+
consecutiveCount = 1;
|
|
809
|
+
}
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return result;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/kernel/scheduler/queue.ts
|
|
818
|
+
function buildReviewQueue(db, options) {
|
|
819
|
+
const maxNew = options.maxNew ?? 10;
|
|
820
|
+
const maxReviews = options.maxReviews ?? 50;
|
|
821
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
822
|
+
const nowISO = now.toISOString();
|
|
823
|
+
const dueRows = db.prepare(
|
|
824
|
+
`SELECT
|
|
825
|
+
c.id AS card_id,
|
|
826
|
+
c.token_id AS token_id,
|
|
827
|
+
t.slug AS slug,
|
|
828
|
+
t.concept AS concept,
|
|
829
|
+
t.domain AS domain,
|
|
830
|
+
t.bloom_level AS bloom_level,
|
|
831
|
+
c.state AS state,
|
|
832
|
+
c.due_at AS due_at
|
|
833
|
+
FROM cards c
|
|
834
|
+
JOIN tokens t ON t.id = c.token_id
|
|
835
|
+
WHERE c.user_id = ?
|
|
836
|
+
AND c.blocked = 0
|
|
837
|
+
AND c.due_at <= ?
|
|
838
|
+
AND c.state IN ('review', 'relearning', 'learning')
|
|
839
|
+
AND t.deprecated_at IS NULL
|
|
840
|
+
ORDER BY c.due_at ASC`
|
|
841
|
+
).all(options.userId, nowISO);
|
|
842
|
+
const newRows = db.prepare(
|
|
843
|
+
`SELECT
|
|
844
|
+
c.id AS card_id,
|
|
845
|
+
c.token_id AS token_id,
|
|
846
|
+
t.slug AS slug,
|
|
847
|
+
t.concept AS concept,
|
|
848
|
+
t.domain AS domain,
|
|
849
|
+
t.bloom_level AS bloom_level,
|
|
850
|
+
c.state AS state,
|
|
851
|
+
c.due_at AS due_at
|
|
852
|
+
FROM cards c
|
|
853
|
+
JOIN tokens t ON t.id = c.token_id
|
|
854
|
+
WHERE c.user_id = ?
|
|
855
|
+
AND c.blocked = 0
|
|
856
|
+
AND c.state = 'new'
|
|
857
|
+
AND t.deprecated_at IS NULL
|
|
858
|
+
ORDER BY t.bloom_level ASC, t.slug ASC
|
|
859
|
+
LIMIT ?`
|
|
860
|
+
).all(options.userId, maxNew);
|
|
861
|
+
const nowMs = now.getTime();
|
|
862
|
+
const sortedDue = [...dueRows].sort((a, b) => {
|
|
863
|
+
const overdueA = nowMs - new Date(a.due_at).getTime();
|
|
864
|
+
const overdueB = nowMs - new Date(b.due_at).getTime();
|
|
865
|
+
return overdueB - overdueA;
|
|
866
|
+
});
|
|
867
|
+
const interleavedDue = interleave(
|
|
868
|
+
sortedDue.map((row) => ({ ...rowToItem(row), domain: row.domain }))
|
|
869
|
+
);
|
|
870
|
+
const newItems = newRows.map(rowToItem);
|
|
871
|
+
const merged = intersperseNew(interleavedDue, newItems, 5);
|
|
872
|
+
const capped = merged.slice(0, maxReviews);
|
|
873
|
+
let newCount = 0;
|
|
874
|
+
let reviewCount = 0;
|
|
875
|
+
let relearnCount = 0;
|
|
876
|
+
const domainSet = /* @__PURE__ */ new Set();
|
|
877
|
+
for (const item of capped) {
|
|
878
|
+
domainSet.add(item.domain);
|
|
879
|
+
switch (item.state) {
|
|
880
|
+
case "new":
|
|
881
|
+
newCount++;
|
|
882
|
+
break;
|
|
883
|
+
case "relearning":
|
|
884
|
+
relearnCount++;
|
|
885
|
+
break;
|
|
886
|
+
default:
|
|
887
|
+
reviewCount++;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return {
|
|
892
|
+
items: capped,
|
|
893
|
+
newCount,
|
|
894
|
+
reviewCount,
|
|
895
|
+
relearnCount,
|
|
896
|
+
totalDomains: [...domainSet].sort()
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function rowToItem(row) {
|
|
900
|
+
return {
|
|
901
|
+
cardId: row.card_id,
|
|
902
|
+
tokenId: row.token_id,
|
|
903
|
+
slug: row.slug,
|
|
904
|
+
concept: row.concept,
|
|
905
|
+
domain: row.domain,
|
|
906
|
+
bloomLevel: row.bloom_level,
|
|
907
|
+
state: row.state,
|
|
908
|
+
dueAt: row.due_at
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function intersperseNew(reviews, newCards, interval) {
|
|
912
|
+
if (newCards.length === 0) return [...reviews];
|
|
913
|
+
if (reviews.length === 0) return [...newCards];
|
|
914
|
+
const result = [];
|
|
915
|
+
let reviewIdx = 0;
|
|
916
|
+
let newIdx = 0;
|
|
917
|
+
let position = 0;
|
|
918
|
+
while (reviewIdx < reviews.length || newIdx < newCards.length) {
|
|
919
|
+
if (newIdx < newCards.length && position > 0 && position % interval === interval - 1) {
|
|
920
|
+
result.push(newCards[newIdx]);
|
|
921
|
+
newIdx++;
|
|
922
|
+
} else if (reviewIdx < reviews.length) {
|
|
923
|
+
result.push(reviews[reviewIdx]);
|
|
924
|
+
reviewIdx++;
|
|
925
|
+
} else if (newIdx < newCards.length) {
|
|
926
|
+
result.push(newCards[newIdx]);
|
|
927
|
+
newIdx++;
|
|
928
|
+
}
|
|
929
|
+
position++;
|
|
930
|
+
}
|
|
931
|
+
return result;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/kernel/recall/prompter.ts
|
|
935
|
+
var BLOOM_VERBS = {
|
|
936
|
+
1: "Remember",
|
|
937
|
+
2: "Understand",
|
|
938
|
+
3: "Apply",
|
|
939
|
+
4: "Analyze",
|
|
940
|
+
5: "Synthesize"
|
|
941
|
+
};
|
|
942
|
+
var BLOOM_PROMPTS = {
|
|
943
|
+
1: (c) => `What is: ${c}?`,
|
|
944
|
+
2: (c) => `Explain how this works: ${c}`,
|
|
945
|
+
3: (c) => `Apply this concept: ${c}`,
|
|
946
|
+
4: (c) => `Analyze the trade-offs: ${c}`,
|
|
947
|
+
5: (c) => `Design a solution using: ${c}`
|
|
948
|
+
};
|
|
949
|
+
function generatePrompt(input) {
|
|
950
|
+
const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
|
|
951
|
+
return {
|
|
952
|
+
cardId: input.cardId,
|
|
953
|
+
tokenId: input.tokenId,
|
|
954
|
+
slug: input.slug,
|
|
955
|
+
question: BLOOM_PROMPTS[bloom](input.concept),
|
|
956
|
+
concept: input.concept,
|
|
957
|
+
domain: input.domain,
|
|
958
|
+
bloomLevel: bloom,
|
|
959
|
+
bloomVerb: BLOOM_VERBS[bloom],
|
|
960
|
+
hints: []
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/kernel/recall/evaluator.ts
|
|
965
|
+
import { ulid as ulid6 } from "ulid";
|
|
966
|
+
function evaluateRating(db, input) {
|
|
967
|
+
const card = db.prepare("SELECT * FROM cards WHERE id = ?").get(input.cardId);
|
|
968
|
+
if (!card) {
|
|
969
|
+
throw new Error(`Card not found: ${input.cardId}`);
|
|
970
|
+
}
|
|
971
|
+
const now = /* @__PURE__ */ new Date();
|
|
972
|
+
const fsrs = createFSRS();
|
|
973
|
+
const schedulingCard = {
|
|
974
|
+
stability: card.stability,
|
|
975
|
+
difficulty: card.difficulty,
|
|
976
|
+
elapsedDays: card.elapsed_days,
|
|
977
|
+
scheduledDays: card.scheduled_days,
|
|
978
|
+
reps: card.reps,
|
|
979
|
+
lapses: card.lapses,
|
|
980
|
+
state: card.state,
|
|
981
|
+
dueAt: new Date(card.due_at),
|
|
982
|
+
lastReviewAt: card.last_review_at ? new Date(card.last_review_at) : null
|
|
983
|
+
};
|
|
984
|
+
const updated = fsrs.schedule(schedulingCard, input.rating, now);
|
|
985
|
+
updateCard(db, input.cardId, {
|
|
986
|
+
stability: updated.stability,
|
|
987
|
+
difficulty: updated.difficulty,
|
|
988
|
+
elapsed_days: updated.elapsedDays,
|
|
989
|
+
scheduled_days: updated.scheduledDays,
|
|
990
|
+
reps: updated.reps,
|
|
991
|
+
lapses: updated.lapses,
|
|
992
|
+
state: updated.state,
|
|
993
|
+
due_at: updated.dueAt.toISOString(),
|
|
994
|
+
last_review_at: now.toISOString()
|
|
995
|
+
});
|
|
996
|
+
db.prepare(
|
|
997
|
+
`INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
|
|
998
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
999
|
+
).run(
|
|
1000
|
+
ulid6(),
|
|
1001
|
+
input.cardId,
|
|
1002
|
+
input.tokenId,
|
|
1003
|
+
input.userId,
|
|
1004
|
+
input.rating,
|
|
1005
|
+
input.responseTimeMs ?? null,
|
|
1006
|
+
now.toISOString(),
|
|
1007
|
+
card.due_at,
|
|
1008
|
+
input.sessionId ?? null
|
|
1009
|
+
);
|
|
1010
|
+
return {
|
|
1011
|
+
nextDueAt: updated.dueAt.toISOString(),
|
|
1012
|
+
stability: updated.stability,
|
|
1013
|
+
difficulty: updated.difficulty,
|
|
1014
|
+
state: updated.state,
|
|
1015
|
+
scheduledDays: updated.scheduledDays,
|
|
1016
|
+
reps: updated.reps,
|
|
1017
|
+
lapses: updated.lapses
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/kernel/analytics/stats.ts
|
|
1022
|
+
function q(db, sql, ...params) {
|
|
1023
|
+
return db.prepare(sql).get(...params);
|
|
1024
|
+
}
|
|
1025
|
+
function getUserStats(db, userId) {
|
|
1026
|
+
return {
|
|
1027
|
+
userId,
|
|
1028
|
+
totalTokens: q(db, "SELECT COUNT(*) as n FROM tokens").n,
|
|
1029
|
+
cardsInDeck: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ?", userId).n,
|
|
1030
|
+
dueToday: q(
|
|
1031
|
+
db,
|
|
1032
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 0 AND due_at <= datetime('now')",
|
|
1033
|
+
userId
|
|
1034
|
+
).n,
|
|
1035
|
+
blocked: q(db, "SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND blocked = 1", userId).n,
|
|
1036
|
+
mature: q(
|
|
1037
|
+
db,
|
|
1038
|
+
"SELECT COUNT(*) as n FROM cards WHERE user_id = ? AND reps >= 3 AND stability >= 21",
|
|
1039
|
+
userId
|
|
1040
|
+
).n,
|
|
1041
|
+
avgStability: (() => {
|
|
1042
|
+
const v = q(db, "SELECT AVG(stability) as v FROM cards WHERE user_id = ? AND reps > 0", userId);
|
|
1043
|
+
return v.v ? Math.round(v.v * 100) / 100 : null;
|
|
1044
|
+
})(),
|
|
1045
|
+
totalSessions: q(db, "SELECT COUNT(*) as n FROM sessions WHERE user_id = ?", userId).n,
|
|
1046
|
+
lastSession: (() => {
|
|
1047
|
+
const r = db.prepare(
|
|
1048
|
+
"SELECT started_at FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1"
|
|
1049
|
+
).get(userId);
|
|
1050
|
+
return r?.started_at ?? null;
|
|
1051
|
+
})()
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
function getDomainCompetence(db, userId) {
|
|
1055
|
+
const domains = db.prepare(
|
|
1056
|
+
`SELECT DISTINCT t.domain FROM cards c
|
|
1057
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1058
|
+
WHERE c.user_id = ? AND t.domain != ''`
|
|
1059
|
+
).all(userId);
|
|
1060
|
+
return domains.map((d) => {
|
|
1061
|
+
const total = q(
|
|
1062
|
+
db,
|
|
1063
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
1064
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1065
|
+
WHERE c.user_id = ? AND t.domain = ?`,
|
|
1066
|
+
userId,
|
|
1067
|
+
d.domain
|
|
1068
|
+
).n;
|
|
1069
|
+
const mature = q(
|
|
1070
|
+
db,
|
|
1071
|
+
`SELECT COUNT(*) as n FROM cards c
|
|
1072
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1073
|
+
WHERE c.user_id = ? AND t.domain = ? AND c.reps >= 3 AND c.stability >= 21`,
|
|
1074
|
+
userId,
|
|
1075
|
+
d.domain
|
|
1076
|
+
).n;
|
|
1077
|
+
const avgStab = q(
|
|
1078
|
+
db,
|
|
1079
|
+
`SELECT AVG(c.stability) as v FROM cards c
|
|
1080
|
+
JOIN tokens t ON t.id = c.token_id
|
|
1081
|
+
WHERE c.user_id = ? AND t.domain = ? AND c.reps > 0`,
|
|
1082
|
+
userId,
|
|
1083
|
+
d.domain
|
|
1084
|
+
).v ?? 0;
|
|
1085
|
+
const reviews = q(
|
|
1086
|
+
db,
|
|
1087
|
+
`SELECT COUNT(*) as total,
|
|
1088
|
+
SUM(CASE WHEN rating >= 2 THEN 1 ELSE 0 END) as passed
|
|
1089
|
+
FROM review_logs
|
|
1090
|
+
WHERE user_id = ? AND token_id IN (SELECT id FROM tokens WHERE domain = ?)`,
|
|
1091
|
+
userId,
|
|
1092
|
+
d.domain
|
|
1093
|
+
);
|
|
1094
|
+
const retentionRate = reviews.total > 0 ? reviews.passed / reviews.total : 0;
|
|
1095
|
+
let suggestedMode;
|
|
1096
|
+
if (retentionRate > 0.9 && avgStab > 30) {
|
|
1097
|
+
suggestedMode = "autonomy";
|
|
1098
|
+
} else if (retentionRate > 0.7 && avgStab > 7) {
|
|
1099
|
+
suggestedMode = "copilot";
|
|
1100
|
+
} else {
|
|
1101
|
+
suggestedMode = "shadowing";
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
domain: d.domain,
|
|
1105
|
+
totalCards: total,
|
|
1106
|
+
matureCards: mature,
|
|
1107
|
+
avgStability: Math.round(avgStab * 100) / 100,
|
|
1108
|
+
retentionRate: Math.round(retentionRate * 1e3) / 1e3,
|
|
1109
|
+
suggestedMode
|
|
1110
|
+
};
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/kernel/observation/analyzer.ts
|
|
1115
|
+
function parseMonitorLog(jsonl) {
|
|
1116
|
+
const events = [];
|
|
1117
|
+
for (const line of jsonl.split("\n")) {
|
|
1118
|
+
const trimmed = line.trim();
|
|
1119
|
+
if (!trimmed) continue;
|
|
1120
|
+
try {
|
|
1121
|
+
events.push(JSON.parse(trimmed));
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return events;
|
|
1126
|
+
}
|
|
1127
|
+
function pairCommands(events) {
|
|
1128
|
+
const starts = /* @__PURE__ */ new Map();
|
|
1129
|
+
const records = [];
|
|
1130
|
+
for (const e of events) {
|
|
1131
|
+
if (e.type === "command_start" && e.seq != null) {
|
|
1132
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1133
|
+
starts.set(key, e);
|
|
1134
|
+
} else if (e.type === "command_end" && e.seq != null) {
|
|
1135
|
+
const key = `${e.pid ?? 0}:${e.seq}`;
|
|
1136
|
+
const start = starts.get(key);
|
|
1137
|
+
if (start) {
|
|
1138
|
+
const startMs = new Date(start.ts).getTime();
|
|
1139
|
+
const endMs = new Date(e.ts).getTime();
|
|
1140
|
+
records.push({
|
|
1141
|
+
seq: e.seq,
|
|
1142
|
+
pid: e.pid ?? 0,
|
|
1143
|
+
command: start.command ?? "",
|
|
1144
|
+
cwd: start.cwd ?? "",
|
|
1145
|
+
startedAt: start.ts,
|
|
1146
|
+
endedAt: e.ts,
|
|
1147
|
+
durationMs: endMs - startMs,
|
|
1148
|
+
exitCode: e.exit_code ?? null
|
|
1149
|
+
});
|
|
1150
|
+
starts.delete(key);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
for (const [, start] of starts) {
|
|
1155
|
+
records.push({
|
|
1156
|
+
seq: start.seq ?? 0,
|
|
1157
|
+
pid: start.pid ?? 0,
|
|
1158
|
+
command: start.command ?? "",
|
|
1159
|
+
cwd: start.cwd ?? "",
|
|
1160
|
+
startedAt: start.ts,
|
|
1161
|
+
endedAt: null,
|
|
1162
|
+
durationMs: null,
|
|
1163
|
+
exitCode: null
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
records.sort((a, b) => new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime());
|
|
1167
|
+
return records;
|
|
1168
|
+
}
|
|
1169
|
+
var HELP_PATTERNS = ["--help", "man ", "tldr ", "help "];
|
|
1170
|
+
var HELP_WINDOW_MS = 6e4;
|
|
1171
|
+
function matchesToken(command, patterns) {
|
|
1172
|
+
const lower = command.toLowerCase();
|
|
1173
|
+
return patterns.some((p) => lower.includes(p.toLowerCase()));
|
|
1174
|
+
}
|
|
1175
|
+
function isHelpCommand(command) {
|
|
1176
|
+
const lower = command.toLowerCase();
|
|
1177
|
+
return HELP_PATTERNS.some((p) => lower.includes(p));
|
|
1178
|
+
}
|
|
1179
|
+
function commandPrefix(command) {
|
|
1180
|
+
return command.split(/\s+/).slice(0, 2).join(" ").toLowerCase();
|
|
1181
|
+
}
|
|
1182
|
+
function computeMedian(values) {
|
|
1183
|
+
if (values.length === 0) return null;
|
|
1184
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1185
|
+
const mid = Math.floor(sorted.length / 2);
|
|
1186
|
+
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1187
|
+
}
|
|
1188
|
+
function analyzeObservation(commands, tokenPatterns) {
|
|
1189
|
+
const matchedSet = /* @__PURE__ */ new Set();
|
|
1190
|
+
const ratings = [];
|
|
1191
|
+
for (const tp of tokenPatterns) {
|
|
1192
|
+
const matchIndices = [];
|
|
1193
|
+
const matchedTexts = [];
|
|
1194
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1195
|
+
if (matchesToken(commands[i].command, tp.patterns)) {
|
|
1196
|
+
matchIndices.push(i);
|
|
1197
|
+
matchedTexts.push(commands[i].command);
|
|
1198
|
+
matchedSet.add(i);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (matchIndices.length === 0) {
|
|
1202
|
+
ratings.push({
|
|
1203
|
+
tokenSlug: tp.slug,
|
|
1204
|
+
rating: null,
|
|
1205
|
+
confidence: "low",
|
|
1206
|
+
evidence: {
|
|
1207
|
+
matchedCommands: 0,
|
|
1208
|
+
helpSeeking: false,
|
|
1209
|
+
errorCount: 0,
|
|
1210
|
+
selfCorrections: 0,
|
|
1211
|
+
medianGapMs: null,
|
|
1212
|
+
thinkingGapMs: null
|
|
1213
|
+
},
|
|
1214
|
+
matchedCommandTexts: []
|
|
1215
|
+
});
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
let helpSeeking = false;
|
|
1219
|
+
for (const mi of matchIndices) {
|
|
1220
|
+
const matchTime = new Date(commands[mi].startedAt).getTime();
|
|
1221
|
+
for (let j = 0; j < commands.length; j++) {
|
|
1222
|
+
if (j === mi) continue;
|
|
1223
|
+
const cmdTime = new Date(commands[j].startedAt).getTime();
|
|
1224
|
+
if (cmdTime >= matchTime - HELP_WINDOW_MS && cmdTime < matchTime) {
|
|
1225
|
+
if (isHelpCommand(commands[j].command)) {
|
|
1226
|
+
helpSeeking = true;
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (helpSeeking) break;
|
|
1232
|
+
}
|
|
1233
|
+
let errorCount = 0;
|
|
1234
|
+
for (const mi of matchIndices) {
|
|
1235
|
+
if (commands[mi].exitCode != null && commands[mi].exitCode !== 0) {
|
|
1236
|
+
errorCount++;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
let selfCorrections = 0;
|
|
1240
|
+
const prefixGroups = /* @__PURE__ */ new Map();
|
|
1241
|
+
for (const mi of matchIndices) {
|
|
1242
|
+
const prefix = commandPrefix(commands[mi].command);
|
|
1243
|
+
prefixGroups.set(prefix, (prefixGroups.get(prefix) ?? 0) + 1);
|
|
1244
|
+
}
|
|
1245
|
+
for (const count of prefixGroups.values()) {
|
|
1246
|
+
if (count > 1) selfCorrections += count - 1;
|
|
1247
|
+
}
|
|
1248
|
+
const gaps = [];
|
|
1249
|
+
for (let k = 1; k < matchIndices.length; k++) {
|
|
1250
|
+
const prev = commands[matchIndices[k - 1]];
|
|
1251
|
+
const curr = commands[matchIndices[k]];
|
|
1252
|
+
if (prev.endedAt) {
|
|
1253
|
+
const gap = new Date(curr.startedAt).getTime() - new Date(prev.endedAt).getTime();
|
|
1254
|
+
if (gap >= 0) gaps.push(gap);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
let thinkingGapMs = null;
|
|
1258
|
+
const firstMatchIdx = matchIndices[0];
|
|
1259
|
+
if (firstMatchIdx > 0) {
|
|
1260
|
+
const prev = commands[firstMatchIdx - 1];
|
|
1261
|
+
if (prev.endedAt) {
|
|
1262
|
+
thinkingGapMs = new Date(commands[firstMatchIdx].startedAt).getTime() - new Date(prev.endedAt).getTime();
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
const medianGapMs = computeMedian(gaps);
|
|
1266
|
+
const rating = inferRating({
|
|
1267
|
+
helpSeeking,
|
|
1268
|
+
errorCount,
|
|
1269
|
+
selfCorrections,
|
|
1270
|
+
medianGapMs,
|
|
1271
|
+
thinkingGapMs,
|
|
1272
|
+
matchedCommands: matchIndices.length
|
|
1273
|
+
});
|
|
1274
|
+
const confidence = matchIndices.length >= 3 ? "high" : matchIndices.length >= 2 ? "medium" : "low";
|
|
1275
|
+
ratings.push({
|
|
1276
|
+
tokenSlug: tp.slug,
|
|
1277
|
+
rating,
|
|
1278
|
+
confidence,
|
|
1279
|
+
evidence: {
|
|
1280
|
+
matchedCommands: matchIndices.length,
|
|
1281
|
+
helpSeeking,
|
|
1282
|
+
errorCount,
|
|
1283
|
+
selfCorrections,
|
|
1284
|
+
medianGapMs,
|
|
1285
|
+
thinkingGapMs
|
|
1286
|
+
},
|
|
1287
|
+
matchedCommandTexts: matchedTexts
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
const unmatchedCommands = [];
|
|
1291
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1292
|
+
if (!matchedSet.has(i) && !isHelpCommand(commands[i].command)) {
|
|
1293
|
+
unmatchedCommands.push(commands[i].command);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
let timeSpan = null;
|
|
1297
|
+
if (commands.length > 0) {
|
|
1298
|
+
const first = commands[0];
|
|
1299
|
+
const last = commands[commands.length - 1];
|
|
1300
|
+
const endTs = last.endedAt ?? last.startedAt;
|
|
1301
|
+
timeSpan = {
|
|
1302
|
+
start: first.startedAt,
|
|
1303
|
+
end: endTs,
|
|
1304
|
+
durationMs: new Date(endTs).getTime() - new Date(first.startedAt).getTime()
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
return { ratings, unmatchedCommands, timeSpan };
|
|
1308
|
+
}
|
|
1309
|
+
function inferRating(signals) {
|
|
1310
|
+
const { helpSeeking, errorCount, selfCorrections, medianGapMs, thinkingGapMs } = signals;
|
|
1311
|
+
let negatives = 0;
|
|
1312
|
+
if (helpSeeking) negatives += 2;
|
|
1313
|
+
if (errorCount >= 3) negatives += 3;
|
|
1314
|
+
else if (errorCount >= 1) negatives += 1;
|
|
1315
|
+
if (selfCorrections >= 2) negatives += 2;
|
|
1316
|
+
else if (selfCorrections >= 1) negatives += 1;
|
|
1317
|
+
if (medianGapMs != null && medianGapMs > 3e4) negatives += 2;
|
|
1318
|
+
else if (medianGapMs != null && medianGapMs > 1e4) negatives += 1;
|
|
1319
|
+
if (thinkingGapMs != null && thinkingGapMs > 3e4) negatives += 1;
|
|
1320
|
+
if (negatives >= 5) return 1;
|
|
1321
|
+
if (negatives >= 3) return 2;
|
|
1322
|
+
if (negatives >= 1) return 3;
|
|
1323
|
+
return 4;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/kernel/observation/monitor-io.ts
|
|
1327
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, appendFileSync, statSync } from "fs";
|
|
1328
|
+
import { homedir as homedir2 } from "os";
|
|
1329
|
+
import { join as join2 } from "path";
|
|
1330
|
+
var MONITOR_DIR = join2(homedir2(), ".zam", "monitor");
|
|
1331
|
+
function getMonitorDir() {
|
|
1332
|
+
return MONITOR_DIR;
|
|
1333
|
+
}
|
|
1334
|
+
function getMonitorPath(sessionId) {
|
|
1335
|
+
return join2(MONITOR_DIR, `${sessionId}.jsonl`);
|
|
1336
|
+
}
|
|
1337
|
+
function ensureMonitorDir() {
|
|
1338
|
+
if (!existsSync2(MONITOR_DIR)) {
|
|
1339
|
+
mkdirSync2(MONITOR_DIR, { recursive: true, mode: 448 });
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function writeMonitorEvent(sessionId, event) {
|
|
1343
|
+
ensureMonitorDir();
|
|
1344
|
+
const path = getMonitorPath(sessionId);
|
|
1345
|
+
appendFileSync(path, JSON.stringify(event) + "\n");
|
|
1346
|
+
}
|
|
1347
|
+
function readMonitorLog(sessionId) {
|
|
1348
|
+
const path = getMonitorPath(sessionId);
|
|
1349
|
+
if (!existsSync2(path)) {
|
|
1350
|
+
return [];
|
|
1351
|
+
}
|
|
1352
|
+
const content = readFileSync(path, "utf-8");
|
|
1353
|
+
return parseMonitorLog(content);
|
|
1354
|
+
}
|
|
1355
|
+
function monitorLogExists(sessionId) {
|
|
1356
|
+
return existsSync2(getMonitorPath(sessionId));
|
|
1357
|
+
}
|
|
1358
|
+
function getMonitorLogStats(sessionId) {
|
|
1359
|
+
const path = getMonitorPath(sessionId);
|
|
1360
|
+
if (!existsSync2(path)) {
|
|
1361
|
+
return { exists: false, sizeBytes: 0, lineCount: 0 };
|
|
1362
|
+
}
|
|
1363
|
+
const stat = statSync(path);
|
|
1364
|
+
const content = readFileSync(path, "utf-8");
|
|
1365
|
+
const lineCount = content.split("\n").filter((l) => l.trim()).length;
|
|
1366
|
+
return { exists: true, sizeBytes: stat.size, lineCount };
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/kernel/observation/shell-hooks.ts
|
|
1370
|
+
function generateZshHooks(monitorFile, sessionId) {
|
|
1371
|
+
return `
|
|
1372
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
1373
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1374
|
+
export __ZAM_MONITOR_SEQ=0
|
|
1375
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
1376
|
+
|
|
1377
|
+
__zam_ts() {
|
|
1378
|
+
if [[ -n "\${EPOCHREALTIME:-}" ]]; then
|
|
1379
|
+
local sec="\${EPOCHREALTIME%%.*}"
|
|
1380
|
+
local frac="\${EPOCHREALTIME##*.}"
|
|
1381
|
+
frac="\${frac:0:3}"
|
|
1382
|
+
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"
|
|
1383
|
+
else
|
|
1384
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1385
|
+
fi
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
__zam_preexec() {
|
|
1389
|
+
(( __ZAM_MONITOR_SEQ++ ))
|
|
1390
|
+
local cmd="\${1//\\"/\\\\\\"}"
|
|
1391
|
+
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
1392
|
+
local ts="$(__zam_ts)"
|
|
1393
|
+
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
1394
|
+
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1395
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
__zam_precmd() {
|
|
1399
|
+
local exit_code=$?
|
|
1400
|
+
[[ $__ZAM_MONITOR_SEQ -eq 0 ]] && return
|
|
1401
|
+
local ts="$(__zam_ts)"
|
|
1402
|
+
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1403
|
+
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1404
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
autoload -Uz add-zsh-hook
|
|
1408
|
+
add-zsh-hook preexec __zam_preexec
|
|
1409
|
+
add-zsh-hook precmd __zam_precmd
|
|
1410
|
+
|
|
1411
|
+
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1412
|
+
`.trim();
|
|
1413
|
+
}
|
|
1414
|
+
function generateBashHooks(monitorFile, sessionId) {
|
|
1415
|
+
return `
|
|
1416
|
+
# ZAM monitor hooks for session ${sessionId}
|
|
1417
|
+
export __ZAM_MONITOR_FILE="${monitorFile}"
|
|
1418
|
+
export __ZAM_MONITOR_SEQ=0
|
|
1419
|
+
export __ZAM_MONITOR_SESSION="${sessionId}"
|
|
1420
|
+
export __ZAM_MONITOR_CMD_ACTIVE=0
|
|
1421
|
+
|
|
1422
|
+
__zam_ts() {
|
|
1423
|
+
date -u '+%Y-%m-%dT%H:%M:%SZ'
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
__zam_debug_trap() {
|
|
1427
|
+
[[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]] && return
|
|
1428
|
+
__ZAM_MONITOR_CMD_ACTIVE=1
|
|
1429
|
+
(( __ZAM_MONITOR_SEQ++ ))
|
|
1430
|
+
local cmd="\${BASH_COMMAND//\\"/\\\\\\"}"
|
|
1431
|
+
local cwd="\${PWD//\\"/\\\\\\"}"
|
|
1432
|
+
local ts="$(__zam_ts)"
|
|
1433
|
+
printf '{"type":"command_start","ts":"%s","command":"%s","cwd":"%s","seq":%d,"pid":%d}\\n' \\
|
|
1434
|
+
"$ts" "$cmd" "$cwd" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1435
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
__zam_prompt_cmd() {
|
|
1439
|
+
local exit_code=$?
|
|
1440
|
+
if [[ "$__ZAM_MONITOR_CMD_ACTIVE" -eq 1 ]]; then
|
|
1441
|
+
__ZAM_MONITOR_CMD_ACTIVE=0
|
|
1442
|
+
local ts="$(__zam_ts)"
|
|
1443
|
+
printf '{"type":"command_end","ts":"%s","exit_code":%d,"seq":%d,"pid":%d}\\n' \\
|
|
1444
|
+
"$ts" "$exit_code" "$__ZAM_MONITOR_SEQ" "$$" \\
|
|
1445
|
+
>> "$__ZAM_MONITOR_FILE"
|
|
1446
|
+
fi
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
trap '__zam_debug_trap' DEBUG
|
|
1450
|
+
PROMPT_COMMAND="__zam_prompt_cmd;\${PROMPT_COMMAND:-}"
|
|
1451
|
+
|
|
1452
|
+
echo "ZAM monitor active for session $__ZAM_MONITOR_SESSION"
|
|
1453
|
+
`.trim();
|
|
1454
|
+
}
|
|
1455
|
+
function generateZshUnhooks() {
|
|
1456
|
+
return `
|
|
1457
|
+
# Remove ZAM monitor hooks
|
|
1458
|
+
add-zsh-hook -d preexec __zam_preexec 2>/dev/null
|
|
1459
|
+
add-zsh-hook -d precmd __zam_precmd 2>/dev/null
|
|
1460
|
+
unset -f __zam_preexec __zam_precmd __zam_ts 2>/dev/null
|
|
1461
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION 2>/dev/null
|
|
1462
|
+
echo "ZAM monitor stopped."
|
|
1463
|
+
`.trim();
|
|
1464
|
+
}
|
|
1465
|
+
function generateBashUnhooks() {
|
|
1466
|
+
return `
|
|
1467
|
+
# Remove ZAM monitor hooks
|
|
1468
|
+
trap - DEBUG
|
|
1469
|
+
PROMPT_COMMAND="\${PROMPT_COMMAND/__zam_prompt_cmd;/}"
|
|
1470
|
+
unset -f __zam_debug_trap __zam_prompt_cmd __zam_ts 2>/dev/null
|
|
1471
|
+
unset __ZAM_MONITOR_FILE __ZAM_MONITOR_SEQ __ZAM_MONITOR_SESSION __ZAM_MONITOR_CMD_ACTIVE 2>/dev/null
|
|
1472
|
+
echo "ZAM monitor stopped."
|
|
1473
|
+
`.trim();
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/kernel/observation/skill-discovery.ts
|
|
1477
|
+
function discoverSkills(sessionCommands, options = {}) {
|
|
1478
|
+
const minSessions = options.minSessions ?? 2;
|
|
1479
|
+
const minLen = options.minSequenceLength ?? 2;
|
|
1480
|
+
const maxLen = options.maxSequenceLength ?? 5;
|
|
1481
|
+
const existing = new Set(options.existingSkillSlugs ?? []);
|
|
1482
|
+
const sessionSequences = /* @__PURE__ */ new Map();
|
|
1483
|
+
for (const [sessionId, commands] of sessionCommands) {
|
|
1484
|
+
const sequences = extractSequences(commands, minLen, maxLen);
|
|
1485
|
+
if (sequences.length > 0) {
|
|
1486
|
+
sessionSequences.set(sessionId, sequences);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const sequenceIndex = /* @__PURE__ */ new Map();
|
|
1490
|
+
for (const [, sequences] of sessionSequences) {
|
|
1491
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1492
|
+
for (const seq of sequences) {
|
|
1493
|
+
const key = seq.join(" \u2192 ");
|
|
1494
|
+
if (seen.has(key)) continue;
|
|
1495
|
+
seen.add(key);
|
|
1496
|
+
const entry = sequenceIndex.get(key);
|
|
1497
|
+
if (entry) {
|
|
1498
|
+
entry.sessionCount++;
|
|
1499
|
+
entry.totalOccurrences++;
|
|
1500
|
+
} else {
|
|
1501
|
+
sequenceIndex.set(key, {
|
|
1502
|
+
steps: seq,
|
|
1503
|
+
sessionCount: 1,
|
|
1504
|
+
totalOccurrences: 1,
|
|
1505
|
+
examples: []
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
const lastSessionId = [...sessionCommands.keys()].pop();
|
|
1511
|
+
if (lastSessionId) {
|
|
1512
|
+
const lastCommands = sessionCommands.get(lastSessionId);
|
|
1513
|
+
for (const [key, entry] of sequenceIndex) {
|
|
1514
|
+
if (entry.sessionCount >= minSessions) {
|
|
1515
|
+
entry.examples = findExamplesForSequence(lastCommands, entry.steps);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const candidates = [...sequenceIndex.values()].filter(
|
|
1520
|
+
(s) => s.sessionCount >= minSessions
|
|
1521
|
+
);
|
|
1522
|
+
const pruned = removeSubsequences(candidates);
|
|
1523
|
+
const proposals = [];
|
|
1524
|
+
for (const seq of pruned) {
|
|
1525
|
+
const slug = generateSlug(seq.steps);
|
|
1526
|
+
if (existing.has(slug)) continue;
|
|
1527
|
+
proposals.push({
|
|
1528
|
+
slug,
|
|
1529
|
+
description: describeSequence(seq.steps),
|
|
1530
|
+
steps: seq.steps,
|
|
1531
|
+
sessionCount: seq.sessionCount,
|
|
1532
|
+
confidence: seq.sessionCount >= 4 ? "high" : seq.sessionCount >= 3 ? "medium" : "low",
|
|
1533
|
+
examples: seq.examples
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
const confidenceOrder = { high: 0, medium: 1, low: 2 };
|
|
1537
|
+
proposals.sort((a, b) => {
|
|
1538
|
+
const confDiff = confidenceOrder[a.confidence] - confidenceOrder[b.confidence];
|
|
1539
|
+
if (confDiff !== 0) return confDiff;
|
|
1540
|
+
return b.sessionCount - a.sessionCount;
|
|
1541
|
+
});
|
|
1542
|
+
return proposals;
|
|
1543
|
+
}
|
|
1544
|
+
function normalizeCommand(command) {
|
|
1545
|
+
const parts = command.trim().split(/\s+/);
|
|
1546
|
+
const multiWord = ["docker compose", "npm run", "npx", "git"];
|
|
1547
|
+
const lower = command.toLowerCase();
|
|
1548
|
+
for (const mw of multiWord) {
|
|
1549
|
+
if (lower.startsWith(mw) && parts.length >= mw.split(" ").length + 1) {
|
|
1550
|
+
return parts.slice(0, mw.split(" ").length + 1).join(" ").toLowerCase();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return parts.slice(0, Math.min(2, parts.length)).join(" ").toLowerCase();
|
|
1554
|
+
}
|
|
1555
|
+
function extractSequences(commands, minLen, maxLen) {
|
|
1556
|
+
const filtered = commands.filter((c) => {
|
|
1557
|
+
const lower = c.command.toLowerCase().trim();
|
|
1558
|
+
return lower.length > 0 && !lower.startsWith("cd ") && lower !== "cd" && lower !== "ls" && lower !== "pwd" && lower !== "clear" && lower !== "exit" && !lower.startsWith("echo ");
|
|
1559
|
+
});
|
|
1560
|
+
const normalized = filtered.map((c) => normalizeCommand(c.command));
|
|
1561
|
+
const sequences = [];
|
|
1562
|
+
for (let len = minLen; len <= maxLen; len++) {
|
|
1563
|
+
for (let i = 0; i <= normalized.length - len; i++) {
|
|
1564
|
+
const seq = normalized.slice(i, i + len);
|
|
1565
|
+
if (new Set(seq).size >= 2) {
|
|
1566
|
+
sequences.push(seq);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return sequences;
|
|
1571
|
+
}
|
|
1572
|
+
function findExamplesForSequence(commands, steps) {
|
|
1573
|
+
const normalized = commands.map((c) => ({
|
|
1574
|
+
norm: normalizeCommand(c.command),
|
|
1575
|
+
full: c.command
|
|
1576
|
+
}));
|
|
1577
|
+
for (let i = 0; i <= normalized.length - steps.length; i++) {
|
|
1578
|
+
let match = true;
|
|
1579
|
+
for (let j = 0; j < steps.length; j++) {
|
|
1580
|
+
if (normalized[i + j].norm !== steps[j]) {
|
|
1581
|
+
match = false;
|
|
1582
|
+
break;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
if (match) {
|
|
1586
|
+
return normalized.slice(i, i + steps.length).map((n) => n.full);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return [];
|
|
1590
|
+
}
|
|
1591
|
+
function removeSubsequences(candidates) {
|
|
1592
|
+
const sorted = [...candidates].sort((a, b) => b.steps.length - a.steps.length);
|
|
1593
|
+
const result = [];
|
|
1594
|
+
for (const candidate of sorted) {
|
|
1595
|
+
const key = candidate.steps.join(" \u2192 ");
|
|
1596
|
+
const isSubsequence = result.some((longer) => {
|
|
1597
|
+
const longerKey = longer.steps.join(" \u2192 ");
|
|
1598
|
+
return longerKey.includes(key) && longerKey !== key;
|
|
1599
|
+
});
|
|
1600
|
+
if (!isSubsequence) {
|
|
1601
|
+
result.push(candidate);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return result;
|
|
1605
|
+
}
|
|
1606
|
+
function generateSlug(steps) {
|
|
1607
|
+
return steps.map((s) => {
|
|
1608
|
+
const parts = s.split(/\s+/);
|
|
1609
|
+
return parts[parts.length - 1];
|
|
1610
|
+
}).join("-");
|
|
1611
|
+
}
|
|
1612
|
+
function describeSequence(steps) {
|
|
1613
|
+
return `Recurring pattern: ${steps.join(" \u2192 ")}`;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/kernel/goals/engine.ts
|
|
1617
|
+
import { readdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3 } from "fs";
|
|
1618
|
+
import { join as join3, basename } from "path";
|
|
1619
|
+
|
|
1620
|
+
// src/kernel/goals/parser.ts
|
|
1621
|
+
function parseGoalFile(content, slug, filePath) {
|
|
1622
|
+
const { frontmatter, body } = splitFrontmatter(content);
|
|
1623
|
+
const validStatuses = ["active", "completed", "paused", "abandoned"];
|
|
1624
|
+
const status = validStatuses.includes(frontmatter.status) ? frontmatter.status : "active";
|
|
1625
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1626
|
+
return {
|
|
1627
|
+
slug,
|
|
1628
|
+
title: frontmatter.title || slug,
|
|
1629
|
+
status,
|
|
1630
|
+
parent: frontmatter.parent || null,
|
|
1631
|
+
created: frontmatter.created || now,
|
|
1632
|
+
updated: frontmatter.updated || now,
|
|
1633
|
+
body,
|
|
1634
|
+
filePath
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function serializeGoal(goal) {
|
|
1638
|
+
const lines = [
|
|
1639
|
+
"---",
|
|
1640
|
+
`title: ${goal.title}`,
|
|
1641
|
+
`status: ${goal.status}`
|
|
1642
|
+
];
|
|
1643
|
+
if (goal.parent) {
|
|
1644
|
+
lines.push(`parent: ${goal.parent}`);
|
|
1645
|
+
}
|
|
1646
|
+
lines.push(`created: ${goal.created}`);
|
|
1647
|
+
lines.push(`updated: ${goal.updated}`);
|
|
1648
|
+
lines.push("---");
|
|
1649
|
+
lines.push("");
|
|
1650
|
+
if (goal.body.trim()) {
|
|
1651
|
+
lines.push(goal.body.trim());
|
|
1652
|
+
lines.push("");
|
|
1653
|
+
}
|
|
1654
|
+
return lines.join("\n");
|
|
1655
|
+
}
|
|
1656
|
+
function extractTasks(body) {
|
|
1657
|
+
const tasks = [];
|
|
1658
|
+
const taskRegex = /^[-*]\s+\[([ xX])\]\s+(.+)$/gm;
|
|
1659
|
+
let match;
|
|
1660
|
+
while ((match = taskRegex.exec(body)) !== null) {
|
|
1661
|
+
tasks.push({
|
|
1662
|
+
done: match[1] !== " ",
|
|
1663
|
+
text: match[2].trim()
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
return tasks;
|
|
1667
|
+
}
|
|
1668
|
+
function extractTokenRefs(body) {
|
|
1669
|
+
const tokensSection = body.match(/## Tokens\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1670
|
+
if (!tokensSection) return [];
|
|
1671
|
+
const refs = [];
|
|
1672
|
+
const lines = tokensSection[1].split("\n");
|
|
1673
|
+
for (const line of lines) {
|
|
1674
|
+
const match = line.match(/^[-*]\s+(\S+)/);
|
|
1675
|
+
if (match) {
|
|
1676
|
+
refs.push(match[1]);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return refs;
|
|
1680
|
+
}
|
|
1681
|
+
function splitFrontmatter(content) {
|
|
1682
|
+
const trimmed = content.trim();
|
|
1683
|
+
if (!trimmed.startsWith("---")) {
|
|
1684
|
+
return { frontmatter: {}, body: trimmed };
|
|
1685
|
+
}
|
|
1686
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
1687
|
+
if (endIndex === -1) {
|
|
1688
|
+
return { frontmatter: {}, body: trimmed };
|
|
1689
|
+
}
|
|
1690
|
+
const fmBlock = trimmed.slice(3, endIndex).trim();
|
|
1691
|
+
const body = trimmed.slice(endIndex + 3).trim();
|
|
1692
|
+
const frontmatter = {};
|
|
1693
|
+
for (const line of fmBlock.split("\n")) {
|
|
1694
|
+
const colonIndex = line.indexOf(":");
|
|
1695
|
+
if (colonIndex === -1) continue;
|
|
1696
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1697
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
1698
|
+
if (key && value) {
|
|
1699
|
+
frontmatter[key] = value;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return { frontmatter, body };
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/kernel/goals/engine.ts
|
|
1706
|
+
function listGoals(goalsDir) {
|
|
1707
|
+
if (!existsSync3(goalsDir)) return [];
|
|
1708
|
+
const files = readdirSync(goalsDir).filter(
|
|
1709
|
+
(f) => f.endsWith(".md") && f !== "README.md"
|
|
1710
|
+
);
|
|
1711
|
+
const summaries = [];
|
|
1712
|
+
for (const file of files) {
|
|
1713
|
+
const filePath = join3(goalsDir, file);
|
|
1714
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1715
|
+
const slug = basename(file, ".md");
|
|
1716
|
+
const goal = parseGoalFile(content, slug, filePath);
|
|
1717
|
+
const tasks = extractTasks(goal.body);
|
|
1718
|
+
const tokens = extractTokenRefs(goal.body);
|
|
1719
|
+
summaries.push({
|
|
1720
|
+
slug: goal.slug,
|
|
1721
|
+
title: goal.title,
|
|
1722
|
+
status: goal.status,
|
|
1723
|
+
parent: goal.parent,
|
|
1724
|
+
taskCount: tasks.length,
|
|
1725
|
+
tasksDone: tasks.filter((t) => t.done).length,
|
|
1726
|
+
tokenCount: tokens.length
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
const statusOrder = {
|
|
1730
|
+
active: 0,
|
|
1731
|
+
paused: 1,
|
|
1732
|
+
completed: 2,
|
|
1733
|
+
abandoned: 3
|
|
1734
|
+
};
|
|
1735
|
+
summaries.sort((a, b) => {
|
|
1736
|
+
const statusDiff = statusOrder[a.status] - statusOrder[b.status];
|
|
1737
|
+
if (statusDiff !== 0) return statusDiff;
|
|
1738
|
+
return a.title.localeCompare(b.title);
|
|
1739
|
+
});
|
|
1740
|
+
return summaries;
|
|
1741
|
+
}
|
|
1742
|
+
function getGoal(goalsDir, slug) {
|
|
1743
|
+
const filePath = join3(goalsDir, `${slug}.md`);
|
|
1744
|
+
if (!existsSync3(filePath)) return void 0;
|
|
1745
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1746
|
+
return parseGoalFile(content, slug, filePath);
|
|
1747
|
+
}
|
|
1748
|
+
function createGoal(goalsDir, input) {
|
|
1749
|
+
const filePath = join3(goalsDir, `${input.slug}.md`);
|
|
1750
|
+
if (existsSync3(filePath)) {
|
|
1751
|
+
throw new Error(`Goal already exists: ${input.slug}`);
|
|
1752
|
+
}
|
|
1753
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1754
|
+
const goal = {
|
|
1755
|
+
slug: input.slug,
|
|
1756
|
+
title: input.title,
|
|
1757
|
+
status: input.status ?? "active",
|
|
1758
|
+
parent: input.parent ?? null,
|
|
1759
|
+
created: now,
|
|
1760
|
+
updated: now,
|
|
1761
|
+
body: input.description ? `## Description
|
|
1762
|
+
${input.description}
|
|
1763
|
+
|
|
1764
|
+
## Tasks
|
|
1765
|
+
|
|
1766
|
+
## Tokens` : "## Description\n\n## Tasks\n\n## Tokens",
|
|
1767
|
+
filePath
|
|
1768
|
+
};
|
|
1769
|
+
writeFileSync(filePath, serializeGoal(goal), "utf-8");
|
|
1770
|
+
return goal;
|
|
1771
|
+
}
|
|
1772
|
+
function updateGoalStatus(goalsDir, slug, status) {
|
|
1773
|
+
const goal = getGoal(goalsDir, slug);
|
|
1774
|
+
if (!goal) throw new Error(`Goal not found: ${slug}`);
|
|
1775
|
+
goal.status = status;
|
|
1776
|
+
goal.updated = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1777
|
+
writeFileSync(goal.filePath, serializeGoal(goal), "utf-8");
|
|
1778
|
+
return goal;
|
|
1779
|
+
}
|
|
1780
|
+
function getGoalTree(goalsDir) {
|
|
1781
|
+
const all = listGoals(goalsDir);
|
|
1782
|
+
const bySlug = new Map(all.map((g) => [g.slug, g]));
|
|
1783
|
+
const roots = [];
|
|
1784
|
+
const children = /* @__PURE__ */ new Map();
|
|
1785
|
+
for (const g of all) {
|
|
1786
|
+
if (g.parent && bySlug.has(g.parent)) {
|
|
1787
|
+
const list = children.get(g.parent) ?? [];
|
|
1788
|
+
list.push(g);
|
|
1789
|
+
children.set(g.parent, list);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
for (const g of all) {
|
|
1793
|
+
if (!g.parent || !bySlug.has(g.parent)) {
|
|
1794
|
+
roots.push({ ...g, children: children.get(g.slug) ?? [] });
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return roots;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/kernel/connectors/azure-devops.ts
|
|
1801
|
+
function loadADOConfig(db) {
|
|
1802
|
+
const orgUrl = getSetting(db, "ado.org_url");
|
|
1803
|
+
const project = getSetting(db, "ado.project");
|
|
1804
|
+
const pat = getSetting(db, "ado.pat");
|
|
1805
|
+
if (!orgUrl || !project || !pat) return null;
|
|
1806
|
+
return { orgUrl: orgUrl.replace(/\/+$/, ""), project, pat };
|
|
1807
|
+
}
|
|
1808
|
+
function authHeader(pat) {
|
|
1809
|
+
return `Basic ${Buffer.from(`:${pat}`).toString("base64")}`;
|
|
1810
|
+
}
|
|
1811
|
+
async function fetchActiveWorkItems(config) {
|
|
1812
|
+
const { orgUrl, project, pat } = config;
|
|
1813
|
+
const wiqlUrl = `${orgUrl}/${project}/_apis/wit/wiql?api-version=7.1`;
|
|
1814
|
+
const wiqlBody = {
|
|
1815
|
+
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`
|
|
1816
|
+
};
|
|
1817
|
+
const wiqlRes = await fetch(wiqlUrl, {
|
|
1818
|
+
method: "POST",
|
|
1819
|
+
headers: {
|
|
1820
|
+
Authorization: authHeader(pat),
|
|
1821
|
+
"Content-Type": "application/json"
|
|
1822
|
+
},
|
|
1823
|
+
body: JSON.stringify(wiqlBody)
|
|
1824
|
+
});
|
|
1825
|
+
if (!wiqlRes.ok) {
|
|
1826
|
+
const text = await wiqlRes.text();
|
|
1827
|
+
throw new Error(`ADO WIQL query failed (${wiqlRes.status}): ${text}`);
|
|
1828
|
+
}
|
|
1829
|
+
const wiqlData = await wiqlRes.json();
|
|
1830
|
+
const ids = wiqlData.workItems.map((wi) => wi.id);
|
|
1831
|
+
if (ids.length === 0) return [];
|
|
1832
|
+
const batchIds = ids.slice(0, 200);
|
|
1833
|
+
const fields = "System.Id,System.Title,System.State,System.WorkItemType,System.AssignedTo";
|
|
1834
|
+
const detailUrl = `${orgUrl}/${project}/_apis/wit/workitems?ids=${batchIds.join(",")}&fields=${fields}&api-version=7.1`;
|
|
1835
|
+
const detailRes = await fetch(detailUrl, {
|
|
1836
|
+
headers: { Authorization: authHeader(pat) }
|
|
1837
|
+
});
|
|
1838
|
+
if (!detailRes.ok) {
|
|
1839
|
+
const text = await detailRes.text();
|
|
1840
|
+
throw new Error(`ADO work items fetch failed (${detailRes.status}): ${text}`);
|
|
1841
|
+
}
|
|
1842
|
+
const detailData = await detailRes.json();
|
|
1843
|
+
return detailData.value.map((wi) => ({
|
|
1844
|
+
id: wi.id,
|
|
1845
|
+
title: wi.fields["System.Title"],
|
|
1846
|
+
state: wi.fields["System.State"],
|
|
1847
|
+
type: wi.fields["System.WorkItemType"],
|
|
1848
|
+
assignedTo: wi.fields["System.AssignedTo"]?.displayName ?? ""
|
|
1849
|
+
}));
|
|
1850
|
+
}
|
|
1851
|
+
export {
|
|
1852
|
+
addPrerequisite,
|
|
1853
|
+
analyzeObservation,
|
|
1854
|
+
buildReviewQueue,
|
|
1855
|
+
cascadeBlock,
|
|
1856
|
+
createAgentSkill,
|
|
1857
|
+
createFSRS,
|
|
1858
|
+
createGoal,
|
|
1859
|
+
createToken,
|
|
1860
|
+
deleteSetting,
|
|
1861
|
+
deprecateToken,
|
|
1862
|
+
discoverSkills,
|
|
1863
|
+
endSession,
|
|
1864
|
+
ensureCard,
|
|
1865
|
+
ensureMonitorDir,
|
|
1866
|
+
evaluateRating,
|
|
1867
|
+
extractTasks,
|
|
1868
|
+
extractTokenRefs,
|
|
1869
|
+
fetchActiveWorkItems,
|
|
1870
|
+
findTokens,
|
|
1871
|
+
generateBashHooks,
|
|
1872
|
+
generateBashUnhooks,
|
|
1873
|
+
generatePrompt,
|
|
1874
|
+
generateZshHooks,
|
|
1875
|
+
generateZshUnhooks,
|
|
1876
|
+
getAgentSkill,
|
|
1877
|
+
getAllSettings,
|
|
1878
|
+
getAllSettingsDetailed,
|
|
1879
|
+
getBlockedCards,
|
|
1880
|
+
getCard,
|
|
1881
|
+
getDefaultDbPath,
|
|
1882
|
+
getDependents,
|
|
1883
|
+
getDomainCompetence,
|
|
1884
|
+
getDueCards,
|
|
1885
|
+
getGoal,
|
|
1886
|
+
getGoalTree,
|
|
1887
|
+
getMonitorDir,
|
|
1888
|
+
getMonitorLogStats,
|
|
1889
|
+
getMonitorPath,
|
|
1890
|
+
getPrerequisites,
|
|
1891
|
+
getReviewsForCard,
|
|
1892
|
+
getReviewsForUser,
|
|
1893
|
+
getSessionSummary,
|
|
1894
|
+
getSetting,
|
|
1895
|
+
getTokenById,
|
|
1896
|
+
getTokenBySlug,
|
|
1897
|
+
getUserStats,
|
|
1898
|
+
interleave,
|
|
1899
|
+
listAgentSkills,
|
|
1900
|
+
listGoals,
|
|
1901
|
+
listTokens,
|
|
1902
|
+
loadADOConfig,
|
|
1903
|
+
logReview,
|
|
1904
|
+
logStep,
|
|
1905
|
+
monitorLogExists,
|
|
1906
|
+
openDatabase,
|
|
1907
|
+
openDatabaseWithSync,
|
|
1908
|
+
pairCommands,
|
|
1909
|
+
parseGoalFile,
|
|
1910
|
+
parseMonitorLog,
|
|
1911
|
+
readMonitorLog,
|
|
1912
|
+
serializeGoal,
|
|
1913
|
+
setSetting,
|
|
1914
|
+
startSession,
|
|
1915
|
+
unblockReady,
|
|
1916
|
+
updateCard,
|
|
1917
|
+
updateGoalStatus,
|
|
1918
|
+
writeMonitorEvent
|
|
1919
|
+
};
|
|
1920
|
+
//# sourceMappingURL=index.js.map
|