total-recall-brain 3.39.4 → 3.39.7
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/docs/reference/cli-reference.md +3 -2
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/SKILL.md +0 -1
- 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/secrets-sync.mjs +12 -9
- 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/mesh/src/core/leader-election.mjs +19 -10
- package/src/cli/agent-dir.mjs +41 -23
- package/src/cli/config.mjs +23 -0
- package/src/cli/connect.mjs +3 -0
- package/src/cli/context.mjs +11 -1
- package/src/cli/deploy.mjs +4 -1
- package/src/cli/doctor.mjs +57 -0
- package/src/cli/dream.mjs +2 -2
- package/src/cli/forget.mjs +5 -3
- package/src/cli/init.mjs +14 -1
- package/src/cli/memory-init.mjs +11 -2
- package/src/cli/plugin/create.mjs +18 -2
- package/src/cli/plugin/install.mjs +11 -1
- package/src/cli/rebuild.mjs +26 -3
- package/src/cli/remember.mjs +2 -2
- package/src/cli/rules.mjs +32 -6
- package/src/cli/share.mjs +2 -2
- package/src/cli/snapshot.mjs +2 -2
- package/src/cli/status.mjs +2 -2
- package/src/cli/sync.mjs +14 -1
- package/src/core/auto-compile.mjs +7 -2
- package/src/core/config.mjs +43 -13
- package/src/core/daemon-loop.mjs +26 -4
- package/src/core/dream.mjs +4 -2
- package/src/core/embeddings.mjs +33 -5
- package/src/core/fact-seeker.mjs +5 -2
- package/src/core/logger.mjs +65 -0
- package/src/core/plugin-store.mjs +10 -1
- package/src/core/repo-sync.mjs +71 -0
- package/src/core/runtime.mjs +7 -2
- package/src/core/surface.mjs +40 -4
- package/src/server/api.mjs +6 -3
- package/src/server/index.mjs +5 -2
- package/src/server/routes/_shared.mjs +18 -2
- package/scaffold/.agent/skills/total-recall/references/crm.md +0 -21
|
@@ -128,8 +128,9 @@ action and the largest rules; `draft --out policy.json` scaffolds the policy wit
|
|
|
128
128
|
each rule's original text; shorten the directives by hand (keep every operative
|
|
129
129
|
constraint), narrow `actions`, or exclude locally with `enabled:false` plus a
|
|
130
130
|
`reason`; `apply policy.json` validates hashes and merges into the single
|
|
131
|
-
`context:policy` decision; `verify` exits 1 on stale or orphan curation or
|
|
132
|
-
action capsule over budget; `prune [--apply] [--archive]` drops stale
|
|
131
|
+
`context:policy` decision; `verify [--json]` exits 1 on stale or orphan curation or
|
|
132
|
+
an action capsule over budget; `prune [--apply] [--archive] [--json]` drops stale
|
|
133
|
+
entries and
|
|
133
134
|
lists expired, superseded or duplicate rules. Originals are never modified.
|
|
134
135
|
|
|
135
136
|
### `recall`
|
package/package.json
CHANGED
|
@@ -19,7 +19,6 @@ Total Recall is working memory and the operation toolbox. Before decisions about
|
|
|
19
19
|
| Remember, recall, context, compile, dream, proposals, vault | Core portable memory and SSSS contracts | [Memory and instructions](references/memory.md) |
|
|
20
20
|
| Credentials, passwords, sessions, passkeys, PATs, encryption, rotation, projections, sync | `total-recall auth --help`; `auth secret --help` | [Auth and secrets](references/auth.md) |
|
|
21
21
|
| Documentation hubs and graph ingestion | `total-recall openwiki --help`; `openwiki hubs --json` | [OpenWiki](references/openwiki.md) |
|
|
22
|
-
| Accounts, buyers, records, branding, portable CRM instances | `total-recall crm status`; `crm menu` | [CRM](references/crm.md) |
|
|
23
22
|
| Nodes, access, enrollment, control server, presence, dispatch | `total-recall mesh --help` | [Mesh](references/mesh.md) |
|
|
24
23
|
| Install, author, configure and run plugins | `total-recall plugin --help`; `plugin list --json` | [Plugins](references/plugins.md) |
|
|
25
24
|
| Machines, networks, fleet audits and verified hardening | `total-recall security status --json`; owning plugin skill | [Security](references/security.md) |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
|
|
3
3
|
"id": "auth",
|
|
4
4
|
"name": "Auth & Secrets",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.1",
|
|
6
6
|
"description": "Hardware WebAuthn biometric FIDO2 passkeys, OAuth session security, and access control.",
|
|
7
7
|
"author": "Total Recall contributors",
|
|
8
8
|
"license": "MIT",
|
|
@@ -7,17 +7,20 @@ import { resolveSecretsPath, loadSecrets, saveSecrets, ensureMeta, appendAudit,
|
|
|
7
7
|
|
|
8
8
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
9
9
|
|
|
10
|
-
function serverPort() {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
function serverPort(override) {
|
|
11
|
+
// The leader's port is whatever it advertised on the mesh. Assuming the local
|
|
12
|
+
// default made sync fail with "fetch failed" against a leader that was up and
|
|
13
|
+
// reachable on a different port (the cloud brain serves on 3900, not 3000).
|
|
14
|
+
const candidate = Number(override ?? process.env.TR_SERVER_PORT ?? process.env.PORT ?? 3000);
|
|
15
|
+
return Number.isInteger(candidate) && candidate > 0 && candidate <= 65535 ? candidate : 3000;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
function meshUrl(ip, route) {
|
|
18
|
+
function meshUrl(ip, route, port) {
|
|
16
19
|
const octets = String(ip || '').split('.').map(Number);
|
|
17
20
|
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255) || octets[0] !== 100 || octets[1] < 64 || octets[1] > 127) {
|
|
18
21
|
throw new Error('Leader IP is outside the Tailscale CGNAT range');
|
|
19
22
|
}
|
|
20
|
-
return `http://${ip}:${serverPort()}${route}`;
|
|
23
|
+
return `http://${ip}:${serverPort(port)}${route}`;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function getSecretsChecksum() {
|
|
@@ -39,8 +42,8 @@ export function secretsSyncMode(env = process.env) {
|
|
|
39
42
|
* anything. The node's store is never replaced wholesale and the node keeps
|
|
40
43
|
* its own master password.
|
|
41
44
|
*/
|
|
42
|
-
export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch } = {}) {
|
|
43
|
-
const base = meshUrl(leaderIp, '/api/secrets/mesh');
|
|
45
|
+
export async function syncGrantedSecrets(leaderIp, { dir = brainDir, fetchImpl = fetch, port } = {}) {
|
|
46
|
+
const base = meshUrl(leaderIp, '/api/secrets/mesh', port);
|
|
44
47
|
const listResponse = await fetchImpl(base, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
45
48
|
if (!listResponse.ok) throw new Error(`Leader returned ${listResponse.status}`);
|
|
46
49
|
const { keys } = await listResponse.json();
|
|
@@ -76,6 +79,6 @@ export async function syncLoop() {
|
|
|
76
79
|
if (await isLeader()) return;
|
|
77
80
|
const leaderInfo = await getLeaderInfo();
|
|
78
81
|
if (!leaderInfo?.ip) return;
|
|
79
|
-
try { await syncGrantedSecrets(leaderInfo.ip); }
|
|
80
|
-
catch (err) { logger.error('secrets-sync', 'Failed to sync granted secrets', { error: err.message }); }
|
|
82
|
+
try { await syncGrantedSecrets(leaderInfo.ip, { port: leaderInfo.brain_port }); }
|
|
83
|
+
catch (err) { logger.error('secrets-sync', 'Failed to sync granted secrets', { error: err.message, leader: leaderInfo.ip, port: leaderInfo.brain_port ?? 'default' }); }
|
|
81
84
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
|
|
3
3
|
"id": "mesh",
|
|
4
4
|
"name": "Headscale",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.1",
|
|
6
6
|
"description": "Control-server mesh administration, Tailscale SSH, and peer node discovery.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"branding": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { getMeshPeers, getMeshSelf } from './mesh.mjs';
|
|
3
|
+
import { getMeshPeers, getMeshSelf, listEnrichedMeshNodes } from './mesh.mjs';
|
|
4
4
|
import { brainDir } from './config.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -26,12 +26,12 @@ export const FAILOVER_BOUND_MS = 12_000;
|
|
|
26
26
|
* Operator pin for which node leads.
|
|
27
27
|
*
|
|
28
28
|
* Lowest-online-IP is deterministic, but it says nothing about whether that node
|
|
29
|
-
* can actually run the daemon.
|
|
30
|
-
*
|
|
31
|
-
* permanently
|
|
32
|
-
*
|
|
33
|
-
* anywhere. Followers skip the task loop entirely, so "lowest IP" quietly
|
|
34
|
-
* "nobody works".
|
|
29
|
+
* can actually run the daemon. A shared or infrastructure node can hold the
|
|
30
|
+
* lowest address while running no Total Recall at all, so it captures
|
|
31
|
+
* leadership permanently and can never serve `/api/secrets/sync`; every other
|
|
32
|
+
* node idles as a follower: no research, no dream cycle, no surface recompile,
|
|
33
|
+
* anywhere. Followers skip the task loop entirely, so "lowest IP" quietly
|
|
34
|
+
* became "nobody works".
|
|
35
35
|
*
|
|
36
36
|
* Set `TR_LEADER_IP` to the mesh IP of the machine that owns the canonical brain
|
|
37
37
|
* on EVERY node that runs Total Recall, so they all agree on the answer. A node
|
|
@@ -70,18 +70,27 @@ function compareNodes(a, b) {
|
|
|
70
70
|
* each node wrote a private, node-local lease document and could elect itself.
|
|
71
71
|
*/
|
|
72
72
|
export async function getLeaderInfo() {
|
|
73
|
-
|
|
73
|
+
// Use the ENTITY-ENRICHED node list, not getMeshPeers(). `brain_port` is an
|
|
74
|
+
// entity variable: it lives in the vault's mesh-node document and is absent
|
|
75
|
+
// from raw Tailscale status. getMeshPeers() reads only that status, so its
|
|
76
|
+
// `brain_port` was always null and every caller fell back to port 3000 --
|
|
77
|
+
// while the leader was reachable the whole time on its real port.
|
|
78
|
+
const nodes = listEnrichedMeshNodes().filter((node) => node.online);
|
|
74
79
|
if (nodes.length === 0) return null;
|
|
75
80
|
|
|
76
81
|
const pin = leaderPin();
|
|
77
82
|
if (pin) {
|
|
78
83
|
const pinned = nodes.find((node) => node.ip === pin);
|
|
79
84
|
if (!pinned) return null;
|
|
80
|
-
return { hostname: pinned.hostname, ip: pinned.ip, strategy: 'pinned-mesh-ip' };
|
|
85
|
+
return { hostname: pinned.hostname, ip: pinned.ip, brain_port: pinned.brain_port ?? null, strategy: 'pinned-mesh-ip' };
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
const leader = [...nodes].sort(compareNodes)[0];
|
|
84
|
-
|
|
89
|
+
// `brain_port` is carried through because the leader does not necessarily
|
|
90
|
+
// serve on the default port. It was dropped here, so callers such as
|
|
91
|
+
// secrets-sync fell back to 3000 and reported "fetch failed" against a leader
|
|
92
|
+
// that was up and reachable the whole time on a different port.
|
|
93
|
+
return { hostname: leader.hostname, ip: leader.ip, brain_port: leader.brain_port ?? null, strategy: 'lowest-mesh-ip' };
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/**
|
package/src/cli/agent-dir.mjs
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
+
import { detectProjectBrain as detectProjectBrainInConfig } from '../core/config.mjs';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Parse --global / --project layer flags from CLI args.
|
|
@@ -39,32 +40,18 @@ export function getGlobalBrainDir() {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
|
-
* Detect a project-level brain by checking
|
|
43
|
-
* Returns { agentDir, brainDir }
|
|
43
|
+
* Detect a project-level brain by checking `<dir>/.agent/skills/total-recall/`
|
|
44
|
+
* (and the `.agents` spelling). Returns `{ agentDir, brainDir, projectRoot }`.
|
|
45
|
+
*
|
|
46
|
+
* Delegates to core/config.mjs so there is exactly ONE implementation of the
|
|
47
|
+
* rule. These were two independent copies that had already diverged: one
|
|
48
|
+
* checked `.agent` only, ignored `AGENT_DIR`, and required `SKILL.md`, so they
|
|
49
|
+
* could disagree about whether a project brain exists at all — and their
|
|
50
|
+
* callers then disagreed about where the surface belongs.
|
|
44
51
|
*/
|
|
45
52
|
export function detectProjectBrain(startDir = process.cwd()) {
|
|
46
53
|
if (process.env._TR_TEST_AGENT_DIR) return null;
|
|
47
|
-
|
|
48
|
-
const homeDir = os.homedir();
|
|
49
|
-
while (dir !== path.dirname(dir)) {
|
|
50
|
-
if (dir === homeDir) break; // don't detect global as project
|
|
51
|
-
const candidates = [
|
|
52
|
-
{ dir: path.join(dir, '.agent'), candidate: path.join(dir, '.agent', 'skills', 'total-recall') },
|
|
53
|
-
{ dir: path.join(dir, '.agents'), candidate: path.join(dir, '.agents', 'skills', 'total-recall') }
|
|
54
|
-
];
|
|
55
|
-
for (const c of candidates) {
|
|
56
|
-
if (fs.existsSync(path.join(c.candidate, 'SKILL.md')) || fs.existsSync(path.join(c.candidate, 'memory-vault'))) {
|
|
57
|
-
return {
|
|
58
|
-
agentDir: c.dir,
|
|
59
|
-
// Project brain lives at the detected candidate — never alias to global.
|
|
60
|
-
brainDir: c.candidate,
|
|
61
|
-
projectRoot: dir,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
dir = path.dirname(dir);
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
54
|
+
return detectProjectBrainInConfig(startDir);
|
|
68
55
|
}
|
|
69
56
|
|
|
70
57
|
/**
|
|
@@ -122,6 +109,37 @@ export function getBothBrains(targetPath = process.cwd()) {
|
|
|
122
109
|
};
|
|
123
110
|
}
|
|
124
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The directory an instruction surface belongs in, which is NOT always the
|
|
114
|
+
* agent dir. Harnesses read `AGENTS.md`/`CLAUDE.md` at the workspace root:
|
|
115
|
+
* global -> <home>/.agent (the agent dir IS the workspace root)
|
|
116
|
+
* project -> <projectRoot> (the repo root, NOT <repo>/.agent)
|
|
117
|
+
*
|
|
118
|
+
* Writing a project surface into `<repo>/.agent/` puts it where no harness
|
|
119
|
+
* looks, while the compile still reports success and "0 drift". Every caller
|
|
120
|
+
* that writes INSTRUCTIONS.md or client shims must use this instead of
|
|
121
|
+
* `resolveAgentDir`, which names the brain's own directory.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveSurfaceDir(layer = 'auto', targetPath = process.cwd()) {
|
|
124
|
+
if (process.env.AGENT_DIR || process.env._TR_TEST_AGENT_DIR) {
|
|
125
|
+
return process.env.AGENT_DIR || process.env._TR_TEST_AGENT_DIR;
|
|
126
|
+
}
|
|
127
|
+
if (layer === 'global') return getGlobalAgentDir();
|
|
128
|
+
const project = detectProjectBrain(targetPath);
|
|
129
|
+
if (!project) {
|
|
130
|
+
if (layer === 'project') throw new Error('No project brain found. Run `npx total-recall init --project` to create one.');
|
|
131
|
+
return getGlobalAgentDir();
|
|
132
|
+
}
|
|
133
|
+
return project.projectRoot || project.agentDir;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The path of the canonical instruction surface for a layer.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveInstructionsFile(layer = 'auto', targetPath = process.cwd()) {
|
|
140
|
+
return path.join(resolveSurfaceDir(layer, targetPath), 'INSTRUCTIONS.md');
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
/**
|
|
126
144
|
* Determine the default layer for a memory category.
|
|
127
145
|
* Always defaults to project brain. Use --global to explicitly target global.
|
package/src/cli/config.mjs
CHANGED
|
@@ -25,6 +25,11 @@ const KEY_MAP = {
|
|
|
25
25
|
'rules_invariants_budget_chars': { file: 'surface', path: 'rules.invariants_budget_chars', type: 'number' },
|
|
26
26
|
'rules_preferences_budget_chars': { file: 'surface', path: 'rules.preferences_budget_chars', type: 'number' },
|
|
27
27
|
'rules_corrections_budget_chars': { file: 'surface', path: 'rules.corrections_budget_chars', type: 'number' },
|
|
28
|
+
|
|
29
|
+
// Token budget for the `context` instruction capsule. Raise it above the
|
|
30
|
+
// default when every rule should load for every action instead of being
|
|
31
|
+
// narrowed by applicability.
|
|
32
|
+
'context_budget': { file: 'surface', path: 'context.budget', type: 'number' },
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
function resolveKeyInfo(key) {
|
|
@@ -113,6 +118,23 @@ function castAndValidate(valueStr, type, allowedValues) {
|
|
|
113
118
|
return valueStr;
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
export function readConfigValue(key, layer = 'global') {
|
|
122
|
+
const info = KEY_MAP[key];
|
|
123
|
+
if (!info) throw new Error(`Unknown config key: ${key}`);
|
|
124
|
+
const configDir = path.join(resolveBrainDir(layer), 'config');
|
|
125
|
+
const filePath = path.join(configDir, `${info.file}.yml`);
|
|
126
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
127
|
+
let parsed = {};
|
|
128
|
+
try { parsed = yaml.parse(fs.readFileSync(filePath, 'utf8')) || {}; } catch { return undefined; }
|
|
129
|
+
const parts = info.path.split('.');
|
|
130
|
+
let current = parsed;
|
|
131
|
+
for (const part of parts) {
|
|
132
|
+
if (current === null || current === undefined) return undefined;
|
|
133
|
+
current = current[part];
|
|
134
|
+
}
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
|
|
116
138
|
export default async function configCommand(args) {
|
|
117
139
|
const { layer, remainingArgs } = parseLayerFlag(args);
|
|
118
140
|
const command = remainingArgs[0] || 'get';
|
|
@@ -140,6 +162,7 @@ export default async function configCommand(args) {
|
|
|
140
162
|
rules_invariants_budget_chars Characters for the invariants section of CLAUDE.md/AGENTS.md (number; default 9000)
|
|
141
163
|
rules_preferences_budget_chars Same for preferences (default 4000)
|
|
142
164
|
rules_corrections_budget_chars Same for corrections (default 8000)
|
|
165
|
+
context_budget Token budget for the context instruction capsule (number; default 4000)
|
|
143
166
|
allowed_origins CORS origins (e.g. "http://localhost:5173,http://localhost:3000")
|
|
144
167
|
|
|
145
168
|
Examples:
|
package/src/cli/connect.mjs
CHANGED
|
@@ -315,6 +315,9 @@ function bootstrapAgentDir(agentDir) {
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
function readInstructions(cwd, agentDir) {
|
|
318
|
+
// `cwd` IS the project root the caller means, so read the surface there.
|
|
319
|
+
// resolveSurfaceDir() would re-detect from process.cwd() and could name a
|
|
320
|
+
// different project than the caller is operating on.
|
|
318
321
|
const candidates = [
|
|
319
322
|
path.join(cwd, 'INSTRUCTIONS.md'),
|
|
320
323
|
path.join(agentDir, 'INSTRUCTIONS.md')
|
package/src/cli/context.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { mergeGlobalRuleNodes, legacyRuleContributions } from '../core/surface.m
|
|
|
5
5
|
import { compileContext, capsuleResponse, renderCapsule } from '../core/context-compiler.mjs';
|
|
6
6
|
import { isBrainEnabled } from '../core/brain-registry.mjs';
|
|
7
7
|
import { surfaceInputsHash } from '../core/command-surface.mjs';
|
|
8
|
+
import { readConfigValue } from './config.mjs';
|
|
8
9
|
|
|
9
10
|
export default async function context(args = []) {
|
|
10
11
|
if (!args.length || args.includes('--help')) {
|
|
@@ -13,7 +14,16 @@ export default async function context(args = []) {
|
|
|
13
14
|
}
|
|
14
15
|
const { layer, remainingArgs } = parseLayerFlag(args);
|
|
15
16
|
const query = remainingArgs[0];
|
|
16
|
-
|
|
17
|
+
const configuredBudget = (() => {
|
|
18
|
+
for (const candidate of [layer === 'global' ? 'global' : 'project', 'global']) {
|
|
19
|
+
try {
|
|
20
|
+
const value = readConfigValue('context_budget', candidate);
|
|
21
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
22
|
+
} catch { /* fall through to the next layer */ }
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
})();
|
|
26
|
+
let actions = [], budget = configuredBudget ?? 4000, format = 'text', includeKnowledge = false, debug = false;
|
|
17
27
|
for (let i = 1; i < remainingArgs.length; i++) {
|
|
18
28
|
const flag = remainingArgs[i];
|
|
19
29
|
if (flag === '--knowledge') { includeKnowledge = true; continue; }
|
package/src/cli/deploy.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import fs from 'node:fs';
|
|
|
40
40
|
import path from 'node:path';
|
|
41
41
|
import os from 'node:os';
|
|
42
42
|
import { fileURLToPath } from 'node:url';
|
|
43
|
+
import { resolveSurfaceDir } from './agent-dir.mjs';
|
|
43
44
|
import yaml from 'yaml';
|
|
44
45
|
import crypto from 'node:crypto';
|
|
45
46
|
import { loadPluginModule } from '../core/plugin-runtime.mjs';
|
|
@@ -1689,7 +1690,9 @@ ${fmtLine('Target VFS: ', '~/.agent/')}
|
|
|
1689
1690
|
vaultDir: path.join(BRAIN_DIR, 'memory-vault'),
|
|
1690
1691
|
skillsDir: path.join(AGENT_DIR, 'skills'),
|
|
1691
1692
|
derivedDir: path.join(BRAIN_DIR, 'memory-derived'),
|
|
1692
|
-
|
|
1693
|
+
// This compile reads the hardcoded global brain above, so the surface
|
|
1694
|
+
// must be the global one too; auto-detect would resolve a project.
|
|
1695
|
+
instructionsFile: path.join(resolveSurfaceDir('global'), 'INSTRUCTIONS.md'),
|
|
1693
1696
|
});
|
|
1694
1697
|
logOk('Initial compile complete');
|
|
1695
1698
|
} catch (err) {
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -260,6 +260,63 @@ export default async function doctor() {
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// ── Instruction surface freshness ──
|
|
264
|
+
hr();
|
|
265
|
+
logInfo('INSTRUCTION SURFACE FRESHNESS');
|
|
266
|
+
|
|
267
|
+
// A surface that stopped being rewritten is invisible from the exit code:
|
|
268
|
+
// `compile` printed "Post-build verification passed: 0 drift" for months while
|
|
269
|
+
// the file harnesses actually read stayed frozen, because drift detection
|
|
270
|
+
// compares the vault to the jsonl indexes and never inspects the surface.
|
|
271
|
+
// This compares the surface against the vault hash it was built from.
|
|
272
|
+
try {
|
|
273
|
+
const { instructionsFilePath } = await import('../core/config.mjs');
|
|
274
|
+
const { computeVaultHash } = await import('../core/surface.mjs');
|
|
275
|
+
const { resolveBrainDir } = await import('./agent-dir.mjs');
|
|
276
|
+
|
|
277
|
+
const surfacePath = instructionsFilePath();
|
|
278
|
+
const stat = fs.statSync(surfacePath, { throwIfNoEntry: false });
|
|
279
|
+
|
|
280
|
+
// Compare against the vault that OWNS this surface. Using the global brain
|
|
281
|
+
// unconditionally compared a project surface against the global vault and
|
|
282
|
+
// warned on every run, even straight after a rebuild.
|
|
283
|
+
const brainDir = resolveBrainDir();
|
|
284
|
+
const vaultDir = path.join(brainDir, 'memory-vault');
|
|
285
|
+
const hashFile = path.join(brainDir, 'memory-derived', 'vault-hash.txt');
|
|
286
|
+
const brainExists = fs.existsSync(vaultDir);
|
|
287
|
+
|
|
288
|
+
if (!stat || !stat.isFile() || stat.size === 0) {
|
|
289
|
+
if (brainExists) {
|
|
290
|
+
// A brain exists but produced no surface: exactly the silent failure
|
|
291
|
+
// this check exists to catch, so it is a real failure.
|
|
292
|
+
logFail(`No instruction surface at ${surfacePath} — this is the file harnesses read.`);
|
|
293
|
+
failures++;
|
|
294
|
+
} else {
|
|
295
|
+
// Nothing initialised here yet; a missing surface is expected.
|
|
296
|
+
logOk(`No brain at ${brainDir} yet; no surface required.`);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
logOk(`Surface present: ${surfacePath} (${stat.size} bytes)`);
|
|
300
|
+
|
|
301
|
+
if (!brainExists || !fs.existsSync(hashFile)) {
|
|
302
|
+
logWarn(`No compiled vault hash at ${hashFile}; freshness not verified.`);
|
|
303
|
+
warnings++;
|
|
304
|
+
} else {
|
|
305
|
+
const stored = fs.readFileSync(hashFile, 'utf8').trim();
|
|
306
|
+
const current = computeVaultHash(vaultDir);
|
|
307
|
+
if (stored && current && stored !== current) {
|
|
308
|
+
logWarn(`Vault ${brainDir} changed since the surface was compiled — run \`total-recall rebuild\`.`);
|
|
309
|
+
warnings++;
|
|
310
|
+
} else {
|
|
311
|
+
logOk(`Surface is current with ${brainDir}.`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
logWarn(`Surface freshness could not be determined: ${err.message}`);
|
|
317
|
+
warnings++;
|
|
318
|
+
}
|
|
319
|
+
|
|
263
320
|
// ── Diagnostics Summary ──
|
|
264
321
|
hr();
|
|
265
322
|
console.log('\n ┌─────────────────────────────────────────────────────────┐');
|
package/src/cli/dream.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import { resolveAgentDir, resolveBrainDir, parseLayerFlag } from './agent-dir.mjs';
|
|
16
|
+
import { resolveAgentDir, resolveBrainDir, parseLayerFlag, resolveSurfaceDir } from './agent-dir.mjs';
|
|
17
17
|
|
|
18
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
19
|
|
|
@@ -59,7 +59,7 @@ export default async function dream(args) {
|
|
|
59
59
|
const skillsDir = path.join(agentDir, 'skills');
|
|
60
60
|
const derivedDir = path.join(brainDir, 'memory-derived');
|
|
61
61
|
const conflictsDir = path.join(brainDir, 'memory-inbox', 'conflicts');
|
|
62
|
-
const instructionsFile = path.join(
|
|
62
|
+
const instructionsFile = path.join(resolveSurfaceDir(effectiveLayer), 'INSTRUCTIONS.md');
|
|
63
63
|
|
|
64
64
|
console.error('\n 🌙 Starting dream cycle...\n');
|
|
65
65
|
const startTime = Date.now();
|
package/src/cli/forget.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains } from './agent-dir.mjs';
|
|
3
|
+
import { resolveAgentDir, resolveBrainDir, parseLayerFlag, getBothBrains, resolveSurfaceDir } from './agent-dir.mjs';
|
|
4
4
|
import { compileSurface } from '../core/surface.mjs';
|
|
5
5
|
import { isSafeVaultName } from '../core/vault.mjs';
|
|
6
6
|
import { getNodes } from '../core/vault-cache.mjs';
|
|
@@ -65,8 +65,10 @@ export default async function forget(args) {
|
|
|
65
65
|
if (!remainingArgs.includes('--no-compile')) {
|
|
66
66
|
const brainDir = path.dirname(globalVaultDir);
|
|
67
67
|
const agentDir = path.dirname(path.dirname(brainDir));
|
|
68
|
+
// This branch operates on the GLOBAL vault, so its surface is the global
|
|
69
|
+
// one. `layer` is not in scope here and must not be invented.
|
|
68
70
|
await compileSurface({ vaultDir: globalVaultDir, skillsDir: path.join(agentDir, 'skills'),
|
|
69
|
-
derivedDir: path.join(brainDir, 'memory-derived'), instructionsFile: path.join(
|
|
71
|
+
derivedDir: path.join(brainDir, 'memory-derived'), instructionsFile: path.join(resolveSurfaceDir('global'), 'INSTRUCTIONS.md'), semantic: false });
|
|
70
72
|
}
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
@@ -109,7 +111,7 @@ export default async function forget(args) {
|
|
|
109
111
|
const vaultDir = path.join(resolvedBrainDir, 'memory-vault');
|
|
110
112
|
const derivedDir = path.join(resolvedBrainDir, 'memory-derived');
|
|
111
113
|
const skillsDir = path.join(resolvedAgentDir, 'skills');
|
|
112
|
-
const instructionsFile = path.join(
|
|
114
|
+
const instructionsFile = path.join(resolveSurfaceDir(layer), 'INSTRUCTIONS.md');
|
|
113
115
|
const layerLabel = layer === 'project' ? '[project]' : '[global]';
|
|
114
116
|
|
|
115
117
|
// Attempt deletion
|
package/src/cli/init.mjs
CHANGED
|
@@ -249,6 +249,9 @@ export default async function init(args) {
|
|
|
249
249
|
if (!fs.existsSync(path.join(authSource, 'plugin.json'))) throw new Error('Required auth scaffold is missing');
|
|
250
250
|
if (!fs.existsSync(authTarget) && !opts.dryRun) fs.cpSync(authSource, authTarget, { recursive: true, errorOnExist: true, force: false });
|
|
251
251
|
|
|
252
|
+
// Every scaffold plugin's node_modules are wired in Step 2.5, after the skill
|
|
253
|
+
// tree has been copied into the brain (see the note there).
|
|
254
|
+
|
|
252
255
|
// ── Step 2: Seed core skills ──
|
|
253
256
|
logStep('2/4', 'Installing core skills into .agent/skills/');
|
|
254
257
|
|
|
@@ -304,9 +307,19 @@ export default async function init(args) {
|
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
// ── Step 2.5: Create data directories inside the meta-skill (brain dir) ──
|
|
310
|
+
// Wire every scaffold plugin's dependencies AFTER the skill copy above: the
|
|
311
|
+
// plugin directories only exist once the skill tree has been copied, so an
|
|
312
|
+
// earlier pass silently skipped everything except auth (copied separately).
|
|
313
|
+
// A plugin shipped without its node_modules aborts plugin loading for the whole
|
|
314
|
+
// scope — the symptom was 'Cannot find package express' on an unrelated command.
|
|
307
315
|
if (!opts.dryRun) {
|
|
308
316
|
const { preparePluginDependencies } = await import('../core/plugin-dependencies.mjs');
|
|
309
|
-
|
|
317
|
+
const installedPluginsDir = path.join(brainDir, 'plugins');
|
|
318
|
+
for (const id of (fs.existsSync(installedPluginsDir) ? fs.readdirSync(installedPluginsDir) : [])) {
|
|
319
|
+
const target = path.join(installedPluginsDir, id);
|
|
320
|
+
if (!fs.existsSync(path.join(target, 'package.json'))) continue;
|
|
321
|
+
preparePluginDependencies(target);
|
|
322
|
+
}
|
|
310
323
|
}
|
|
311
324
|
logStep('2.5/4', 'Creating user data directories inside meta-skill');
|
|
312
325
|
|
package/src/cli/memory-init.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Initialize portable memory without provisioning feature runtimes. */
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { getGlobalAgentDir } from './agent-dir.mjs';
|
|
5
6
|
import { registerProjectBrain, writeBrainIdentity, VAULT_CATEGORIES } from '../core/memory-brain.mjs';
|
|
6
7
|
|
|
@@ -25,7 +26,12 @@ export default async function init(args = []) {
|
|
|
25
26
|
}
|
|
26
27
|
const skillFile = path.join(brainDir, 'SKILL.md');
|
|
27
28
|
if (!fs.existsSync(skillFile)) {
|
|
28
|
-
|
|
29
|
+
// Resolve the template by PATH, not `new URL(..., import.meta.url)`. Under a
|
|
30
|
+
// transform (vitest, and any loader that does not hand back a file: URL)
|
|
31
|
+
// import.meta.url is not a file URL and the copy throws
|
|
32
|
+
// "The URL must be of scheme file", which aborted the whole init.
|
|
33
|
+
const template = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'templates', 'memory', 'SKILL.md');
|
|
34
|
+
fs.copyFileSync(template, skillFile);
|
|
29
35
|
}
|
|
30
36
|
const existingIdentity = fs.existsSync(path.join(brainDir, 'config', 'brain.json'));
|
|
31
37
|
writeBrainIdentity(brainDir, { name: existingIdentity ? undefined : project ? path.basename(process.cwd()) : 'global',
|
|
@@ -38,7 +44,10 @@ export default async function init(args = []) {
|
|
|
38
44
|
const { compileSurface } = await import('../core/surface.mjs');
|
|
39
45
|
const stats = await compileSurface({
|
|
40
46
|
vaultDir: path.join(brainDir, 'memory-vault'), skillsDir: path.join(agentDir, 'skills'),
|
|
41
|
-
derivedDir: path.join(brainDir, 'memory-derived'),
|
|
47
|
+
derivedDir: path.join(brainDir, 'memory-derived'),
|
|
48
|
+
// This function knows the layer as a boolean, not a `layer` variable, and
|
|
49
|
+
// already passes projectRoot below. Use the same source of truth.
|
|
50
|
+
instructionsFile: path.join(project ? process.cwd() : agentDir, 'INSTRUCTIONS.md'),
|
|
42
51
|
projectRoot: project ? process.cwd() : path.dirname(agentDir), semantic: false,
|
|
43
52
|
});
|
|
44
53
|
console.log(`Initialized ${project ? 'project' : 'global'} memory: ${brainDir} (${stats.nodesProcessed} nodes)`);
|
|
@@ -300,6 +300,22 @@ export async function run(argv = []) {
|
|
|
300
300
|
const args = Array.isArray(argv) ? argv.slice(3) : [];
|
|
301
301
|
const sub = args[0] || "status";
|
|
302
302
|
const json = args.includes("--json");
|
|
303
|
+
const wantsHelp = args.includes("--help") || args.includes("-h");
|
|
304
|
+
|
|
305
|
+
if (wantsHelp || sub === "help") {
|
|
306
|
+
const subs = ["status", ...Object.keys(IMPLEMENTED)];
|
|
307
|
+
console.log(\`${pluginName} v${manifest.version}
|
|
308
|
+
|
|
309
|
+
Usage: total-recall ${id} <subcommand> [options]
|
|
310
|
+
|
|
311
|
+
Subcommands:
|
|
312
|
+
\${subs.map((s) => \` \${s}\`).join("\\n")}
|
|
313
|
+
|
|
314
|
+
Options:
|
|
315
|
+
--json Machine-readable output
|
|
316
|
+
--help, -h Show this help\`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
303
319
|
|
|
304
320
|
if (sub === "status") {
|
|
305
321
|
const status = { plugin: ${JSON.stringify(id)}, implemented: Object.keys(IMPLEMENTED) };
|
|
@@ -312,7 +328,7 @@ export async function run(argv = []) {
|
|
|
312
328
|
|
|
313
329
|
if (IMPLEMENTED[sub]) return IMPLEMENTED[sub](args.slice(1));
|
|
314
330
|
|
|
315
|
-
console.error(\`${pluginName}: "\${sub}" is not implemented.\`);
|
|
331
|
+
console.error(\`${pluginName}: "\${sub}" is not implemented. Run 'total-recall ${id} --help' for available subcommands.\`);
|
|
316
332
|
process.exitCode = 2;
|
|
317
333
|
}
|
|
318
334
|
export default run;
|
|
@@ -494,7 +510,7 @@ const manifest = JSON.parse(fs.readFileSync(path.join(root, 'plugin.json'), 'utf
|
|
|
494
510
|
|
|
495
511
|
test('manifest identifies the plugin', () => {
|
|
496
512
|
assert.equal(manifest.id, ${JSON.stringify(id)});
|
|
497
|
-
assert.match(manifest.version,
|
|
513
|
+
assert.match(manifest.version, /^\\d+\\.\\d+\\.\\d+/);
|
|
498
514
|
assert.equal(manifest.license, 'MIT');
|
|
499
515
|
});
|
|
500
516
|
|
|
@@ -33,7 +33,17 @@ export async function installPlugin(args = []) {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const { autoCompileSurfaces } = await import('../../core/auto-compile.mjs');
|
|
36
|
-
|
|
36
|
+
// Compile the layer the plugin actually landed in. `scope` is not enough on
|
|
37
|
+
// its own: from a directory with no project brain, projectPluginsDir(cwd)
|
|
38
|
+
// resolves to the global plugins dir, so a plugin installed into the global
|
|
39
|
+
// brain still reports scope 'project'. Ask the resolver whether a project
|
|
40
|
+
// brain exists at all, and otherwise fall back to the global layer — the one
|
|
41
|
+
// that was really written. Assuming 'project' compiled nothing and printed
|
|
42
|
+
// "Auto-compile failed: No project brain found" after a successful install.
|
|
43
|
+
const { resolveBrainDir } = await import('../agent-dir.mjs');
|
|
44
|
+
let targetLayer = 'global';
|
|
45
|
+
try { resolveBrainDir('project'); targetLayer = 'project'; } catch { targetLayer = 'global'; }
|
|
46
|
+
if (!await autoCompileSurfaces({ layer: targetLayer, semantic: false, force: true })) process.exitCode = 1;
|
|
37
47
|
} catch (err) {
|
|
38
48
|
console.error(`❌ ${err.message}`);
|
|
39
49
|
process.exit(1);
|