total-recall-brain 3.44.0 → 3.45.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/bin/total-recall.mjs +6 -4
- package/docs/ARCHITECTURE.md +10 -7
- package/docs/reference/cli-reference.md +13 -8
- package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +2 -2
- package/frontend/src/pages/PluginsPage.tsx +1 -1
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
- package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +25 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +124 -24
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +147 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +92 -18
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +42 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +26 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +25 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +285 -40
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
- package/src/cli/agent.mjs +1 -1
- package/src/cli/command.mjs +41 -2
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/init.mjs +0 -25
- package/src/cli/lessons.mjs +51 -0
- package/src/cli/memory-plugin.mjs +1 -1
- package/src/cli/plugin/copies.mjs +64 -14
- package/src/cli/plugin/create.mjs +6 -7
- package/src/cli/plugin/index.mjs +11 -7
- package/src/cli/plugin/install.mjs +10 -15
- package/src/cli/plugin/remove.mjs +1 -1
- package/src/cli/plugin/search.mjs +2 -2
- package/src/cli/preflight.mjs +19 -6
- package/src/cli/proposals.mjs +1 -1
- package/src/cli/recall.mjs +66 -23
- package/src/cli/remember.mjs +3 -14
- package/src/cli/server.mjs +18 -0
- package/src/cli/startup.mjs +1 -0
- package/src/cli/task.mjs +9 -2
- package/src/cli/update.mjs +6 -2
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +31 -11
- package/src/core/claude-hooks.mjs +3 -1
- package/src/core/cleanup-task.mjs +15 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-maintenance.mjs +4 -1
- package/src/core/cli-semantic-index.mjs +23 -3
- package/src/core/context-compiler.mjs +20 -0
- package/src/core/daemon-loop.mjs +39 -62
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/dream.mjs +33 -105
- package/src/core/ensure-dependencies.mjs +148 -0
- package/src/core/fast-recall.mjs +2 -2
- package/src/core/field-evaluation.mjs +23 -0
- package/src/core/gate-mode.mjs +1 -1
- package/src/core/harness-hooks.mjs +14 -2
- package/src/core/lesson-hooks.mjs +74 -0
- package/src/core/lesson-memory.mjs +24 -0
- package/src/core/lesson-sessions.mjs +52 -0
- package/src/core/lessons.mjs +96 -0
- package/src/core/meta-harness.mjs +2 -2
- package/src/core/optimizer.mjs +66 -165
- package/src/core/plugin-contracts/card.mjs +3 -0
- package/src/core/plugin-loader.mjs +12 -3
- package/src/core/plugin-runtime.mjs +40 -1
- package/src/core/plugin-store.mjs +35 -23
- package/src/core/post-mortem.mjs +52 -36
- package/src/core/preflight-gate.mjs +23 -2
- package/src/core/proposal-applier.mjs +31 -2
- package/src/core/repo-sync.mjs +4 -6
- package/src/core/required-plugins.mjs +26 -4
- package/src/core/rule-length.mjs +23 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/scheduler.mjs +35 -43
- package/src/core/schema.mjs +1 -2
- package/src/core/search.mjs +4 -3
- package/src/core/server-service.mjs +148 -0
- package/src/core/session-watcher.mjs +19 -1
- package/src/core/skill-gap.mjs +16 -0
- package/src/core/ssss-kernel-bridge.mjs +32 -4
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +65 -54
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +1 -0
- package/src/core/vector-field.mjs +134 -192
- package/src/server/invoke-core-child.mjs +24 -0
- package/src/server/invoke-core-operations.mjs +102 -0
- package/src/server/route-manifest.json +0 -56
- package/src/server/routes/brains.mjs +0 -18
- package/src/server/routes/context.mjs +0 -41
- package/src/server/routes/invoke.mjs +26 -1
- package/src/server/routes/ssss.mjs +0 -169
- package/src/server/routes/system.mjs +0 -15
- package/src/server/routes/tasks.mjs +0 -26
- package/src/server/routes/vault.mjs +2 -23
- package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
- package/src/core/append-log.mjs +0 -131
- package/src/core/plugin-tasks.mjs +0 -173
- package/templates/onboarding-interview.md +0 -82
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
package/src/core/dream.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
4
|
import matter from './frontmatter.mjs';
|
|
6
5
|
import { writeNode, walkMd } from './vault.mjs';
|
|
7
6
|
import { updateNodeInPlace } from './validated-write.mjs';
|
|
@@ -127,7 +126,7 @@ export function autoPruneStorage(brainDir, vaultDir, conflictsDir) {
|
|
|
127
126
|
// Retrieve node slugs of active research projects so we never prune their drafts in progress
|
|
128
127
|
const activeResearchSlugs = new Set();
|
|
129
128
|
try {
|
|
130
|
-
const queue = loadQueue();
|
|
129
|
+
const queue = loadQueue(brainDir);
|
|
131
130
|
for (const item of queue) {
|
|
132
131
|
if (item.status === 'pending' || item.status === 'in_progress') {
|
|
133
132
|
if (item.node_slug) {
|
|
@@ -152,6 +151,13 @@ export function autoPruneStorage(brainDir, vaultDir, conflictsDir) {
|
|
|
152
151
|
if (activeResearchSlugs.has(baseName)) {
|
|
153
152
|
continue;
|
|
154
153
|
}
|
|
154
|
+
// Lesson drafts remain reviewable until promotion or explicit disposition.
|
|
155
|
+
if (dir.startsWith(inboxDir) && file.endsWith('.md')) {
|
|
156
|
+
try {
|
|
157
|
+
const { data } = matter(fs.readFileSync(fullPath, 'utf8'));
|
|
158
|
+
if (data.x_review_hold || data.tags?.includes('post-mortem') || data.tags?.includes('lesson-review')) continue;
|
|
159
|
+
} catch { continue; }
|
|
160
|
+
}
|
|
155
161
|
if (now - stat.mtimeMs > maxAge) {
|
|
156
162
|
fs.unlinkSync(fullPath);
|
|
157
163
|
}
|
|
@@ -244,22 +250,14 @@ export function autoPruneStorage(brainDir, vaultDir, conflictsDir) {
|
|
|
244
250
|
|
|
245
251
|
import {
|
|
246
252
|
generateMemoryCleanupProposals,
|
|
247
|
-
generateStaleKnowledgeRefreshProposals,
|
|
248
253
|
evaluateProposalGate,
|
|
249
254
|
dedupeProposals,
|
|
255
|
+
runSmartDecay,
|
|
250
256
|
} from './optimizer.mjs';
|
|
251
257
|
import { applyAcceptedProposals } from './proposal-applier.mjs';
|
|
252
258
|
|
|
253
259
|
const DREAM_PROMOTION_THRESHOLD = 0.7;
|
|
254
260
|
|
|
255
|
-
// Master switch for the stale-knowledge-refresh *ticket generator* (see PHASE 4).
|
|
256
|
-
// Disabled 2026-08-01: it filed one .md per stale node every cycle (16,401
|
|
257
|
-
// unread tickets at its peak). Its successor, which queued "Verify
|
|
258
|
-
// still-current" research every cycle, was removed too (RESEARCH_SYSTEM2):
|
|
259
|
-
// research runs only when a human asks or a project needs it. Stale nodes are
|
|
260
|
-
// listed for a human by `total-recall proposals stale` / GET /api/proposals/stale.
|
|
261
|
-
const ENABLE_STALE_KNOWLEDGE_REFRESH = false;
|
|
262
|
-
|
|
263
261
|
/**
|
|
264
262
|
* Phase 1: Light Sleep. Scan for modifications.
|
|
265
263
|
*/
|
|
@@ -298,6 +296,7 @@ export function loadCandidatesFromInbox(brainDirPath) {
|
|
|
298
296
|
const { data, content } = matter(raw);
|
|
299
297
|
if (!data || (!data.slug && !data.title && !String(content || '').trim())) continue;
|
|
300
298
|
const status = data.status || 'draft';
|
|
299
|
+
if (data.x_review_hold === true) continue;
|
|
301
300
|
if (status === 'archived' || status === 'active') continue;
|
|
302
301
|
candidates.push({
|
|
303
302
|
...data,
|
|
@@ -319,91 +318,12 @@ export function loadCandidatesFromInbox(brainDirPath) {
|
|
|
319
318
|
return candidates;
|
|
320
319
|
}
|
|
321
320
|
|
|
322
|
-
/**
|
|
323
|
-
* Lightweight session → candidate extraction (deterministic heuristics).
|
|
324
|
-
* User lines that signal durable preferences/rules become draft candidates.
|
|
325
|
-
*/
|
|
326
|
-
export function loadCandidatesFromSessions(sessionsDir, { maxFiles = 8, maxCandidates = 20 } = {}) {
|
|
327
|
-
const candidates = [];
|
|
328
|
-
if (!fs.existsSync(sessionsDir)) return candidates;
|
|
329
|
-
|
|
330
|
-
const signal =
|
|
331
|
-
/\b(always|never|prefer|remember|must not|must|invariant|don't|do not|from now on)\b/i;
|
|
332
|
-
|
|
333
|
-
let files = [];
|
|
334
|
-
try {
|
|
335
|
-
files = fs
|
|
336
|
-
.readdirSync(sessionsDir)
|
|
337
|
-
.filter((f) => f.endsWith('.jsonl'))
|
|
338
|
-
.map((f) => ({
|
|
339
|
-
name: f,
|
|
340
|
-
mtime: fs.statSync(path.join(sessionsDir, f)).mtimeMs,
|
|
341
|
-
}))
|
|
342
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
343
|
-
.slice(0, maxFiles);
|
|
344
|
-
} catch {
|
|
345
|
-
return candidates;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
for (const file of files) {
|
|
349
|
-
let lines;
|
|
350
|
-
try {
|
|
351
|
-
lines = fs.readFileSync(path.join(sessionsDir, file.name), 'utf8').split('\n').filter(Boolean);
|
|
352
|
-
} catch {
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
for (const line of lines) {
|
|
356
|
-
if (candidates.length >= maxCandidates) return candidates;
|
|
357
|
-
let entry;
|
|
358
|
-
try {
|
|
359
|
-
entry = JSON.parse(line);
|
|
360
|
-
} catch {
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
const content = entry.content || entry.text || '';
|
|
364
|
-
if (!content || content.length < 24 || content.length > 2000) continue;
|
|
365
|
-
if (entry.type === 'tool_call') continue;
|
|
366
|
-
const role = entry.role;
|
|
367
|
-
if (role && role !== 'user' && role !== 'human') continue;
|
|
368
|
-
if (!signal.test(content)) continue;
|
|
369
|
-
|
|
370
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 10);
|
|
371
|
-
candidates.push({
|
|
372
|
-
slug: `session-extract-${hash}`,
|
|
373
|
-
category: 'preferences',
|
|
374
|
-
title: content.slice(0, 80).replace(/\s+/g, ' '),
|
|
375
|
-
status: 'draft',
|
|
376
|
-
confidence: 0.55,
|
|
377
|
-
importance: 3,
|
|
378
|
-
evidence_count: 1,
|
|
379
|
-
modality: /must not|never|don't|do not/i.test(content) ? 'must_not' : 'should',
|
|
380
|
-
subject: 'user',
|
|
381
|
-
predicate: 'prefers',
|
|
382
|
-
object: 'behavior',
|
|
383
|
-
tags: ['session-extract', 'dream-rem'],
|
|
384
|
-
source: {
|
|
385
|
-
type: 'session',
|
|
386
|
-
session_id: path.basename(file.name, '.jsonl'),
|
|
387
|
-
evidence_count: 1,
|
|
388
|
-
},
|
|
389
|
-
body: content,
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return candidates;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Collect all REM candidates (inbox + sessions). Dedupe by slug.
|
|
398
|
-
*/
|
|
321
|
+
/** Collect reviewed inbox candidates. Session models run through lesson-review. */
|
|
399
322
|
export function collectRemCandidates({ brainDirPath, sessionsDir }) {
|
|
400
323
|
const bySlug = new Map();
|
|
401
324
|
for (const c of loadCandidatesFromInbox(brainDirPath)) {
|
|
402
325
|
bySlug.set(c.slug, c);
|
|
403
326
|
}
|
|
404
|
-
for (const c of loadCandidatesFromSessions(sessionsDir)) {
|
|
405
|
-
if (!bySlug.has(c.slug)) bySlug.set(c.slug, c);
|
|
406
|
-
}
|
|
407
327
|
return [...bySlug.values()];
|
|
408
328
|
}
|
|
409
329
|
|
|
@@ -444,9 +364,11 @@ export function evaluateCandidates(candidates, existingNodes, conflictsDir) {
|
|
|
444
364
|
* Main Dream Cycle execution.
|
|
445
365
|
*/
|
|
446
366
|
export async function runDreamCycle({
|
|
447
|
-
vaultDir, skillsDir, derivedDir, conflictsDir, instructionsFile
|
|
367
|
+
vaultDir, skillsDir, derivedDir, conflictsDir, instructionsFile, runtimeConfig
|
|
448
368
|
}) {
|
|
449
|
-
const
|
|
369
|
+
const cycleBrainDir = path.dirname(vaultDir);
|
|
370
|
+
const sessionsDir = path.join(cycleBrainDir, 'sessions');
|
|
371
|
+
const errors = [];
|
|
450
372
|
|
|
451
373
|
logger.info('dream', 'PHASE 0 — Session Ingestion (IDE Conversation Logs)');
|
|
452
374
|
try {
|
|
@@ -462,7 +384,7 @@ export async function runDreamCycle({
|
|
|
462
384
|
logger.info('dream', `Modified vault files: ${modified.length}`);
|
|
463
385
|
|
|
464
386
|
const candidates = collectRemCandidates({
|
|
465
|
-
brainDirPath:
|
|
387
|
+
brainDirPath: cycleBrainDir,
|
|
466
388
|
sessionsDir,
|
|
467
389
|
});
|
|
468
390
|
logger.info('dream', `REM candidates collected: ${candidates.length}`);
|
|
@@ -508,6 +430,7 @@ export async function runDreamCycle({
|
|
|
508
430
|
await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile });
|
|
509
431
|
logger.info('dream', 'Surface recompiled successfully');
|
|
510
432
|
} catch (err) {
|
|
433
|
+
errors.push(`Deep Sleep compile failed: ${err.message}`);
|
|
511
434
|
logger.error('dream', `Deep Sleep compile failed: ${err.message}`);
|
|
512
435
|
}
|
|
513
436
|
|
|
@@ -516,14 +439,16 @@ export async function runDreamCycle({
|
|
|
516
439
|
try {
|
|
517
440
|
const cleanupProposals = await generateMemoryCleanupProposals(vaultDir);
|
|
518
441
|
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
:
|
|
442
|
+
// DECAY-01: memories the access ledger shows unused for the decay age are judged by the
|
|
443
|
+
// local reasoning runtime; the gate and applier below verify each one before it applies.
|
|
444
|
+
let decayProposals = [];
|
|
445
|
+
try {
|
|
446
|
+
decayProposals = await runSmartDecay(vaultDir, runtimeConfig, undefined, { derivedDir });
|
|
447
|
+
logger.info('dream', `Smart decay: ${decayProposals.length} proposal(s).`);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
errors.push(`Smart decay failed: ${err.message}`);
|
|
450
|
+
logger.error('dream', `Smart decay failed: ${err.message}`);
|
|
451
|
+
}
|
|
527
452
|
|
|
528
453
|
// The generators are pure functions of vault state, so every cycle produces
|
|
529
454
|
// the SAME proposals as the last one — a node stale on Monday is still stale
|
|
@@ -531,7 +456,7 @@ export async function runDreamCycle({
|
|
|
531
456
|
// random slugs; the global vault accumulated 16,401 proposals for 594 real
|
|
532
457
|
// targets (~28 copies each, ~5k/day) until proposals were 95% of the vault.
|
|
533
458
|
const { kept: allProposals, suppressed } = dedupeProposals(
|
|
534
|
-
[...cleanupProposals, ...
|
|
459
|
+
[...cleanupProposals, ...decayProposals],
|
|
535
460
|
vaultDir,
|
|
536
461
|
);
|
|
537
462
|
if (suppressed > 0) {
|
|
@@ -560,6 +485,7 @@ export async function runDreamCycle({
|
|
|
560
485
|
logger.info('dream', `Applied ${applied.applied} proposals (${applied.superseded} superseded, ${applied.failed} failed).`);
|
|
561
486
|
}
|
|
562
487
|
} catch (err) {
|
|
488
|
+
errors.push(`Optimizer failed: ${err.message}`);
|
|
563
489
|
logger.error('dream', `Optimizer failed: ${err.message}`);
|
|
564
490
|
}
|
|
565
491
|
|
|
@@ -573,18 +499,20 @@ export async function runDreamCycle({
|
|
|
573
499
|
`Proposals accepted: ${proposalCount}`,
|
|
574
500
|
]);
|
|
575
501
|
} catch (err) {
|
|
502
|
+
errors.push(`Daily note write failed: ${err.message}`);
|
|
576
503
|
logger.error('dream', `Daily note write failed: ${err.message}`);
|
|
577
504
|
}
|
|
578
505
|
|
|
579
506
|
logger.info('dream', 'PHASE 5 — Automatic Storage & Memory Pruning');
|
|
580
507
|
try {
|
|
581
|
-
autoPruneStorage(
|
|
508
|
+
autoPruneStorage(cycleBrainDir, vaultDir, conflictsDir);
|
|
582
509
|
logger.info('dream', 'Automated VFS storage pruning completed successfully');
|
|
583
510
|
} catch (err) {
|
|
511
|
+
errors.push(`Storage pruning failed: ${err.message}`);
|
|
584
512
|
logger.error('dream', `Storage pruning failed: ${err.message}`);
|
|
585
513
|
}
|
|
586
514
|
|
|
587
|
-
return { status: 'success' };
|
|
515
|
+
return errors.length ? { status: 'failed', errors } : { status: 'success' };
|
|
588
516
|
}
|
|
589
517
|
|
|
590
518
|
// ─── Standalone Daemon Execution ────────────────────────────────────────────────
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Restore a repository's locked dependencies before edit or test work. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_INSTALL_TIMEOUT_MS = 300_000;
|
|
9
|
+
const MARKER = '.total-recall-install.json';
|
|
10
|
+
const LOCKFILES = [
|
|
11
|
+
['npm', 'npm-shrinkwrap.json'], ['npm', 'package-lock.json'],
|
|
12
|
+
['pnpm', 'pnpm-lock.yaml'], ['yarn', 'yarn.lock'],
|
|
13
|
+
['bun', 'bun.lock'], ['bun', 'bun.lockb'],
|
|
14
|
+
];
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
// The package this module runs from. `npm ci` deletes node_modules before installing, so running
|
|
17
|
+
// it here breaks every total-recall command, the preflight gate included, for the whole install
|
|
18
|
+
// (2026-11-10: the release's checks and every agent tool call failed until a manual install).
|
|
19
|
+
const RUNNING_PACKAGE_ROOT = (() => {
|
|
20
|
+
try { return fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')); }
|
|
21
|
+
catch { return null; }
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* npm records the tree it installed in node_modules/.package-lock.json. The tree matches the
|
|
26
|
+
* lockfile when every installed package has the locked version and every required locked package
|
|
27
|
+
* is installed (optional packages may be absent on this platform; workspace links are not copies).
|
|
28
|
+
*/
|
|
29
|
+
export function npmTreeMatchesLock(root) {
|
|
30
|
+
let lock, installed;
|
|
31
|
+
try {
|
|
32
|
+
lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8')).packages;
|
|
33
|
+
installed = JSON.parse(fs.readFileSync(path.join(root, 'node_modules', '.package-lock.json'), 'utf8')).packages;
|
|
34
|
+
} catch { return false; }
|
|
35
|
+
if (!lock || !installed) return false;
|
|
36
|
+
for (const [key, entry] of Object.entries(installed)) {
|
|
37
|
+
if (!key || entry.link) continue;
|
|
38
|
+
if (!lock[key] || lock[key].version !== entry.version) return false;
|
|
39
|
+
}
|
|
40
|
+
for (const [key, entry] of Object.entries(lock)) {
|
|
41
|
+
if (!key.startsWith('node_modules/') && !key.includes('/node_modules/')) continue;
|
|
42
|
+
if (entry.optional || entry.link || entry.peer) continue;
|
|
43
|
+
if (!installed[key]) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
49
|
+
|
|
50
|
+
/** No shell, bounded output, and a deadline that also stops lifecycle-script children. */
|
|
51
|
+
function runInstall(command, args, { cwd, timeoutMs }) {
|
|
52
|
+
return new Promise(resolve => {
|
|
53
|
+
const child = spawn(command, args, {
|
|
54
|
+
cwd, env: { ...process.env, CI: 'true' }, detached: process.platform !== 'win32',
|
|
55
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
56
|
+
});
|
|
57
|
+
let output = '', timedOut = false, error = null;
|
|
58
|
+
const collect = chunk => { output += chunk.toString().slice(0, Math.max(0, 65536 - output.length)); };
|
|
59
|
+
child.stdout.on('data', collect);
|
|
60
|
+
child.stderr.on('data', collect);
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
timedOut = true;
|
|
63
|
+
try {
|
|
64
|
+
if (process.platform === 'win32') child.kill('SIGKILL');
|
|
65
|
+
else process.kill(-child.pid, 'SIGKILL');
|
|
66
|
+
} catch { /* the install already exited */ }
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
child.on('error', err => { error = err.message; });
|
|
69
|
+
child.on('close', (exitCode, signal) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
resolve({ exitCode, signal, timedOut, output, error });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function ensure(root, { reason = 'work', timeoutMs = DEFAULT_INSTALL_TIMEOUT_MS, run = runInstall, packageRoot = RUNNING_PACKAGE_ROOT } = {}) {
|
|
77
|
+
const started = Date.now();
|
|
78
|
+
const base = { root, reason, manager: null, lockfile: null, command: null };
|
|
79
|
+
const result = (status, fields = {}) => ({ ...base, ok: status !== 'failed', status, durationMs: Date.now() - started, ...fields });
|
|
80
|
+
try {
|
|
81
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 600_000) throw new Error('Install timeout must be 1..600000 ms');
|
|
82
|
+
const locks = LOCKFILES.filter(([, file]) => fs.existsSync(path.join(root, file)));
|
|
83
|
+
if (!locks.length) return result('skipped', { trigger: 'no-lockfile' });
|
|
84
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
85
|
+
const declared = manifest.packageManager?.split('@')[0];
|
|
86
|
+
const managers = new Set(locks.map(([manager]) => manager));
|
|
87
|
+
const manager = declared || (managers.size === 1 ? locks[0][0] : null);
|
|
88
|
+
if (!manager || !managers.has(manager)) throw new Error('Lockfiles do not identify one package manager; set package.json packageManager to a manager with a lockfile');
|
|
89
|
+
const lockfile = locks.find(([name]) => name === manager)[1];
|
|
90
|
+
const lockPath = path.join(root, lockfile);
|
|
91
|
+
const lockHash = hash(lockPath);
|
|
92
|
+
const lockMtimeMs = fs.statSync(lockPath).mtimeMs;
|
|
93
|
+
const modernYarn = manager === 'yarn' && (/^yarn@(?:[2-9]|\d{2,})\./.test(manifest.packageManager || '') || /^__metadata:/m.test(fs.readFileSync(lockPath, 'utf8')));
|
|
94
|
+
const args = manager === 'npm' ? ['ci'] : ['install', modernYarn ? '--immutable' : '--frozen-lockfile'];
|
|
95
|
+
Object.assign(base, { manager, lockfile, command: [manager, ...args] });
|
|
96
|
+
const installDir = () => path.join(root, modernYarn && fs.existsSync(path.join(root, '.pnp.cjs')) ? '.yarn' : 'node_modules');
|
|
97
|
+
const markerPath = () => path.join(installDir(), MARKER);
|
|
98
|
+
let marker;
|
|
99
|
+
try { marker = JSON.parse(fs.readFileSync(markerPath(), 'utf8')); } catch { /* no trusted install */ }
|
|
100
|
+
const current = marker?.version === 1 && marker.manager === manager && marker.lockfile === lockfile
|
|
101
|
+
&& marker.lockHash === lockHash && marker.lockMtimeMs === lockMtimeMs;
|
|
102
|
+
if (current) return result('current');
|
|
103
|
+
const writeMarker = () => {
|
|
104
|
+
fs.mkdirSync(installDir(), { recursive: true });
|
|
105
|
+
fs.writeFileSync(markerPath(), `${JSON.stringify({ version: 1, manager, lockfile, lockHash, lockMtimeMs, installedAt: new Date().toISOString() })}\n`);
|
|
106
|
+
};
|
|
107
|
+
// Installed by hand or by an earlier npm run: npm's own record says the tree is current.
|
|
108
|
+
if (manager === 'npm' && lockfile === 'package-lock.json' && npmTreeMatchesLock(root)) {
|
|
109
|
+
writeMarker();
|
|
110
|
+
return result('current', { trigger: 'npm-tree-record' });
|
|
111
|
+
}
|
|
112
|
+
// Never reinstall the package this process runs from: the install would delete the code
|
|
113
|
+
// that is running, and every other total-recall process with it.
|
|
114
|
+
if (packageRoot && root === packageRoot) {
|
|
115
|
+
return result('failed', { trigger: 'running-package', error: `${root} is the Total Recall package this command runs from and its dependencies do not match ${lockfile}. Run '${manager} install' there yourself; it is never reinstalled automatically.` });
|
|
116
|
+
}
|
|
117
|
+
const trigger = fs.existsSync(installDir()) ? 'stale' : 'missing';
|
|
118
|
+
// A failed reinstall must not leave an old marker that can certify a partial install.
|
|
119
|
+
fs.rmSync(markerPath(), { force: true });
|
|
120
|
+
const installed = await run(manager, args, { cwd: root, timeoutMs });
|
|
121
|
+
if (installed.error || installed.timedOut || installed.exitCode !== 0) {
|
|
122
|
+
return result('failed', { ...installed, trigger, error: installed.error || (installed.timedOut ? `Install timed out after ${timeoutMs} ms` : `Install exited ${installed.exitCode ?? installed.signal ?? 'without a status'}`) });
|
|
123
|
+
}
|
|
124
|
+
if (hash(lockPath) !== lockHash || fs.statSync(lockPath).mtimeMs !== lockMtimeMs) throw new Error('Lockfile changed during install; retry with the current lockfile');
|
|
125
|
+
fs.mkdirSync(installDir(), { recursive: true });
|
|
126
|
+
const stamp = { version: 1, manager, lockfile, lockHash, lockMtimeMs, installedAt: new Date().toISOString() };
|
|
127
|
+
fs.writeFileSync(markerPath(), `${JSON.stringify(stamp)}\n`);
|
|
128
|
+
return result('installed', { trigger });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return result('failed', { error: err.message });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @returns {Promise<{ok:boolean, status:string, root:string, reason:string, manager:string|null, lockfile:string|null, command:string[]|null, durationMs:number}>}
|
|
136
|
+
* `run` is a fixture seam. Production callers use the bounded installer above.
|
|
137
|
+
* Concurrent callers in this host process share one install for the same real root.
|
|
138
|
+
*/
|
|
139
|
+
export async function ensureDependencies(root, options = {}) {
|
|
140
|
+
let resolved;
|
|
141
|
+
try { resolved = fs.realpathSync(root); }
|
|
142
|
+
catch (err) { return { ok: false, status: 'failed', root, reason: options.reason || 'work', manager: null, lockfile: null, command: null, durationMs: 0, error: err.message }; }
|
|
143
|
+
if (pending.has(resolved)) return pending.get(resolved);
|
|
144
|
+
const job = ensure(resolved, options);
|
|
145
|
+
pending.set(resolved, job);
|
|
146
|
+
try { return await job; }
|
|
147
|
+
finally { pending.delete(resolved); }
|
|
148
|
+
}
|
package/src/core/fast-recall.mjs
CHANGED
|
@@ -73,7 +73,7 @@ function readIndex(file) {
|
|
|
73
73
|
|
|
74
74
|
/** Local retrieval never imports the whole-vault cache or a provider. */
|
|
75
75
|
export function fastSearch(query, { derivedDir, vaultDir, top_k = 5, category = null,
|
|
76
|
-
tags = null, modality = null, importance = null, priority = null, fullText = false } = {}) {
|
|
76
|
+
tags = null, modality = null, importance = null, priority = null, fullText = false, rankResults = null } = {}) {
|
|
77
77
|
const started = performance.now();
|
|
78
78
|
const stats = { mode: fullText ? 'local-full-text' : 'local-metadata', index: 'missing',
|
|
79
79
|
freshness: 'index-snapshot; selected documents verified against canonical Markdown', hydrated_documents: 0, stale_documents: 0 };
|
|
@@ -114,7 +114,7 @@ export function fastSearch(query, { derivedDir, vaultDir, top_k = 5, category =
|
|
|
114
114
|
candidates.sort((a, b) => b.score - a.score);
|
|
115
115
|
stats.match_ms = performance.now() - started - stats.index_ms;
|
|
116
116
|
const hydration = performance.now();
|
|
117
|
-
for (const candidate of candidates) {
|
|
117
|
+
for (const candidate of rankResults ? rankResults(candidates) : candidates) {
|
|
118
118
|
if (results.length >= Math.max(1, Math.min(20, top_k))) break;
|
|
119
119
|
if (!vaultDir) { results.push(candidate); continue; }
|
|
120
120
|
const relative = candidate.relative || (isSafeVaultName(candidate.category) && isSafeVaultName(candidate.slug) ? `${candidate.category}/${candidate.slug}.md` : null);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Paired local recall evaluation. The caller fixes the corpus, labels and query order. */
|
|
2
|
+
export async function evaluateField(cases, { search, topK = 5 } = {}) {
|
|
3
|
+
if (!Number.isInteger(topK) || topK < 1 || topK > 20) throw new Error('topK must be an integer from 1 to 20');
|
|
4
|
+
if (!Array.isArray(cases) || !cases.length || cases.some(entry =>
|
|
5
|
+
typeof entry?.query !== 'string' || !entry.query.trim() || !Array.isArray(entry.relevant) ||
|
|
6
|
+
!entry.relevant.length || entry.relevant.some(slug => typeof slug !== 'string' || !slug.trim()))) {
|
|
7
|
+
throw new Error('Queries must be a nonempty array of { query, relevant: [slug, ...] }');
|
|
8
|
+
}
|
|
9
|
+
const rows = [];
|
|
10
|
+
for (const entry of cases) {
|
|
11
|
+
const baseline = (await search(entry.query, false)).slice(0, topK).map(result => result.slug);
|
|
12
|
+
const field = (await search(entry.query, true)).slice(0, topK).map(result => result.slug);
|
|
13
|
+
const shared = new Set(baseline.filter(slug => field.includes(slug))).size;
|
|
14
|
+
rows.push({ query: entry.query, relevant: entry.relevant, baseline, field,
|
|
15
|
+
overlap: shared / topK,
|
|
16
|
+
baseline_hit: baseline.some(slug => entry.relevant.includes(slug)),
|
|
17
|
+
field_hit: field.some(slug => entry.relevant.includes(slug)) });
|
|
18
|
+
}
|
|
19
|
+
const average = key => rows.reduce((sum, row) => sum + Number(row[key]), 0) / rows.length;
|
|
20
|
+
return { mode: 'local', top_k: topK, query_count: rows.length,
|
|
21
|
+
top_k_overlap: average('overlap'), baseline_hit_rate: average('baseline_hit'),
|
|
22
|
+
field_hit_rate: average('field_hit'), default_enabled: false, queries: rows };
|
|
23
|
+
}
|
package/src/core/gate-mode.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The preflight receipt is not one of these rules and has no mode.
|
|
5
5
|
*
|
|
6
6
|
* Settings live in `config/gate.json` in a brain:
|
|
7
|
-
* { "default": "warn", "router": "enforce", "plugins": { "git
|
|
7
|
+
* { "default": "warn", "router": "enforce", "plugins": { "git": "enforce" } }
|
|
8
8
|
* The repository's brain wins over the machine's. This runs on every tool call, so it reads the
|
|
9
9
|
* files directly, and an unreadable file or unknown value counts as unset: a bad settings file
|
|
10
10
|
* must never be what blocks work.
|
|
@@ -15,7 +15,7 @@ export async function loadGateAdapters(projectRoot = process.cwd()) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** Never throws: an install must finish, and a failure must be reported. */
|
|
18
|
-
export async function installHarnessHooks({ projectRoot, dryRun = false, log = (line) => console.error(line) } = {}) {
|
|
18
|
+
export async function installHarnessHooks({ projectRoot, home, dryRun = false, log = (line) => console.error(line) } = {}) {
|
|
19
19
|
try {
|
|
20
20
|
const { adapters, failures } = await loadGateAdapters(projectRoot);
|
|
21
21
|
if (!adapters?.installAll) {
|
|
@@ -23,12 +23,18 @@ export async function installHarnessHooks({ projectRoot, dryRun = false, log = (
|
|
|
23
23
|
log(` ✖ Harness hooks not installed: ${cause}`);
|
|
24
24
|
return { ok: false, rows: [], error: cause };
|
|
25
25
|
}
|
|
26
|
-
const result = await adapters.installAll({ dryRun });
|
|
26
|
+
const result = await adapters.installAll({ dryRun, ...(home ? { home } : {}) });
|
|
27
|
+
const { installLessonHooks } = await import('./lesson-hooks.mjs');
|
|
28
|
+
for (const row of result.rows) {
|
|
29
|
+
try { row.lessons = installLessonHooks(row, { dryRun }); }
|
|
30
|
+
catch (error) { row.lessons = { status: 'failed', error: error.message }; result.ok = false; }
|
|
31
|
+
}
|
|
27
32
|
const active = result.rows.filter((row) => row.status !== 'not-present');
|
|
28
33
|
log(` ${result.ok ? '✔' : '✖'} Harness hooks: ${active.map((row) => `${row.harness} ${row.status}`).join(', ') || 'no harness on this machine'}`);
|
|
29
34
|
for (const row of active) {
|
|
30
35
|
const note = row.note || row.reason;
|
|
31
36
|
if (note) log(` ${row.harness}: ${note}`);
|
|
37
|
+
if (row.lessons?.error || row.lessons?.note) log(` ${row.harness} lessons: ${row.lessons.error || row.lessons.note}`);
|
|
32
38
|
}
|
|
33
39
|
return result;
|
|
34
40
|
} catch (err) {
|
|
@@ -50,6 +56,12 @@ export async function removeHarnessHooks({ projectRoot, dryRun = false, log = (l
|
|
|
50
56
|
log(` ✖ Harness hooks not removed: ${cause}`);
|
|
51
57
|
return { ok: false, rows: [], error: cause };
|
|
52
58
|
}
|
|
59
|
+
const { installLessonHooks } = await import('./lesson-hooks.mjs');
|
|
60
|
+
// Find each owner's settings before removal can remove the path.
|
|
61
|
+
for (const adapter of adapters.list()) {
|
|
62
|
+
const existing = await adapter.install({ dryRun: true, remove: true });
|
|
63
|
+
if (existing.settings) installLessonHooks({ harness: adapter.id, settings: existing.settings, status: 'installed' }, { dryRun, remove: true });
|
|
64
|
+
}
|
|
53
65
|
const result = await adapters.removeAll({ dryRun });
|
|
54
66
|
const removed = result.rows.filter((row) => row.status === 'removed').map((row) => row.harness);
|
|
55
67
|
log(` ${result.ok ? '✔' : '✖'} Harness hooks: ${removed.length ? `removed from ${removed.join(', ')}` : 'none present'}`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseDocument } from 'yaml';
|
|
4
|
+
import { atomicWrite } from './vault.mjs';
|
|
5
|
+
|
|
6
|
+
const managed = hook => String(hook?.command || '').includes('total-recall lessons ');
|
|
7
|
+
const command = (harness, end = false) => `total-recall lessons ${end ? 'session-end' : 'tick'} --harness ${harness} --global`;
|
|
8
|
+
|
|
9
|
+
/** Extend the existing installer's selected settings file. Preserve unrelated hooks. */
|
|
10
|
+
export function installLessonHooks(row, { dryRun = false, remove = false } = {}) {
|
|
11
|
+
const { harness, settings } = row;
|
|
12
|
+
if (!settings || ['not-present', 'failed', 'unsupported'].includes(row.status)) return { status: 'session-ingest' };
|
|
13
|
+
if (harness === 'hermes') {
|
|
14
|
+
const text = fs.existsSync(settings) ? fs.readFileSync(settings, 'utf8') : '';
|
|
15
|
+
const doc = parseDocument(text || '{}');
|
|
16
|
+
if (doc.errors.length) throw new Error(`Invalid Hermes hook settings: ${doc.errors[0].message}`);
|
|
17
|
+
for (const [event, end] of [['post_llm_call', false], ['on_session_finalize', true]]) {
|
|
18
|
+
const old = doc.getIn(['hooks', event])?.toJSON() || [];
|
|
19
|
+
if (!Array.isArray(old)) throw new Error(`Hermes ${event} hooks must be a list`);
|
|
20
|
+
const hooks = old.filter(hook => !managed(hook));
|
|
21
|
+
if (!remove) hooks.push({ command: command(harness, end), timeout: 10 });
|
|
22
|
+
doc.setIn(['hooks', event], hooks);
|
|
23
|
+
}
|
|
24
|
+
if (!dryRun) atomicWrite(settings, String(doc));
|
|
25
|
+
return { status: remove ? 'removed' : dryRun ? 'would-install' : 'installed', settings };
|
|
26
|
+
}
|
|
27
|
+
if (harness === 'pi') {
|
|
28
|
+
const file = path.join(path.dirname(settings), 'total-recall-lessons.ts');
|
|
29
|
+
if (!dryRun) {
|
|
30
|
+
if (remove) fs.rmSync(file, { force: true });
|
|
31
|
+
else atomicWrite(file, PI_LESSONS_EXTENSION);
|
|
32
|
+
}
|
|
33
|
+
return { status: remove ? 'removed' : dryRun ? 'would-install' : 'installed', settings: file };
|
|
34
|
+
}
|
|
35
|
+
const events = harness === 'cursor' ? [['stop', false], ['sessionEnd', true]]
|
|
36
|
+
: ['claude', 'codex', 'dsh', 'agy'].includes(harness) ? [['Stop', false], ['SessionEnd', true]] : null;
|
|
37
|
+
if (!events || !settings.endsWith('.json')) return { status: 'session-ingest', note: 'Native lesson hook requires the harness owner; use session-end ingest.' };
|
|
38
|
+
const text = fs.existsSync(settings) ? fs.readFileSync(settings, 'utf8') : '{}';
|
|
39
|
+
const config = JSON.parse(text);
|
|
40
|
+
const hooks = harness === 'agy' ? (config['total-recall-lessons'] ||= {}) : (config.hooks ||= {});
|
|
41
|
+
for (const [event, end] of events) {
|
|
42
|
+
const old = hooks[event] || [];
|
|
43
|
+
if (!Array.isArray(old)) throw new Error(`${harness} ${event} hooks must be a list`);
|
|
44
|
+
// Remove only our command even when a group contains another owner's hook.
|
|
45
|
+
hooks[event] = old.flatMap(group => {
|
|
46
|
+
if (Array.isArray(group.hooks)) {
|
|
47
|
+
const kept = group.hooks.filter(hook => !managed(hook));
|
|
48
|
+
return kept.length ? [{ ...group, hooks: kept }] : [];
|
|
49
|
+
}
|
|
50
|
+
return managed(group) ? [] : [group];
|
|
51
|
+
});
|
|
52
|
+
if (!remove) hooks[event].push(harness === 'cursor' ? { command: command(harness, end), timeout: 10 }
|
|
53
|
+
: { hooks: [{ type: 'command', command: command(harness, end), timeout: 10 }] });
|
|
54
|
+
}
|
|
55
|
+
if (!dryRun) atomicWrite(settings, JSON.stringify(config, null, 2) + '\n');
|
|
56
|
+
return { status: remove ? 'removed' : dryRun ? 'would-install' : 'installed', settings };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const PI_LESSONS_EXTENSION = `import { spawn } from 'node:child_process';
|
|
60
|
+
export default function (pi) {
|
|
61
|
+
const review = (event, ctx) => new Promise((resolve) => {
|
|
62
|
+
const entries = ctx.sessionManager.getBranch().filter(e => e.type === 'message').map(e => e.message);
|
|
63
|
+
const child = spawn('total-recall', ['lessons', event, '--harness', 'pi', '--global'], { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
64
|
+
child.on('error', error => { console.error('Total Recall lessons:', error.message); resolve(); });
|
|
65
|
+
child.stderr.on('data', data => console.error(String(data)));
|
|
66
|
+
child.stdin.on('error', () => {});
|
|
67
|
+
child.stdin.end(JSON.stringify({ session_id: ctx.sessionManager.getSessionId(), entries }));
|
|
68
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), 10000);
|
|
69
|
+
child.on('close', () => { clearTimeout(timer); resolve(); });
|
|
70
|
+
});
|
|
71
|
+
pi.on('agent_end', (_event, ctx) => review('tick', ctx));
|
|
72
|
+
pi.on('session_shutdown', (_event, ctx) => review('session-end', ctx));
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { loadNodes } from './vault.mjs';
|
|
6
|
+
|
|
7
|
+
const cli = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../bin/total-recall.mjs');
|
|
8
|
+
|
|
9
|
+
/** Use remember's length, duplicate and validated-write guards, with a pinned brain. */
|
|
10
|
+
export function rememberLessonRule(content, { brainDir, title, sourceRef, hold = false, execute = execFile } = {}) {
|
|
11
|
+
if (typeof content !== 'string' || !content.trim()) throw new Error('Lesson rule has no content');
|
|
12
|
+
const agentDir = path.resolve(brainDir, '../..');
|
|
13
|
+
if (path.resolve(agentDir, 'skills', 'total-recall') !== path.resolve(brainDir)) throw new Error('Lesson remember requires a standard brain path');
|
|
14
|
+
const slug = `lesson-rule-${crypto.createHash('sha256').update(content).digest('hex').slice(0, 16)}`;
|
|
15
|
+
const existing = loadNodes(path.join(brainDir, 'memory-vault')).find(node => node.slug === slug);
|
|
16
|
+
if (existing && existing.body?.trim() === content.trim()) return Promise.resolve({ slug, existing: true });
|
|
17
|
+
const args = [cli, 'remember', 'invariant', content, '--global', '--slug', slug,
|
|
18
|
+
'--status', hold ? 'draft' : 'active', '--tags', 'lesson-review',
|
|
19
|
+
...(title ? ['--title', title] : [])];
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
execute(process.execPath, args, { env: { ...process.env, AGENT_DIR: agentDir, TR_SESSION_ID: sourceRef }, timeout: 120_000, maxBuffer: 1024 * 1024 },
|
|
22
|
+
(error, stdout, stderr) => error ? reject(new Error(`Lesson remember failed: ${stderr || error.message}`)) : resolve({ slug, stdout }));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const text = value => typeof value === 'string' ? value : Array.isArray(value)
|
|
4
|
+
? value.map(block => block.text || (block.type === 'tool_result' ? text(block.content) : JSON.stringify(block))).join('\n')
|
|
5
|
+
: value == null ? '' : JSON.stringify(value);
|
|
6
|
+
|
|
7
|
+
/** Normalize native hook transcripts and the session-ingest format. Keep tool evidence. */
|
|
8
|
+
export function readLessonEntries(file) {
|
|
9
|
+
const records = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).flatMap(line => {
|
|
10
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
11
|
+
});
|
|
12
|
+
// Codex records messages both as event notifications and as response items.
|
|
13
|
+
const hasResponseMessages = records.some(row => row.type === 'response_item' && row.payload?.type === 'message');
|
|
14
|
+
return records.flatMap(row => {
|
|
15
|
+
let message = row.message || row;
|
|
16
|
+
if (row.type === 'response_item') {
|
|
17
|
+
message = row.payload || {};
|
|
18
|
+
if (message.type === 'function_call' || message.type === 'function_call_output') {
|
|
19
|
+
return [{ role: 'tool', content: text(message), ts: row.timestamp }];
|
|
20
|
+
}
|
|
21
|
+
if (message.type !== 'message') return [];
|
|
22
|
+
} else if (row.type === 'event_msg') {
|
|
23
|
+
if (hasResponseMessages) return [];
|
|
24
|
+
const payload = row.payload || {};
|
|
25
|
+
if (!['user_message', 'agent_message'].includes(payload.type)) return [];
|
|
26
|
+
message = { role: payload.type === 'user_message' ? 'user' : 'assistant', content: payload.message };
|
|
27
|
+
}
|
|
28
|
+
let role = message.role || row.role;
|
|
29
|
+
if (role === 'human') role = 'user';
|
|
30
|
+
if (!['user', 'assistant', 'tool'].includes(role)) return [];
|
|
31
|
+
const content = [text(message.content ?? message.text), ...(message.tool_calls ? [JSON.stringify(message.tool_calls)] : [])].filter(Boolean).join('\n');
|
|
32
|
+
if (!content.trim()) return [];
|
|
33
|
+
// Claude tool results have role user, but do not start a user turn.
|
|
34
|
+
if (Array.isArray(message.content) && message.content.every(block => block.type === 'tool_result')) role = 'tool';
|
|
35
|
+
return [{ role, content, ts: row.ts || row.timestamp, ...(row.cwd ? { cwd: row.cwd } : {}) }];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function countLessonTurns(entries) {
|
|
40
|
+
return entries.filter(entry => entry.role === 'user').length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function selectLessonTurns(entries, { fromTurn = 1, toTurn = Infinity } = {}) {
|
|
44
|
+
if (!Number.isInteger(fromTurn) || fromTurn < 1 || !(toTurn === Infinity || Number.isInteger(toTurn)) || toTurn < fromTurn) {
|
|
45
|
+
throw new Error('Lesson turn range must be positive and ordered');
|
|
46
|
+
}
|
|
47
|
+
let turn = 0;
|
|
48
|
+
return entries.filter(entry => {
|
|
49
|
+
if (entry.role === 'user') turn++;
|
|
50
|
+
return turn >= fromTurn && turn <= toTurn;
|
|
51
|
+
});
|
|
52
|
+
}
|