total-recall-brain 3.43.3 → 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 +16 -9
- 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 +3 -3
- package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
- package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
- package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
- package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
- 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 +17 -16
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
- 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 +160 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
- 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 +101 -16
- 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 +54 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
- 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 +64 -15
- 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 +249 -0
- 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 +6 -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/pool.mjs +16 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
- 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 +28 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
- 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 +110 -0
- 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 +597 -70
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/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/connect.mjs +2 -29
- package/src/cli/context.mjs +1 -0
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/ingest-okf.mjs +4 -4
- 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/release.mjs +4 -1
- 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 +10 -3
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/automatic-brain-updates.mjs +11 -6
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +42 -12
- 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-compactor.mjs +7 -2
- package/src/core/context-compiler.mjs +26 -1
- package/src/core/context-policy.mjs +7 -3
- package/src/core/daemon-control.mjs +22 -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/global-brain-updates.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/mesh-brain-updates.mjs +2 -1
- 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-restart.mjs +128 -7
- 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 +33 -6
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +68 -57
- package/src/core/task-context.mjs +9 -8
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +5 -0
- package/src/core/vault-files.mjs +8 -2
- package/src/core/vault.mjs +1 -1
- package/src/core/vector-field.mjs +134 -192
- package/src/server/index.mjs +3 -1
- 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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { atomicWrite } from './vault.mjs';
|
|
5
|
+
import { addTask, getTask, resolveQueueDir } from './task-envelope.mjs';
|
|
6
|
+
import { readLessonEntries, countLessonTurns, selectLessonTurns } from './lesson-sessions.mjs';
|
|
7
|
+
import { acquireBrainUpdateLock } from './brain-updates.mjs';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_LESSON_INTERVAL = 10;
|
|
10
|
+
const hash = value => crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
11
|
+
|
|
12
|
+
/** A local hook only queues work. It never calls a model or elects a leader. */
|
|
13
|
+
export function tickLessons({ brainDir, sessionId, transcriptPath, event = 'Stop', interval,
|
|
14
|
+
turnId, turnCount, harness = 'unknown', hold } = {}) {
|
|
15
|
+
if (!brainDir || !sessionId) throw new Error('Lesson review requires a brain and session id');
|
|
16
|
+
const sessionsDir = path.join(brainDir, 'sessions');
|
|
17
|
+
const configPath = path.join(brainDir, 'config', 'lessons.json');
|
|
18
|
+
if (fs.existsSync(configPath)) {
|
|
19
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
20
|
+
if (config.enabled === false) return { tasks: [], skipped: 'disabled' };
|
|
21
|
+
interval ??= config.interval;
|
|
22
|
+
if (hold === undefined) hold = config.holdWrites === true;
|
|
23
|
+
}
|
|
24
|
+
interval ??= DEFAULT_LESSON_INTERVAL;
|
|
25
|
+
if (!Number.isInteger(interval) || interval < 1) throw new Error('Lesson interval must be a positive integer');
|
|
26
|
+
hold ??= false;
|
|
27
|
+
const root = path.join(brainDir, 'lesson-reviews');
|
|
28
|
+
fs.mkdirSync(root, { recursive: true });
|
|
29
|
+
// Match writeSession's identity so native hooks and later ingest share a cursor.
|
|
30
|
+
const localTranscript = transcriptPath && path.dirname(path.resolve(transcriptPath)) === path.resolve(sessionsDir);
|
|
31
|
+
const key = transcriptPath ? (localTranscript ? path.basename(transcriptPath, '.jsonl') : hash(path.resolve(transcriptPath))) : hash(`${harness}:${sessionId}`);
|
|
32
|
+
const lock = path.join(root, `${key}.lock`);
|
|
33
|
+
const lockFd = acquireBrainUpdateLock(lock, { waitMs: 0 });
|
|
34
|
+
try {
|
|
35
|
+
const stateFile = path.join(root, `${key}.json`);
|
|
36
|
+
const state = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, 'utf8')) : { turns: 0, queuedThrough: 0 };
|
|
37
|
+
let entries;
|
|
38
|
+
const savedTranscript = path.join(sessionsDir, `${key}.jsonl`);
|
|
39
|
+
if (!transcriptPath && fs.existsSync(savedTranscript)) transcriptPath = savedTranscript;
|
|
40
|
+
if (transcriptPath) {
|
|
41
|
+
entries = readLessonEntries(transcriptPath);
|
|
42
|
+
turnCount = countLessonTurns(entries);
|
|
43
|
+
if (!localTranscript) {
|
|
44
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
45
|
+
atomicWrite(path.join(sessionsDir, `${key}.jsonl`), entries.map(entry => JSON.stringify(entry)).join('\n') + '\n');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (turnCount !== undefined) {
|
|
49
|
+
if (!Number.isInteger(turnCount) || turnCount < state.turns) throw new Error('Lesson transcript turn count went backwards');
|
|
50
|
+
state.turns = turnCount;
|
|
51
|
+
} else if (event === 'UserPromptSubmit' && (!turnId || turnId !== state.lastTurnId)) {
|
|
52
|
+
state.turns++;
|
|
53
|
+
state.lastTurnId = turnId;
|
|
54
|
+
}
|
|
55
|
+
const final = event === 'SessionEnd' || event === 'session-end';
|
|
56
|
+
const tasks = [];
|
|
57
|
+
// Prompt hooks count only. Stop has the completed response and tool results.
|
|
58
|
+
if (event !== 'UserPromptSubmit' && entries) {
|
|
59
|
+
while (state.turns - state.queuedThrough >= interval || (final && state.turns > state.queuedThrough)) {
|
|
60
|
+
const fromTurn = state.queuedThrough + 1;
|
|
61
|
+
const toTurn = Math.min(state.turns, state.queuedThrough + interval);
|
|
62
|
+
const slug = `lesson-review-${key}-${fromTurn}-${toTurn}`;
|
|
63
|
+
const queueDir = resolveQueueDir(brainDir);
|
|
64
|
+
if (!getTask(queueDir, slug) && !fs.existsSync(path.join(root, `${slug}.done`))) {
|
|
65
|
+
const snapshot = path.join(root, `${slug}.jsonl`);
|
|
66
|
+
atomicWrite(snapshot, selectLessonTurns(entries, { fromTurn, toTurn }).map(entry => JSON.stringify(entry)).join('\n') + '\n');
|
|
67
|
+
addTask({ slug, intent: `Review session ${sessionId}, turns ${fromTurn}–${toTurn}`,
|
|
68
|
+
kind: 'memory', executor: 'lesson-review', category: 'lesson-review', priority: 40,
|
|
69
|
+
origin: { agent: harness, session_id: sessionId },
|
|
70
|
+
payload: { sessionPath: snapshot, fromTurn, toTurn, hold, sessionId },
|
|
71
|
+
}, queueDir);
|
|
72
|
+
}
|
|
73
|
+
tasks.push(slug);
|
|
74
|
+
state.queuedThrough = toTurn;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
atomicWrite(stateFile, JSON.stringify(state));
|
|
78
|
+
return { turns: state.turns, queuedThrough: state.queuedThrough, tasks };
|
|
79
|
+
} finally {
|
|
80
|
+
// Remove only the inode this process acquired. A dead hook's lock is reclaimed
|
|
81
|
+
// by the shared lock owner on the next tick.
|
|
82
|
+
try { if (fs.statSync(lock).ino === fs.fstatSync(lockFd).ino) fs.unlinkSync(lock); }
|
|
83
|
+
finally { fs.closeSync(lockFd); }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Session-ingest fallback for harnesses without a lifecycle hook. Every node calls this. */
|
|
88
|
+
export function enqueueSessionReviews({ brainDir, now = Date.now(), quietMs = 5 * 60_000, interval, hold } = {}) {
|
|
89
|
+
const dir = path.join(brainDir, 'sessions');
|
|
90
|
+
if (!fs.existsSync(dir)) return [];
|
|
91
|
+
return fs.readdirSync(dir).filter(name => name.endsWith('.jsonl')).map(name => {
|
|
92
|
+
const transcriptPath = path.join(dir, name);
|
|
93
|
+
return tickLessons({ brainDir, sessionId: path.basename(name, '.jsonl'), transcriptPath, interval, hold,
|
|
94
|
+
event: now - fs.statSync(transcriptPath).mtimeMs >= quietMs ? 'SessionEnd' : 'Stop', harness: 'session-ingest' });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -30,7 +30,8 @@ export async function updateMeshBrainPackages({version,versions,listNodes,exec,n
|
|
|
30
30
|
let report,serving;try{const status=JSON.parse(observed.stdout);report=status.last;serving=status.server;}catch{}
|
|
31
31
|
const localFailed=report?(report.local_failed ?? Math.max(0,(report.failed || 0)-(report.mesh?.failed || 0))):null;
|
|
32
32
|
const current=observed.success && report && localFailed===0 && now()>=report.checkedAt && now()-report.checkedAt<2*60*60*1000 && report.versions?.['total-recall-brain']===version && (!versions?.['@gregiteen/ssss-cli'] || report.versions?.['@gregiteen/ssss-cli']===versions['@gregiteen/ssss-cli']);
|
|
33
|
-
// Maintenance
|
|
33
|
+
// Maintenance could not fix this server, or it would have reported a local failure above and
|
|
34
|
+
// been dispatched again, so this is reported. One that lags since the last run waits for that run's report to age.
|
|
34
35
|
if(current && serving?.status==='stale'){row.status='failed';row.report=report;row.server=serving;row.error=serving.reason;continue;}
|
|
35
36
|
if(current){row.status='verified';row.report=report;continue;}
|
|
36
37
|
// npm exec pins the verified release so an old installed CLI can bootstrap
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** Compatibility facade only. Adapter contracts, auth, usage and execution belong to
|
|
1
|
+
/** Compatibility facade only. Adapter contracts, auth, usage and execution belong to Metaharness. */
|
|
2
2
|
import { loadPluginModule } from './plugin-runtime.mjs';
|
|
3
3
|
|
|
4
4
|
let owner, ownerError;
|
|
5
5
|
try { owner = await loadPluginModule('meta-harness', 'harness-compat'); }
|
|
6
6
|
catch (error) { ownerError = error; }
|
|
7
7
|
function implementation() {
|
|
8
|
-
if (!owner) throw new Error(`
|
|
8
|
+
if (!owner) throw new Error(`Metaharness is unavailable: ${ownerError?.message || 'install the plugin'}`);
|
|
9
9
|
return owner;
|
|
10
10
|
}
|
|
11
11
|
|
package/src/core/optimizer.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import crypto from 'crypto';
|
|
|
4
4
|
import matter from './frontmatter.mjs';
|
|
5
5
|
import { writeNode } from './vault.mjs';
|
|
6
6
|
import { getNodes } from './vault-cache.mjs';
|
|
7
|
+
import { readAccess, lastUsedMs, activeDaysSince } from './access-ledger.mjs';
|
|
7
8
|
import { logger } from './logger.mjs';
|
|
8
|
-
import { brainDir } from './config.mjs';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Total Recall Optimizer
|
|
@@ -166,129 +166,6 @@ export async function generateMemoryCleanupProposals(vaultDir) {
|
|
|
166
166
|
return proposals;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/**
|
|
170
|
-
* Scans skill execution logs (if available) and proposes improvements.
|
|
171
|
-
*/
|
|
172
|
-
/**
|
|
173
|
-
* Scan skill files for quality issues and propose improvements.
|
|
174
|
-
* Looks at skill files that haven't been updated in >30 days and
|
|
175
|
-
* checks for common issues: missing examples, vague instructions, outdated commands.
|
|
176
|
-
*/
|
|
177
|
-
export async function generateSkillImprovementProposals(vaultDir) {
|
|
178
|
-
const agentDir = path.join(path.dirname(vaultDir));
|
|
179
|
-
const skillsDir = path.join(agentDir, 'skills');
|
|
180
|
-
const proposals = [];
|
|
181
|
-
|
|
182
|
-
if (!fs.existsSync(skillsDir)) return proposals;
|
|
183
|
-
|
|
184
|
-
const now = Date.now();
|
|
185
|
-
const staleCutoff = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
189
|
-
.filter(d => d.isDirectory())
|
|
190
|
-
.map(d => path.join(skillsDir, d.name, 'SKILL.md'))
|
|
191
|
-
.filter(p => fs.existsSync(p));
|
|
192
|
-
|
|
193
|
-
for (const skillFile of skillDirs) {
|
|
194
|
-
const stat = fs.statSync(skillFile);
|
|
195
|
-
const ageMs = now - stat.mtimeMs;
|
|
196
|
-
if (ageMs < staleCutoff) continue;
|
|
197
|
-
|
|
198
|
-
const raw = fs.readFileSync(skillFile, 'utf8');
|
|
199
|
-
const hasExamples = /##\s*example/i.test(raw);
|
|
200
|
-
const hasCaution = /caution|warning|never|must not/i.test(raw);
|
|
201
|
-
const lineCount = raw.split('\n').length;
|
|
202
|
-
|
|
203
|
-
const issues = [];
|
|
204
|
-
if (!hasExamples) issues.push('No examples section');
|
|
205
|
-
if (!hasCaution) issues.push('No caution/constraint section');
|
|
206
|
-
if (lineCount < 20) issues.push('Very short — may lack detail');
|
|
207
|
-
|
|
208
|
-
if (issues.length > 0) {
|
|
209
|
-
const skillName = path.basename(path.dirname(skillFile));
|
|
210
|
-
proposals.push(createProposal(
|
|
211
|
-
'skill-improvement',
|
|
212
|
-
`Improve skill: ${skillName} (${issues.join(', ')})`,
|
|
213
|
-
skillFile,
|
|
214
|
-
`Skill file is ${Math.floor(ageMs / 86400000)} days old. Issues: ${issues.join('; ')}`,
|
|
215
|
-
));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
} catch (err) {
|
|
219
|
-
logger.debug('optimizer: generateSkillImprovementProposals failed to read skillsDir', { err: err.message });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return proposals;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Scan the task queue for stuck or stalled workflows.
|
|
227
|
-
* Tasks that have been 'in-progress' for >1 hour are considered stalled.
|
|
228
|
-
*/
|
|
229
|
-
export async function generateWorkflowRepairProposals(vaultDir) {
|
|
230
|
-
const queueDir = path.join(brainDir, 'scheduler', 'queue');
|
|
231
|
-
const proposals = [];
|
|
232
|
-
|
|
233
|
-
if (!fs.existsSync(queueDir)) return proposals;
|
|
234
|
-
|
|
235
|
-
const now = Date.now();
|
|
236
|
-
const stalledCutoff = 60 * 60 * 1000; // 1 hour
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const files = fs.readdirSync(queueDir).filter(f => f.endsWith('.md'));
|
|
240
|
-
for (const file of files) {
|
|
241
|
-
const raw = fs.readFileSync(path.join(queueDir, file), 'utf8');
|
|
242
|
-
const { data } = matter(raw);
|
|
243
|
-
if (data.status !== 'in-progress') continue;
|
|
244
|
-
|
|
245
|
-
const updatedAt = new Date(data.updated_at || data.created || 0).getTime();
|
|
246
|
-
if (now - updatedAt < stalledCutoff) continue;
|
|
247
|
-
|
|
248
|
-
proposals.push(createProposal(
|
|
249
|
-
'workflow-repair',
|
|
250
|
-
`Stalled task: ${data.slug || file}`,
|
|
251
|
-
path.join(queueDir, file),
|
|
252
|
-
`Task has been in-progress for ${Math.floor((now - updatedAt) / 60000)} minutes. May need retry or cancellation.`,
|
|
253
|
-
));
|
|
254
|
-
}
|
|
255
|
-
} catch (err) {
|
|
256
|
-
logger.debug('optimizer: generateWorkflowRepairProposals failed to read queueDir', { err: err.message });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return proposals;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Analyze the vault's model usage patterns and propose routing improvements.
|
|
264
|
-
* Flags cases where high-priority tasks were sent to the local LLM
|
|
265
|
-
* that should have used a frontier model.
|
|
266
|
-
*/
|
|
267
|
-
export async function generateModelRoutingProposals(vaultDir) {
|
|
268
|
-
const proposals = [];
|
|
269
|
-
const nodes = getNodes(vaultDir);
|
|
270
|
-
|
|
271
|
-
// Find high-importance facts with low confidence — likely needed frontier model
|
|
272
|
-
const underconfident = nodes.filter(n =>
|
|
273
|
-
n.status === 'active' &&
|
|
274
|
-
n.category === 'facts' &&
|
|
275
|
-
n.importance >= 4 &&
|
|
276
|
-
n.confidence < 0.6 &&
|
|
277
|
-
n.source?.agent === 'fact-seeker',
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
for (const node of underconfident.slice(0, 5)) {
|
|
281
|
-
proposals.push(createProposal(
|
|
282
|
-
'model-routing',
|
|
283
|
-
`Re-research with frontier model: "${node.title}"`,
|
|
284
|
-
node.slug,
|
|
285
|
-
`High-importance fact (importance: ${node.importance}) has low confidence (${node.confidence}). Originally researched by local model. Frontier model may produce better synthesis.`,
|
|
286
|
-
));
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return proposals;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
169
|
/**
|
|
293
170
|
* Nodes that are important but have gone untouched — computed on demand.
|
|
294
171
|
*
|
|
@@ -310,21 +187,6 @@ export function findStaleNodes(vaultDir, { days = 30, minImportance = 4 } = {})
|
|
|
310
187
|
.sort((a, b) => new Date(a.last_accessed) - new Date(b.last_accessed));
|
|
311
188
|
}
|
|
312
189
|
|
|
313
|
-
/**
|
|
314
|
-
* @deprecated Wrote tickets nothing reads. Retained because the dream cycle's
|
|
315
|
-
* master switch (ENABLE_STALE_KNOWLEDGE_REFRESH, off) still references it.
|
|
316
|
-
* Stale nodes are surfaced to a human via findStaleNodes instead; nothing
|
|
317
|
-
* re-researches them automatically (RESEARCH_SYSTEM2).
|
|
318
|
-
*/
|
|
319
|
-
export async function generateStaleKnowledgeRefreshProposals(vaultDir) {
|
|
320
|
-
return findStaleNodes(vaultDir, { days: 30, minImportance: 4 }).map(node => createProposal(
|
|
321
|
-
'stale-knowledge-refresh',
|
|
322
|
-
`Refresh stale high-importance memory: ${node.title}`,
|
|
323
|
-
node.slug,
|
|
324
|
-
'Node has not been accessed in over 30 days but maintains high importance. Requires verification against current state.',
|
|
325
|
-
));
|
|
326
|
-
}
|
|
327
|
-
|
|
328
190
|
/**
|
|
329
191
|
* Local Eval Gate for Proposal Promotion
|
|
330
192
|
* Uses the local runtime to evaluate the validity of a proposal.
|
|
@@ -379,6 +241,14 @@ export async function evaluateProposalGate(proposal, runtimeConfig, vaultDir = n
|
|
|
379
241
|
return stamp('accepted');
|
|
380
242
|
}
|
|
381
243
|
|
|
244
|
+
// smart-decay is verified mechanically against live state: the memory still exists, is
|
|
245
|
+
// active and unprotected, and the access ledger shows no use since it was judged and for
|
|
246
|
+
// the full decay age. Only then may it reach `accepted` and auto-apply (DECAY-01).
|
|
247
|
+
if (proposal.category === 'smart-decay') {
|
|
248
|
+
const reason = verifySmartDecay(proposal, vaultDir);
|
|
249
|
+
return reason ? stamp('rejected', { rejection_reason: reason }) : stamp('accepted');
|
|
250
|
+
}
|
|
251
|
+
|
|
382
252
|
// stale-knowledge-refresh no longer produces tickets at all — the work is
|
|
383
253
|
// handed to the research queue, which can actually perform it. If one is
|
|
384
254
|
// somehow generated, it is not something to auto-accept.
|
|
@@ -422,41 +292,67 @@ Output ONLY valid JSON.`;
|
|
|
422
292
|
*
|
|
423
293
|
* @param {string} vaultDir
|
|
424
294
|
* @param {object} runtimeConfig
|
|
425
|
-
* @param {number} [ageThresholdDays=60]
|
|
295
|
+
* @param {number} [ageThresholdDays=60] Active days of the brain without a use before considering decay
|
|
426
296
|
* @returns {object[]} decay proposals
|
|
427
297
|
*/
|
|
428
|
-
|
|
429
|
-
|
|
298
|
+
/** Active days (days the brain was used), never calendar days: an idle brain never decays. */
|
|
299
|
+
export const DECAY_AGE_DAYS = 60;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Why a smart-decay proposal may not be applied, or null when live state supports it.
|
|
303
|
+
* Shared by the gate and the applier so both check the same facts.
|
|
304
|
+
*/
|
|
305
|
+
export function verifySmartDecay(proposal, vaultDir, { now = Date.now(), ageThresholdDays = DECAY_AGE_DAYS } = {}) {
|
|
306
|
+
if (!vaultDir) return 'No vault to verify against.';
|
|
307
|
+
const slug = String(proposal.target_path || '').trim();
|
|
308
|
+
const node = getNodes(vaultDir).find(n => n.slug === slug);
|
|
309
|
+
if (!node) return `Memory ${slug} no longer exists.`;
|
|
310
|
+
if (node.status !== 'active') return `Memory ${slug} is already ${node.status}.`;
|
|
311
|
+
if (isDecayProtected(node)) return `Memory ${slug} is protected from decay.`;
|
|
312
|
+
const ledger = readAccess(path.join(path.dirname(vaultDir), 'memory-derived'));
|
|
313
|
+
const used = lastUsedMs(node, ledger);
|
|
314
|
+
if (proposal.decay_last_used_at && used > new Date(proposal.decay_last_used_at).getTime()) return `Memory ${slug} was used after it was judged.`;
|
|
315
|
+
if (activeDaysSince(ledger, used) < ageThresholdDays) return `Memory ${slug} was used within the brain's last ${ageThresholdDays} active days.`;
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Memories decay never touches: rules, absolute-priority and importance-5 memories. */
|
|
320
|
+
export function isDecayProtected(node) {
|
|
321
|
+
return node.priority === 'absolute' || node.importance >= 5
|
|
322
|
+
|| ['invariants', 'preferences', 'anti-patterns'].includes(node.category);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Active, unprotected memories unused for at least `ageThresholdDays` of the brain's active days. */
|
|
326
|
+
export function decayCandidates(nodes, ledger, { now = Date.now(), ageThresholdDays = DECAY_AGE_DAYS } = {}) {
|
|
327
|
+
return nodes
|
|
328
|
+
.filter(n => n.status === 'active' && n.type !== 'proposal' && !isDecayProtected(n))
|
|
329
|
+
.filter(n => activeDaysSince(ledger, lastUsedMs(n, ledger)) >= ageThresholdDays)
|
|
330
|
+
.sort((a, b) => lastUsedMs(a, ledger) - lastUsedMs(b, ledger));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function runSmartDecay(vaultDir, runtimeConfig, ageThresholdDays = DECAY_AGE_DAYS, { derivedDir = path.join(path.dirname(vaultDir), 'memory-derived'), call } = {}) {
|
|
334
|
+
const callLocalRuntime = call || (await import('./runtime.mjs')).callLocalRuntime;
|
|
430
335
|
const nodes = getNodes(vaultDir);
|
|
431
336
|
const now = Date.now();
|
|
432
|
-
const
|
|
337
|
+
const ledger = readAccess(derivedDir);
|
|
433
338
|
const proposals = [];
|
|
434
339
|
|
|
435
|
-
//
|
|
436
|
-
const
|
|
437
|
-
if (n.status !== 'active') return false;
|
|
438
|
-
if (n.priority === 'absolute') return false;
|
|
439
|
-
if (n.importance >= 5) return false;
|
|
440
|
-
const lastAccessed = new Date(n.last_accessed || n.updated || 0).getTime();
|
|
441
|
-
return (now - lastAccessed) > ageThresholdMs;
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Process up to 5 candidates per run to avoid excessive LLM calls
|
|
445
|
-
const batch = candidates.slice(0, 5);
|
|
340
|
+
// Oldest unused first, up to 5 per run to bound model calls.
|
|
341
|
+
const batch = decayCandidates(nodes, ledger, { now, ageThresholdDays }).slice(0, 5);
|
|
446
342
|
|
|
447
343
|
for (const node of batch) {
|
|
448
|
-
const ageDays =
|
|
344
|
+
const ageDays = activeDaysSince(ledger, lastUsedMs(node, ledger));
|
|
449
345
|
|
|
450
346
|
const prompt = [
|
|
451
347
|
`## Memory Node: ${node.slug}`,
|
|
452
348
|
`Title: "${node.title}"`,
|
|
453
|
-
`Category: ${node.category} | Importance: ${node.importance} |
|
|
349
|
+
`Category: ${node.category} | Importance: ${node.importance} | Unused for ${ageDays} active days of its brain`,
|
|
454
350
|
`Tags: ${(node.tags || []).join(', ')}`,
|
|
455
351
|
'',
|
|
456
352
|
'## Body',
|
|
457
353
|
(node.body || '').slice(0, 800),
|
|
458
354
|
'',
|
|
459
|
-
`This node has not been
|
|
355
|
+
`This node has not been used on the last ${ageDays} days its brain was active. Should it be retained, archived, or decayed?`,
|
|
460
356
|
].join('\n');
|
|
461
357
|
|
|
462
358
|
try {
|
|
@@ -468,15 +364,20 @@ export async function runSmartDecay(vaultDir, runtimeConfig, ageThresholdDays =
|
|
|
468
364
|
const verdict = result.verdict || 'RETAINED';
|
|
469
365
|
|
|
470
366
|
if (verdict !== 'RETAINED') {
|
|
471
|
-
proposals.push(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
367
|
+
proposals.push({
|
|
368
|
+
...createProposal(
|
|
369
|
+
'smart-decay',
|
|
370
|
+
`Smart Decay [${verdict}]: "${node.title}"`,
|
|
371
|
+
node.slug,
|
|
372
|
+
`Unused for ${ageDays} active days. Reasoning: ${result.reasoning || 'No reasoning provided.'} Confidence adjustment: ${result.confidence_adjustment || 0}`,
|
|
373
|
+
),
|
|
374
|
+
// The gate re-checks that the memory has not been used since this judgement.
|
|
375
|
+
decay_verdict: verdict,
|
|
376
|
+
decay_last_used_at: new Date(lastUsedMs(node, ledger)).toISOString(),
|
|
377
|
+
});
|
|
477
378
|
}
|
|
478
|
-
} catch {
|
|
479
|
-
|
|
379
|
+
} catch (err) {
|
|
380
|
+
logger.warn('optimizer', `Smart decay could not evaluate ${node.slug}: ${err.message}`);
|
|
480
381
|
}
|
|
481
382
|
}
|
|
482
383
|
|
|
@@ -55,6 +55,9 @@ export function validateCardContract(manifest) {
|
|
|
55
55
|
else names.add(sub.name);
|
|
56
56
|
if (sub.effect !== undefined && !COMMAND_EFFECTS.includes(sub.effect)) errors.push(`${where}.effect must be one of: ${COMMAND_EFFECTS.join(', ')}`);
|
|
57
57
|
if (sub.output !== undefined && !OUTPUT_KINDS.includes(sub.output)) errors.push(`${where}.output must be one of: ${OUTPUT_KINDS.join(', ')}`);
|
|
58
|
+
// The runner refuses anything outside this range, so a manifest that
|
|
59
|
+
// declares it must fail validation rather than every call at run time.
|
|
60
|
+
if (sub.timeout_ms !== undefined && (!Number.isInteger(sub.timeout_ms) || sub.timeout_ms < 1 || sub.timeout_ms > 600000)) errors.push(`${where}.timeout_ms must be an integer from 1 to 600000`);
|
|
58
61
|
if (sub.args !== undefined) {
|
|
59
62
|
if (!Array.isArray(sub.args)) errors.push(`${where}.args must be an array`);
|
|
60
63
|
else {
|
|
@@ -48,7 +48,7 @@ export function resolveProjectRoot(projectRoot, vaultDir) {
|
|
|
48
48
|
return fs.existsSync(path.join(parent, '.agent')) ? parent : (projectRoot || process.cwd());
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** True when a 5-field cron expression is syntactically plausible.
|
|
51
|
+
/** True when a 5-field cron expression is syntactically plausible. The scheduling owner checks semantics. */
|
|
52
52
|
export function isCronExpression(expr) {
|
|
53
53
|
if (typeof expr !== 'string') return false;
|
|
54
54
|
const fields = expr.trim().split(/\s+/);
|
|
@@ -277,6 +277,13 @@ export function validatePluginManifest(manifest) {
|
|
|
277
277
|
if (!/^[a-z][a-z0-9-]*$/.test(name) || !isSafeRelativePath(file)) errors.push(`Invalid runtime module ${name}`);
|
|
278
278
|
}
|
|
279
279
|
if (runtime.routers !== undefined && (!Array.isArray(runtime.routers) || runtime.routers.some(file => !isSafeRelativePath(file)))) errors.push('runtime.routers must contain safe relative paths');
|
|
280
|
+
// Daemon services name declared runtime modules; the daemon starts each one.
|
|
281
|
+
if (runtime.daemon !== undefined) {
|
|
282
|
+
if (!Array.isArray(runtime.daemon)) errors.push('runtime.daemon must be an array of runtime module names');
|
|
283
|
+
else for (const name of runtime.daemon) {
|
|
284
|
+
if (typeof name !== 'string' || !Object.hasOwn(runtime.modules || {}, name)) errors.push(`runtime.daemon names undeclared module ${name}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
280
287
|
}
|
|
281
288
|
}
|
|
282
289
|
|
|
@@ -411,9 +418,11 @@ export function discoverPlugins(projectRoot = process.cwd()) {
|
|
|
411
418
|
const seen = new Set();
|
|
412
419
|
const plugins = [];
|
|
413
420
|
const global = globalPluginsDir();
|
|
414
|
-
|
|
421
|
+
// Nothing is installed per repository: every plugin lives in the shared
|
|
422
|
+
// folder, and a repository only switches it on or off. A leftover repository
|
|
423
|
+
// plugin folder is never read (`plugin copies` reports and removes it).
|
|
424
|
+
// `projectRoot` stays in the signature for callers; discovery no longer depends on it.
|
|
415
425
|
const sources = [
|
|
416
|
-
...(path.resolve(project) === path.resolve(global) ? [] : [[project, 'project']]),
|
|
417
426
|
[global, 'global'],
|
|
418
427
|
[path.join(PACKAGE_ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'plugins'), 'scaffold']
|
|
419
428
|
];
|
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
/** Generic in-process extension loading. Domain behavior stays with its owner. */
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import { pathToFileURL } from 'node:url';
|
|
3
4
|
import { discoverPlugins, resolvePluginFile, PACKAGE_ROOT } from './plugin-loader.mjs';
|
|
4
5
|
import { pluginActivation } from './plugin-state.mjs';
|
|
6
|
+
import { logger } from './logger.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Start every daemon service an active plugin declares in `runtime.daemon`.
|
|
10
|
+
* Each named runtime module exports `start({ projectRoot })` and may return a
|
|
11
|
+
* stop function. Services run against the global brain (home), never the
|
|
12
|
+
* directory the daemon happens to start in. One failing service is logged and
|
|
13
|
+
* never stops the others.
|
|
14
|
+
* @returns {Promise<{ started: string[], failed: string[], stop: Function }>}
|
|
15
|
+
*/
|
|
16
|
+
export async function startPluginDaemonServices({ projectRoot = os.homedir(), load = loadPluginModule } = {}) {
|
|
17
|
+
const stops = [];
|
|
18
|
+
const started = [];
|
|
19
|
+
const failed = [];
|
|
20
|
+
for (const plugin of discoverPlugins(projectRoot).filter(item => item.valid)) {
|
|
21
|
+
const names = plugin.manifest.runtime?.daemon || [];
|
|
22
|
+
if (!names.length || !pluginActivation(plugin, { projectRoot }).enabled) continue;
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
const label = `${plugin.id}:${name}`;
|
|
25
|
+
try {
|
|
26
|
+
const module = await load(plugin.id, name, { projectRoot, activeRequired: true });
|
|
27
|
+
if (typeof module.start !== 'function') throw new Error('daemon module must export start()');
|
|
28
|
+
const stop = await module.start({ projectRoot });
|
|
29
|
+
if (typeof stop === 'function') stops.push(stop);
|
|
30
|
+
started.push(label);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
failed.push(label);
|
|
33
|
+
logger.warn('plugin-runtime', `Daemon service ${label} failed to start: ${err.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
started,
|
|
39
|
+
failed,
|
|
40
|
+
stop: () => { for (const stop of stops) { try { stop(); } catch { /* already stopped */ } } },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
5
43
|
|
|
6
44
|
export async function loadPluginModule(id, name, { activeRequired = false, requireValid = true, projectRoot = process.cwd() } = {}) {
|
|
7
45
|
const plugin = discoverPlugins(projectRoot).find(item => item.id === id);
|
|
@@ -18,7 +56,8 @@ export async function loadPluginModule(id, name, { activeRequired = false, requi
|
|
|
18
56
|
}
|
|
19
57
|
process.env.TR_PACKAGE_ROOT = PACKAGE_ROOT;
|
|
20
58
|
const file = resolvePluginFile(plugin.dir, relative);
|
|
21
|
-
|
|
59
|
+
// A file URL is unambiguous on every platform and under every module loader.
|
|
60
|
+
return import(pathToFileURL(file).href);
|
|
22
61
|
}
|
|
23
62
|
|
|
24
63
|
export async function loadPluginRouters() {
|
|
@@ -340,6 +340,31 @@ export function isGitSource(source) {
|
|
|
340
340
|
export async function recompileAfterPluginMutation(projectRoot = process.cwd(), isGlobal = false, dependencies = {}) {
|
|
341
341
|
try {
|
|
342
342
|
const { compileSurface } = dependencies.compileSurface ? dependencies : await import('./surface.mjs');
|
|
343
|
+
// Only this layer's catalog changes. Entry files are static and rules and commands are
|
|
344
|
+
// read on demand at preflight, so a shared plugin change compiles no other repository
|
|
345
|
+
// (ON-DEMAND-CONTEXT-01; one install used to rebuild all registered brains).
|
|
346
|
+
await compileLayer(compileSurface, projectRoot, isGlobal);
|
|
347
|
+
if (isGlobal) {
|
|
348
|
+
// The repository the command ran in refreshes its own catalog now, so its plugin library
|
|
349
|
+
// never lags the change. That compile is cheap: entry files and vault indexes keep their
|
|
350
|
+
// own triggers, so only the catalog and CLI index move.
|
|
351
|
+
const agentDir = path.dirname(path.dirname(path.dirname(globalPluginsDir())));
|
|
352
|
+
const projectBrain = path.dirname(projectPluginsDir(projectRoot));
|
|
353
|
+
if (path.resolve(projectRoot, '.agent') !== path.resolve(agentDir) && fs.existsSync(projectBrain)) {
|
|
354
|
+
await compileLayer(compileSurface, projectRoot, false);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return { ok: true, inventoryUpdated: true };
|
|
358
|
+
} catch (err) {
|
|
359
|
+
try {
|
|
360
|
+
const { logger } = await import('./logger.mjs');
|
|
361
|
+
logger.warn('plugin-store', `Automatic compilation after plugin change failed: ${err.message}`);
|
|
362
|
+
} catch {}
|
|
363
|
+
return { ok: false, error: err.message };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function compileLayer(compileSurface, projectRoot, isGlobal) {
|
|
343
368
|
// Match the actual destination chosen by install/remove, not an ambient brain override.
|
|
344
369
|
const pluginsDir = isGlobal ? globalPluginsDir() : projectPluginsDir(projectRoot);
|
|
345
370
|
const brainDir = path.dirname(pluginsDir);
|
|
@@ -359,19 +384,6 @@ export async function recompileAfterPluginMutation(projectRoot = process.cwd(),
|
|
|
359
384
|
const instructionsFile = path.join(surfaceDir, 'INSTRUCTIONS.md');
|
|
360
385
|
const result = await compileSurface({ vaultDir, skillsDir, derivedDir, instructionsFile, force: true, semantic: false });
|
|
361
386
|
if (result.skipped && result.reason === 'stale-surface-code') throw new Error('Instruction compiler is stale; restart the host or run total-recall compile');
|
|
362
|
-
if (isGlobal) {
|
|
363
|
-
const { compileRegisteredProjects } = dependencies.compileRegisteredProjects ? dependencies : await import('../cli/rebuild.mjs');
|
|
364
|
-
const failed = await compileRegisteredProjects();
|
|
365
|
-
if (failed) throw new Error(`${failed} registered project surface compilation(s) failed`);
|
|
366
|
-
}
|
|
367
|
-
return { ok: true, inventoryUpdated: true };
|
|
368
|
-
} catch (err) {
|
|
369
|
-
try {
|
|
370
|
-
const { logger } = await import('./logger.mjs');
|
|
371
|
-
logger.warn('plugin-store', `Automatic compilation after plugin change failed: ${err.message}`);
|
|
372
|
-
} catch {}
|
|
373
|
-
return { ok: false, error: err.message };
|
|
374
|
-
}
|
|
375
387
|
}
|
|
376
388
|
|
|
377
389
|
/**
|
|
@@ -485,12 +497,16 @@ export function pruneReplacementArchives(archiveDir, { id, keep = 1, apply = tru
|
|
|
485
497
|
* @param {{ projectRoot?: string, global?: boolean, link?: boolean, peerDeps?: object, publicDeps?: object }} [options]
|
|
486
498
|
*/
|
|
487
499
|
export async function installPlugin(source, options = {}) {
|
|
488
|
-
const { projectRoot = process.cwd(),
|
|
500
|
+
const { projectRoot = process.cwd(), link = false, resolveBundle } = options;
|
|
489
501
|
if (!source || typeof source !== 'string') throw new Error('Missing plugin source');
|
|
490
502
|
source = source.trim();
|
|
503
|
+
// Nothing is installed per repository: every install goes to the shared
|
|
504
|
+
// folder, and a repository switches the plugin on or off with `plugin on|off`.
|
|
505
|
+
if (options.project === true) throw new Error("Plugins are installed only in the shared folder; a repository switches them with 'total-recall plugin on|off <id>'");
|
|
506
|
+
const isGlobal = true;
|
|
491
507
|
|
|
492
|
-
const scope =
|
|
493
|
-
const pluginsDir =
|
|
508
|
+
const scope = 'global';
|
|
509
|
+
const pluginsDir = globalPluginsDir();
|
|
494
510
|
|
|
495
511
|
let manifest;
|
|
496
512
|
let dest;
|
|
@@ -610,13 +626,9 @@ export async function installPlugin(source, options = {}) {
|
|
|
610
626
|
}
|
|
611
627
|
}
|
|
612
628
|
|
|
613
|
-
export function findInstalled(id
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// discoverPlugins() hides a global plugin shadowed by a project one.
|
|
617
|
-
return readPluginsDir(globalPluginsDir(), 'global').find((p) => p.id === id) || null;
|
|
618
|
-
}
|
|
619
|
-
return all.find((p) => p.id === id) || null;
|
|
629
|
+
export function findInstalled(id) {
|
|
630
|
+
// Every install lives in the shared folder.
|
|
631
|
+
return readPluginsDir(globalPluginsDir(), 'global').find((p) => p.id === id) || null;
|
|
620
632
|
}
|
|
621
633
|
|
|
622
634
|
export async function uninstallPlugin(id, options = {}) {
|