total-recall-brain 3.40.7 → 3.41.1
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-memory.mjs +0 -0
- package/bin/total-recall.mjs +9 -40
- package/docs/reference/cli-reference.md +65 -1
- package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
- package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
- package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
- package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
- package/frontend/src/components/plugins/card/card.css +2 -5
- package/frontend/src/components/plugins/card/contract.ts +13 -19
- package/frontend/src/components/plugins/card/format.ts +8 -1
- package/frontend/src/components/plugins/card/widgets.tsx +4 -35
- package/frontend/src/components/plugins/plugin-frame.ts +30 -3
- package/frontend/src/pages/PluginsPage.tsx +3 -2
- package/package.json +1 -1
- package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
- package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
- package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
- package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
- package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
- package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
- package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
- package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
- package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
- package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
- package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
- package/scaffold/.agent/skills/plugin/references/card.md +90 -0
- package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
- package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
- package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
- package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
- package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
- package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
- package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
- package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
- package/src/cli/brain.mjs +72 -2
- package/src/cli/connect.mjs +10 -39
- package/src/cli/context-stats.mjs +104 -0
- package/src/cli/context.mjs +8 -0
- package/src/cli/daemon.mjs +10 -1
- package/src/cli/export.mjs +3 -2
- package/src/cli/ingest-okf.mjs +12 -15
- package/src/cli/init.mjs +0 -85
- package/src/cli/lint.mjs +3 -2
- package/src/cli/plugin/copies.mjs +68 -0
- package/src/cli/plugin/create.mjs +22 -24
- package/src/cli/plugin/index.mjs +49 -3
- package/src/cli/plugin/search.mjs +19 -2
- package/src/cli/rebuild.mjs +2 -0
- package/src/cli/remember.mjs +23 -0
- package/src/cli/status.mjs +10 -3
- package/src/cli/update.mjs +3 -2
- package/src/core/action-predictor.mjs +161 -0
- package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
- package/src/core/app-deploy/source.mjs +16 -9
- package/src/core/automatic-brain-updates.mjs +6 -3
- package/src/core/brain-registry.mjs +19 -2
- package/src/core/brain-state.mjs +23 -0
- package/src/core/cli-commands.mjs +1 -0
- package/src/core/cli-maintenance.mjs +1 -1
- package/src/core/cli-semantic-index.mjs +1 -1
- package/src/core/command-surface.mjs +3 -3
- package/src/core/composed-command-resolution.mjs +21 -0
- package/src/core/config.mjs +84 -0
- package/src/core/confined-memory.mjs +93 -0
- package/src/core/context-telemetry.mjs +214 -0
- package/src/core/daemon-control.mjs +14 -43
- package/src/core/fast-recall.mjs +1 -1
- package/src/core/memory-brain.mjs +2 -0
- package/src/core/plugin-activation.mjs +38 -6
- package/src/core/plugin-bundle.mjs +54 -0
- package/src/core/plugin-catalog.mjs +1 -1
- package/src/core/plugin-context.mjs +1 -1
- package/src/core/plugin-contracts/card.mjs +69 -37
- package/src/core/plugin-loader.mjs +5 -6
- package/src/core/plugin-runner.mjs +13 -6
- package/src/core/plugin-runtime.mjs +5 -4
- package/src/core/plugin-setup.mjs +5 -3
- package/src/core/plugin-state-store.mjs +12 -1
- package/src/core/plugin-state.mjs +46 -5
- package/src/core/plugin-store.mjs +78 -3
- package/src/core/plugin-tasks.mjs +3 -1
- package/src/core/plugin-updates.mjs +117 -0
- package/src/core/plugin-vendor.mjs +10 -4
- package/src/core/project-brain.mjs +14 -41
- package/src/core/project-id.mjs +4 -1
- package/src/core/required-plugins.mjs +151 -13
- package/src/core/skills-registry.mjs +27 -3
- package/src/core/ssss-kernel-bridge.mjs +3 -1
- package/src/core/startup-health.mjs +66 -16
- package/src/core/surface.mjs +53 -46
- package/src/core/testing/plugin-fixture.mjs +69 -0
- package/src/core/vault-files.mjs +13 -0
- package/src/core/vault.mjs +4 -30
- package/src/server/index.mjs +13 -1
- package/src/server/invoke-decision-child.mjs +49 -0
- package/src/server/invoke-execution.mjs +87 -0
- package/src/server/invoke-policy.mjs +64 -0
- package/src/server/invoke-read-child.mjs +26 -0
- package/src/server/invoke-targets.mjs +49 -0
- package/src/server/invoke-write-child.mjs +19 -0
- package/src/server/routes/invoke.mjs +314 -0
- package/src/server/routes/plugins.mjs +1 -1
- package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
- package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
- package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
- package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
- package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
- package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
- package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
- package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
- package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
- package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
- package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
- package/scaffold/openwiki/README.md +0 -10
- package/scaffold/openwiki/architecture.md +0 -5
- package/scaffold/openwiki/ide.md +0 -10
- package/scaffold/openwiki/memory.md +0 -11
- package/scaffold/openwiki/secrets.md +0 -7
- package/scaffold/openwiki/skills.md +0 -7
- package/src/core/okf-adapter.mjs +0 -559
- package/templates/openwiki/README.md +0 -10
- package/templates/openwiki/architecture.md +0 -5
- package/templates/openwiki/ide.md +0 -10
- package/templates/openwiki/memory.md +0 -11
- package/templates/openwiki/secrets.md +0 -7
- package/templates/openwiki/skills.md +0 -7
|
@@ -1,12 +1,43 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Required native capability installation checks; never opens credential state.
|
|
3
|
+
*
|
|
4
|
+
* The nine defaults live in ONE shared folder per machine — the global brain.
|
|
5
|
+
* A project brain never receives its own copy: discovery already falls back to
|
|
6
|
+
* the shared folder, and a repository switches a shared plugin on or off with
|
|
7
|
+
* its own `plugin_record` overlay. Copies made by earlier releases are reported
|
|
8
|
+
* by `plugin copies` so they can be audited and removed.
|
|
9
|
+
*/
|
|
2
10
|
import fs from 'node:fs';
|
|
3
11
|
import path from 'node:path';
|
|
4
|
-
import { readPluginsDir, PACKAGE_ROOT, resolvePluginFile } from './plugin-loader.mjs';
|
|
12
|
+
import { readPluginsDir, globalPluginsDir, PACKAGE_ROOT, resolvePluginFile } from './plugin-loader.mjs';
|
|
5
13
|
import { DEFAULT_PLUGIN_IDS } from './default-plugins.mjs';
|
|
14
|
+
import { comparePackageVersions } from './package-version.mjs';
|
|
6
15
|
import { preparePluginDependencies } from './plugin-dependencies.mjs';
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
17
|
+
/**
|
|
18
|
+
* True when this brain is the shared (global) brain that owns the defaults.
|
|
19
|
+
*/
|
|
20
|
+
function isSharedBrain(brainDir) {
|
|
21
|
+
return path.resolve(brainDir) === path.resolve(path.dirname(globalPluginsDir()));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate the required capability for a brain. A shared brain validates its own
|
|
26
|
+
* installs; a project brain validates its own copy when it still has one and
|
|
27
|
+
* otherwise the shared folder that discovery already falls back to.
|
|
28
|
+
*/
|
|
29
|
+
export function inspectRequiredPlugins(brainDir, { pluginsDir, sharedBrainDir } = {}) {
|
|
30
|
+
const ownDir = path.join(brainDir, 'plugins');
|
|
31
|
+
const sharedPlugins = sharedBrainDir ? path.join(sharedBrainDir, 'plugins') : globalPluginsDir();
|
|
32
|
+
// A project brain's plugins directory can exist yet hold none of the defaults
|
|
33
|
+
// (provisioning creates it empty). The shared folder is what actually serves
|
|
34
|
+
// the repository, so consult it whenever this brain carries no auth of its own.
|
|
35
|
+
const resolved = pluginsDir || (isSharedBrain(brainDir) ? globalPluginsDir()
|
|
36
|
+
: (hasOwnRequired(ownDir) ? ownDir : sharedPlugins));
|
|
37
|
+
const own = readPluginsDir(ownDir, 'required');
|
|
38
|
+
const shared = path.resolve(resolved) === path.resolve(ownDir) ? [] : readPluginsDir(resolved, 'shared');
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const plugins = [...own, ...shared].filter(plugin => (seen.has(plugin.id) ? false : seen.add(plugin.id)));
|
|
10
41
|
const errors = [];
|
|
11
42
|
const auth = plugins.find(plugin => plugin.id === 'auth');
|
|
12
43
|
if (!auth?.valid) errors.push('Required auth plugin is missing or invalid');
|
|
@@ -23,24 +54,131 @@ export function inspectRequiredPlugins(brainDir) {
|
|
|
23
54
|
} catch { errors.push(`Required auth file is missing or unsafe: ${file || 'CLI handler'}`); }
|
|
24
55
|
}
|
|
25
56
|
}
|
|
26
|
-
|
|
57
|
+
const source = !auth ? null : (path.resolve(auth.pluginsDir) === path.resolve(ownDir) ? 'local' : 'shared');
|
|
58
|
+
return { status: errors.length ? 'failed' : 'ready', errors, source };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** True when this brain carries the required plugin itself rather than by fallback. */
|
|
62
|
+
function hasOwnRequired(ownDir) {
|
|
63
|
+
return fs.existsSync(path.join(ownDir, 'auth', 'plugin.json'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Only a copy that declares an older release than the bundled one is refreshed; damaged same-release copies still fail. */
|
|
67
|
+
function isStaleInstall(target, source) {
|
|
68
|
+
try {
|
|
69
|
+
const installed = JSON.parse(fs.readFileSync(path.join(target, 'plugin.json'), 'utf8')).version;
|
|
70
|
+
const bundled = JSON.parse(fs.readFileSync(path.join(source, 'plugin.json'), 'utf8')).version;
|
|
71
|
+
return comparePackageVersions(installed, bundled) === -1;
|
|
72
|
+
} catch { return false; }
|
|
27
73
|
}
|
|
28
74
|
|
|
29
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Ensure the required plugins exist for a brain.
|
|
77
|
+
*
|
|
78
|
+
* There is one shared install per machine, in the global brain. A project brain
|
|
79
|
+
* never receives a copy: it is served by the shared folder, and a repository
|
|
80
|
+
* switches a shared plugin on or off with its own record. Provisioning a project
|
|
81
|
+
* therefore makes sure the SHARED brain is seeded, then validates that the
|
|
82
|
+
* required capability resolves for the project. Copies left in a project by an
|
|
83
|
+
* earlier release are refreshed in place (so they stay usable until removed),
|
|
84
|
+
* never created.
|
|
85
|
+
*/
|
|
86
|
+
export function ensureRequiredPlugins(brainDir, { sourceDir = path.join(PACKAGE_ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'plugins'), sharedBrainDir } = {}) {
|
|
30
87
|
// Validate the complete package before changing any installation.
|
|
31
88
|
for (const id of DEFAULT_PLUGIN_IDS) {
|
|
32
89
|
if (!fs.existsSync(path.join(sourceDir, id, 'plugin.json'))) throw new Error(`Required bundled ${id} plugin is missing`);
|
|
33
90
|
}
|
|
91
|
+
if (!isSharedBrain(brainDir)) {
|
|
92
|
+
// A repository installs nothing of its own. The shared brain is what serves
|
|
93
|
+
// it, so seed that one — never this project's plugins directory.
|
|
94
|
+
const sharedBrain = sharedBrainDir || path.dirname(globalPluginsDir());
|
|
95
|
+
if (path.resolve(sharedBrain) !== path.resolve(brainDir)) installDefaultsInto(sharedBrain, sourceDir);
|
|
96
|
+
return inspectRequired(brainDir, { sharedBrainDir: sharedBrain });
|
|
97
|
+
}
|
|
98
|
+
installDefaultsInto(brainDir, sourceDir);
|
|
99
|
+
return inspectRequired(brainDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Seed or refresh the default plugins inside one brain. */
|
|
103
|
+
function installDefaultsInto(brainDir, sourceDir) {
|
|
34
104
|
for (const id of DEFAULT_PLUGIN_IDS) {
|
|
35
|
-
|
|
36
|
-
if (!fs.existsSync(target)) {
|
|
105
|
+
const target = path.join(brainDir, 'plugins', id);
|
|
37
106
|
const source = path.join(sourceDir, id);
|
|
38
|
-
fs.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
107
|
+
if (!fs.existsSync(target)) {
|
|
108
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
109
|
+
fs.cpSync(source, target, { recursive: true, force: false, errorOnExist: true });
|
|
110
|
+
} else if (isStaleInstall(target, source)) {
|
|
111
|
+
// A vendored copy that no longer validates (or predates the bundled release) is replaced; node_modules stays.
|
|
112
|
+
for (const entry of fs.readdirSync(target)) if (entry !== 'node_modules') fs.rmSync(path.join(target, entry), { recursive: true, force: true });
|
|
113
|
+
for (const entry of fs.readdirSync(source)) if (entry !== 'node_modules') fs.cpSync(path.join(source, entry), path.join(target, entry), { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
preparePluginDependencies(target);
|
|
42
116
|
}
|
|
43
|
-
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Validate and fail loudly; provisioning must not report a brain it cannot serve. */
|
|
120
|
+
function inspectRequired(brainDir, options = {}) {
|
|
121
|
+
const report = inspectRequiredPlugins(brainDir, options);
|
|
44
122
|
if (report.status !== 'ready') throw new Error(report.errors.join('; '));
|
|
45
123
|
return report;
|
|
46
124
|
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Audit per-repository copies of the shared defaults.
|
|
128
|
+
*
|
|
129
|
+
* A copy is compared with the bundled plugin source this package ships, because
|
|
130
|
+
* that is what an install was made from. The comparison ignores installed
|
|
131
|
+
* dependencies, test scaffolding and a shared install's own generated state, so
|
|
132
|
+
* a copy reports `edited` only when its plugin source differs — meaning somebody
|
|
133
|
+
* changed the plugin inside that repository, and it must not be removed blindly.
|
|
134
|
+
*/
|
|
135
|
+
export function auditProjectPluginCopies(projectRoot, { sharedDir = globalPluginsDir(), sourceDir = path.join(PACKAGE_ROOT, 'scaffold', '.agent', 'skills', 'total-recall', 'plugins') } = {}) {
|
|
136
|
+
const pluginsDir = path.join(projectRoot, '.agent', 'skills', 'total-recall', 'plugins');
|
|
137
|
+
if (!fs.existsSync(pluginsDir)) return { copies: [], count: 0, sourceDir };
|
|
138
|
+
const copies = [];
|
|
139
|
+
for (const id of DEFAULT_PLUGIN_IDS) {
|
|
140
|
+
const copyDir = path.join(pluginsDir, id);
|
|
141
|
+
if (!fs.existsSync(copyDir)) continue;
|
|
142
|
+
const source = path.join(sourceDir, id);
|
|
143
|
+
const shared = path.join(sharedDir, id);
|
|
144
|
+
const row = {
|
|
145
|
+
id, dir: copyDir,
|
|
146
|
+
sourceDir: fs.existsSync(source) ? source : null,
|
|
147
|
+
sharedDir: fs.existsSync(shared) ? shared : null,
|
|
148
|
+
edited: null, version: null, sourceVersion: null, sharedVersion: null
|
|
149
|
+
};
|
|
150
|
+
const version = (dir) => { try { return JSON.parse(fs.readFileSync(path.join(dir, 'plugin.json'), 'utf8')).version; } catch { return null; } };
|
|
151
|
+
row.version = version(copyDir);
|
|
152
|
+
row.sourceVersion = row.sourceDir ? version(row.sourceDir) : null;
|
|
153
|
+
row.sharedVersion = row.sharedDir ? version(row.sharedDir) : null;
|
|
154
|
+
row.edited = row.sourceDir ? !sameTree(copyDir, row.sourceDir) : null;
|
|
155
|
+
copies.push(row);
|
|
156
|
+
}
|
|
157
|
+
return { copies, count: copies.length, sourceDir };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Compare two plugin trees, ignoring installed dependencies and generated state. */
|
|
161
|
+
function sameTree(left, right) {
|
|
162
|
+
const ignore = new Set(['node_modules', '.git', 'test', 'vitest.config.mjs', 'package-lock.json']);
|
|
163
|
+
// A shared install runs in place, so its tree also carries compiled surfaces
|
|
164
|
+
// and a local secrets projection. Those are not plugin source.
|
|
165
|
+
const ignored = (name) => ignore.has(name) || name.startsWith('.env');
|
|
166
|
+
const walk = (dir, base = '') => {
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
169
|
+
if (ignored(entry.name)) continue;
|
|
170
|
+
const relative = base ? `${base}/${entry.name}` : entry.name;
|
|
171
|
+
const full = path.join(dir, entry.name);
|
|
172
|
+
// Symlinks are compared by their target text; a linked dev checkout is not
|
|
173
|
+
// "edited", it is owned elsewhere.
|
|
174
|
+
if (entry.isSymbolicLink()) { out.push([relative, `link:${fs.readlinkSync(full)}`]); continue; }
|
|
175
|
+
if (entry.isDirectory()) { out.push(...walk(full, relative)); continue; }
|
|
176
|
+
out.push([relative, fs.readFileSync(full, 'utf8')]);
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
const a = walk(left); const b = walk(right);
|
|
182
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
183
|
+
} catch { return false; }
|
|
184
|
+
}
|
|
@@ -93,7 +93,7 @@ export function skillPackageFiles(skillDir) {
|
|
|
93
93
|
const files = [];
|
|
94
94
|
const visit = (dir, relative = '') => {
|
|
95
95
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
96
|
-
if (entry.name
|
|
96
|
+
if (skillPackageExcluded(entry.name)) continue;
|
|
97
97
|
const next = relative ? `${relative}/${entry.name}` : entry.name;
|
|
98
98
|
if (entry.isDirectory()) visit(path.join(dir, entry.name), next);
|
|
99
99
|
else if (entry.isFile() || entry.isSymbolicLink()) files.push(next);
|
|
@@ -264,14 +264,38 @@ export function listInstalls(brainDir, { skillId = null, repo = null } = {}) {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
*
|
|
267
|
+
* State that is never part of a skill package.
|
|
268
|
+
*
|
|
269
|
+
* A skill's package is its content: SKILL.md, references, prompts, scripts,
|
|
270
|
+
* subagents, evals and assets. Everything listed here is either the agent's
|
|
271
|
+
* private state, disposable derived data, or a nested install belonging to
|
|
272
|
+
* something else — and `config/secrets.enc` is the credential store itself,
|
|
273
|
+
* which must never be copied into another repository or published. Excluding
|
|
274
|
+
* them also stops the package hash changing every time memory is written.
|
|
275
|
+
*/
|
|
276
|
+
const SKILL_PACKAGE_EXCLUDED_DIRS = new Set([
|
|
277
|
+
'node_modules', '.git', 'memory-vault', 'memory-derived', 'derived',
|
|
278
|
+
'sessions', 'logs', 'browser-profile', 'plugins', 'plugin-state',
|
|
279
|
+
'memory-inbox', 'dist', 'coverage', '.cache', 'tmp',
|
|
280
|
+
]);
|
|
281
|
+
const SKILL_PACKAGE_EXCLUDED_FILES = [
|
|
282
|
+
/^\.DS_Store$/, /^secrets\.enc(\.|$)/, /^\.env(\.|$)/, /^id_[a-z]+$/, /\.pem$/,
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
/** True when a directory entry is private, derived or a nested install. */
|
|
286
|
+
export function skillPackageExcluded(name) {
|
|
287
|
+
return SKILL_PACKAGE_EXCLUDED_DIRS.has(name) || SKILL_PACKAGE_EXCLUDED_FILES.some((re) => re.test(name));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Recursive copy of skill directory, content only.
|
|
268
292
|
*/
|
|
269
293
|
export function copySkillDir(src, dest) {
|
|
270
294
|
if (!fs.existsSync(src)) throw new Error(`Source missing: ${src}`);
|
|
271
295
|
fs.mkdirSync(dest, { recursive: true });
|
|
272
296
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
273
297
|
for (const ent of entries) {
|
|
274
|
-
if (ent.name
|
|
298
|
+
if (skillPackageExcluded(ent.name)) continue;
|
|
275
299
|
const from = path.join(src, ent.name);
|
|
276
300
|
const to = path.join(dest, ent.name);
|
|
277
301
|
if (ent.isDirectory()) {
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
resolvePrimitiveDefinition,
|
|
21
21
|
} from '@ssss/cli/registry';
|
|
22
22
|
import { detectDirectWrites } from '@ssss/cli/guard';
|
|
23
|
-
import { logger } from './logger.mjs';
|
|
24
23
|
import { validateMemoryNode } from './total-recall-memory-validator.mjs';
|
|
25
24
|
import {
|
|
26
25
|
TOTAL_RECALL_HOST_EXTENSION,
|
|
@@ -56,6 +55,7 @@ const DIRECT_WRITE_APPROVED = [
|
|
|
56
55
|
'src/core/ssss-kernel-bridge.mjs',
|
|
57
56
|
'src/core/validated-write.mjs',
|
|
58
57
|
'src/core/vault.mjs',
|
|
58
|
+
'src/core/vault-files.mjs',
|
|
59
59
|
'src/core/surface.mjs',
|
|
60
60
|
'src/core/blackboard.mjs',
|
|
61
61
|
'src/core/steering.mjs',
|
|
@@ -450,6 +450,8 @@ export function compareVerdicts(localResult, kernelResult) {
|
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
export async function shadowCompare(envelope, vaultRoot, localResult, options = {}) {
|
|
453
|
+
// Ordinary kernel mutations must not bootstrap configuration/auth/plugins.
|
|
454
|
+
const { logger } = await import('./logger.mjs');
|
|
453
455
|
const dryEnvelope = { ...envelope, dry_run: true };
|
|
454
456
|
try {
|
|
455
457
|
const kernelResult = await processViaPackageKernel(dryEnvelope, vaultRoot, options);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { readProcessCommand } from './pid-lock.mjs';
|
|
8
8
|
import { CLI_COMMANDS } from './cli-commands.mjs';
|
|
@@ -12,8 +12,8 @@ export const cliEntry = path.join(packageRoot, 'bin/total-recall.mjs');
|
|
|
12
12
|
|
|
13
13
|
export function runBounded(binary, args, { cwd = process.cwd(), timeoutMs = 15000, maxBytes = 262144 } = {}) {
|
|
14
14
|
return new Promise(resolve => {
|
|
15
|
-
let child, output = '', size = 0, reason = null, settled = false, timer, escalation;
|
|
16
|
-
const finish = code => { if (settled) return; settled = true; clearTimeout(timer); resolve({ code, output, reason }); };
|
|
15
|
+
let child, output = '', stderr = '', size = 0, reason = null, settled = false, timer, escalation;
|
|
16
|
+
const finish = code => { if (settled) return; settled = true; clearTimeout(timer); resolve({ code, output, stderr: stderr.slice(-2000), reason }); };
|
|
17
17
|
const kill = signal => { try { if (process.platform !== 'win32' && child.pid) process.kill(-child.pid, signal); else child.kill(signal); } catch {} };
|
|
18
18
|
const stop = why => { if (reason || settled) return; reason = why; kill('SIGTERM'); escalation = setTimeout(() => { kill('SIGKILL'); finish(null); }, 500); };
|
|
19
19
|
try { child = spawn(binary, args, { cwd, shell: false, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'] }); }
|
|
@@ -21,7 +21,7 @@ export function runBounded(binary, args, { cwd = process.cwd(), timeoutMs = 1500
|
|
|
21
21
|
timer = setTimeout(() => stop('timeout'), timeoutMs);
|
|
22
22
|
child.on('error', () => { reason = 'spawn-failed'; clearTimeout(escalation); finish(null); });
|
|
23
23
|
child.stdout.on('data', chunk => { size += chunk.length; if (size <= maxBytes) output += chunk.toString(); else stop('output-limit'); });
|
|
24
|
-
child.stderr.on('data', chunk => { size += chunk.length; if (size
|
|
24
|
+
child.stderr.on('data', chunk => { size += chunk.length; if (size <= maxBytes) stderr += chunk.toString(); else stop('output-limit'); });
|
|
25
25
|
child.on('close', code => { if (!reason) finish(code); });
|
|
26
26
|
});
|
|
27
27
|
}
|
|
@@ -33,16 +33,57 @@ export async function verifyListener(pid, url, { run = runBounded, cwd = process
|
|
|
33
33
|
return result.code === 0 && !result.reason && result.output.split(/\r?\n/).includes(`p${pid}`);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/** A process's working directory, or null when it cannot be determined. */
|
|
37
|
+
export function readProcessCwd(pid, { run = spawnSync } = {}) {
|
|
38
|
+
try {
|
|
39
|
+
const res = run('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', timeout: 5_000 });
|
|
40
|
+
if (res.status !== 0 || !res.stdout) return null;
|
|
41
|
+
const line = res.stdout.split(/\r?\n/).find(l => l.startsWith('n'));
|
|
42
|
+
return line ? line.slice(1) || null : null;
|
|
43
|
+
} catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Does the process holding `pid` look like the entry at `hint`?
|
|
48
|
+
*
|
|
49
|
+
* A launch agent starts its program with a path relative to the service's
|
|
50
|
+
* WorkingDirectory (`node src/server/index.mjs`), so the recorded command line
|
|
51
|
+
* carries no absolute path. Resolving it against the *caller's* cwd made this
|
|
52
|
+
* check depend on where the operator happened to stand: `total-recall startup
|
|
53
|
+
* check` run from any other directory reported a healthy local server as
|
|
54
|
+
* `server-process-unverified`. Resolve against the process's own cwd instead,
|
|
55
|
+
* An unresolved entry is unknown; a matching suffix alone cannot identify an
|
|
56
|
+
* installation and must never authorize lifecycle actions.
|
|
57
|
+
*/
|
|
58
|
+
export function processIdentity(pidFile, hint, { alive = pid => { try { process.kill(pid, 0); return true; } catch (err) { if (err.code === 'ESRCH') return false; throw err; } }, command = readProcessCommand, cwd = readProcessCwd } = {}) {
|
|
37
59
|
let pid;
|
|
38
|
-
try { pid = Number(fs.readFileSync(pidFile, 'utf8').trim()); }
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!
|
|
60
|
+
try { pid = Number(fs.readFileSync(pidFile, 'utf8').trim()); }
|
|
61
|
+
catch (err) { return err.code === 'ENOENT' ? { status: 'not-started' } : { status: 'unknown', reason: 'pid-unreadable' }; }
|
|
62
|
+
if (!Number.isInteger(pid) || pid <= 0) return { status: 'unknown', reason: 'invalid-pid' };
|
|
63
|
+
try { if (!alive(pid)) return { status: 'stopped' }; }
|
|
64
|
+
catch { return { status: 'unknown', reason: 'process-liveness-unavailable', pid }; }
|
|
65
|
+
let cmd;
|
|
66
|
+
try { cmd = command(pid); } catch { /* unknown below */ }
|
|
67
|
+
if (!cmd) return { status: 'unknown', reason: 'process-identity-unavailable', pid };
|
|
42
68
|
const script = cmd.trim().match(/^(?:\S*node(?:\s+--[^ ]+)*)\s+(\S+)/)?.[1];
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
let entry = null;
|
|
70
|
+
if (script) {
|
|
71
|
+
// Absolute script paths resolve directly; relative ones resolve against the
|
|
72
|
+
// process's own working directory, never the caller's.
|
|
73
|
+
try {
|
|
74
|
+
const base = path.isAbsolute(script) ? null : cwd(pid);
|
|
75
|
+
if (!path.isAbsolute(script) && !base) throw new Error('Missing process cwd');
|
|
76
|
+
entry = fs.realpathSync(base ? path.resolve(base, script) : script);
|
|
77
|
+
if (entry === fs.realpathSync(hint)) return { status: 'running', pid, entry };
|
|
78
|
+
} catch { return { status: 'unknown', reason: 'process-entry-unavailable', pid }; }
|
|
79
|
+
}
|
|
80
|
+
return { status: 'conflict', reason: 'foreign-pid', pid, ...(entry ? { entry } : {}) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The single installation-aware daemon observation for status and lifecycle. */
|
|
84
|
+
export function observeDaemon(brainDir, { identity = processIdentity } = {}) {
|
|
85
|
+
const expected_entry = path.join(packageRoot, 'src', 'core', 'daemon-loop.mjs');
|
|
86
|
+
return { ...identity(path.join(brainDir, 'daemon.pid'), expected_entry), expected_entry, source: 'local-pid' };
|
|
46
87
|
}
|
|
47
88
|
|
|
48
89
|
export function healthVerdict(body) {
|
|
@@ -81,7 +122,6 @@ export async function fetchStartup(url, options = {}, fetchImpl = fetch) {
|
|
|
81
122
|
export async function startupHealth({ cwd = process.cwd(), brainDir, brainUrl = null, token = null, brainId = null, ensure = false,
|
|
82
123
|
appCheck = null, appStart = null, run = runBounded, fetchImpl = fetch, listener = verifyListener, platform = process.platform, home = os.homedir(), identity = processIdentity } = {}) {
|
|
83
124
|
const actions = [];
|
|
84
|
-
const daemonFile = path.join(brainDir, 'daemon.pid');
|
|
85
125
|
const serverFile = path.join(brainDir, 'server.pid');
|
|
86
126
|
const report = { checked_at: new Date().toISOString(), server: { status: 'not-configured' }, brain: { status: 'unknown' }, daemon: { status: 'unknown' },
|
|
87
127
|
ssss: { status: 'unknown', role: 'document-engine-tooling; no standalone daemon required' }, app: { status: 'not-declared' }, actions };
|
|
@@ -105,7 +145,7 @@ export async function startupHealth({ cwd = process.cwd(), brainDir, brainUrl =
|
|
|
105
145
|
} catch { return { status: 'offline', reason: 'health-unreachable' }; }
|
|
106
146
|
};
|
|
107
147
|
report.server = await probeServer();
|
|
108
|
-
report.daemon = local ?
|
|
148
|
+
report.daemon = local ? observeDaemon(brainDir, { identity }) : remoteDaemon;
|
|
109
149
|
const serverIdentity = local ? identity(serverFile, path.join(packageRoot, 'src', 'server', 'index.mjs')) : null;
|
|
110
150
|
if (ensure && local && report.server.status === 'offline' && ['not-started', 'stopped'].includes(serverIdentity.status)) {
|
|
111
151
|
const plist = path.join(home, 'Library', 'LaunchAgents', 'com.totalrecall.server.plist');
|
|
@@ -126,7 +166,7 @@ export async function startupHealth({ cwd = process.cwd(), brainDir, brainUrl =
|
|
|
126
166
|
const result = await run(process.execPath, [cliEntry, 'daemon', 'start'], { cwd });
|
|
127
167
|
actions.push({ component: 'daemon', action: 'cli-start', accepted: result.code === 0 && !result.reason });
|
|
128
168
|
for (let i = 0; i < 10; i++) {
|
|
129
|
-
report.daemon =
|
|
169
|
+
report.daemon = observeDaemon(brainDir, { identity }); if (report.daemon.status === 'running') break;
|
|
130
170
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
131
171
|
}
|
|
132
172
|
}
|
|
@@ -146,7 +186,17 @@ export async function startupHealth({ cwd = process.cwd(), brainDir, brainUrl =
|
|
|
146
186
|
}
|
|
147
187
|
} catch { report.brain = { status: 'offline', reason: 'instructions-unreachable' }; }
|
|
148
188
|
}
|
|
149
|
-
|
|
189
|
+
// SSSS is tooling that may be vendored by the project under test, installed
|
|
190
|
+
// alongside this package, or only on PATH. Searching `cwd` alone reported a
|
|
191
|
+
// working install as `unavailable` whenever the operator ran the check from
|
|
192
|
+
// anywhere other than a repository root, which made `shared_ready` depend on
|
|
193
|
+
// the caller's directory rather than on the machine.
|
|
194
|
+
const candidates = [
|
|
195
|
+
path.join(cwd, 'ssss'),
|
|
196
|
+
path.join(cwd, 'node_modules', '.bin', 'ssss'),
|
|
197
|
+
path.join(packageRoot, 'node_modules', '.bin', 'ssss'),
|
|
198
|
+
path.join(packageRoot, '..', '.bin', 'ssss'),
|
|
199
|
+
];
|
|
150
200
|
const tool = candidates.find(file => { try { fs.accessSync(file, fs.constants.X_OK); return fs.statSync(file).isFile(); } catch { return false; } }) || 'ssss';
|
|
151
201
|
const tooling = await run(tool, ['--help'], { cwd });
|
|
152
202
|
report.ssss = { status: tooling.code === 0 && !tooling.reason && /ssss/i.test(tooling.output) ? 'available' : 'unavailable', role: report.ssss.role };
|
package/src/core/surface.mjs
CHANGED
|
@@ -35,9 +35,10 @@ export function isSurfaceCodeStale(file = SURFACE_SOURCE, loadedHash = LOADED_SU
|
|
|
35
35
|
try { return sourceHash(file) !== loadedHash; } catch { return false; }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/** Composed scope order: selected project, exact-member groups, global. */
|
|
39
39
|
export function commandDirsFor(skillsDir) {
|
|
40
|
-
const dirs = skillsDir ? commandScopeDirectories(path.dirname(path.dirname(skillsDir)), globalAgentDir
|
|
40
|
+
const dirs = skillsDir ? commandScopeDirectories(path.dirname(path.dirname(skillsDir)), globalAgentDir,
|
|
41
|
+
{ projectAgentDir: path.dirname(skillsDir) }) : [{ scope: 'global', dir: path.join(globalAgentDir, 'commands') }];
|
|
41
42
|
// Compiling the global brain itself: its commands are the global ones.
|
|
42
43
|
if (dirs.length === 2 && path.resolve(dirs[0].dir) === path.resolve(dirs[1].dir)) dirs.shift();
|
|
43
44
|
return dirs;
|
|
@@ -399,7 +400,7 @@ export function buildInstalledPluginsSummary(projectRoot = process.cwd()) {
|
|
|
399
400
|
const desc = (p.manifest.description || 'Plugin capability').replace(/\s+/g, ' ').slice(0, 30).trimEnd();
|
|
400
401
|
const ops = (p.manifest.cli?.subcommands || []).map(sub => sub.name).filter(Boolean);
|
|
401
402
|
const shown = ops.join(' | ');
|
|
402
|
-
lines.push(`- **${p.id}** v${p.manifest.version || 'unknown'} · ${pluginActivation(p).state} · \`total-recall ${cmd}\` ${shown ? `[${shown}]` : '(no operations)'}: ${desc}`);
|
|
403
|
+
lines.push(`- **${p.id}** v${p.manifest.version || 'unknown'} · ${pluginActivation(p, { projectRoot }).state} · \`total-recall ${cmd}\` ${shown ? `[${shown}]` : '(no operations)'}: ${desc}`);
|
|
403
404
|
}
|
|
404
405
|
return lines.join('\n') + '\n';
|
|
405
406
|
} catch {
|
|
@@ -509,7 +510,7 @@ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir
|
|
|
509
510
|
if (!fs.statSync(target).isFile()) throw new Error('Linked instruction target must be a regular file');
|
|
510
511
|
// Preserve both the link and authored target text; replace only our block.
|
|
511
512
|
const raw = fs.readFileSync(target, 'utf8');
|
|
512
|
-
|
|
513
|
+
writeVerifiedSurface(target, injectDirectives(raw, rulesBlock));
|
|
513
514
|
return true;
|
|
514
515
|
} else {
|
|
515
516
|
const raw = fs.readFileSync(shimPath, 'utf8');
|
|
@@ -520,14 +521,14 @@ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir
|
|
|
520
521
|
}
|
|
521
522
|
|
|
522
523
|
const updated = injectDirectives(cleaned, rulesBlock);
|
|
523
|
-
|
|
524
|
+
writeVerifiedSurface(shimPath, updated);
|
|
524
525
|
return true;
|
|
525
526
|
}
|
|
526
527
|
} else {
|
|
527
528
|
if (!fs.existsSync(shimDir)) {
|
|
528
529
|
fs.mkdirSync(shimDir, { recursive: true });
|
|
529
530
|
}
|
|
530
|
-
|
|
531
|
+
writeVerifiedSurface(shimPath, fullContent);
|
|
531
532
|
return true;
|
|
532
533
|
}
|
|
533
534
|
} catch (err) {
|
|
@@ -535,6 +536,11 @@ async function writeShim(shimPath, skillsDir, nodes = [], { vaultDir, derivedDir
|
|
|
535
536
|
}
|
|
536
537
|
}
|
|
537
538
|
|
|
539
|
+
function writeVerifiedSurface(file, content) {
|
|
540
|
+
atomicWrite(file, content);
|
|
541
|
+
if (fs.readFileSync(file, 'utf8') !== content) throw new Error('Instruction read-back verification failed');
|
|
542
|
+
}
|
|
543
|
+
|
|
538
544
|
/**
|
|
539
545
|
* Map of client names → shim file paths they require.
|
|
540
546
|
*/
|
|
@@ -561,8 +567,8 @@ const CLIENT_SHIMS = {
|
|
|
561
567
|
vscode: ['.github/copilot-instructions.md', '.vscode/copilot-instructions.md'],
|
|
562
568
|
githubCopilot: ['.github/copilot-instructions.md'],
|
|
563
569
|
pi: ['AGENTS.md'],
|
|
564
|
-
hermes: ['.
|
|
565
|
-
'hermes-agent': ['.
|
|
570
|
+
hermes: ['AGENTS.md'],
|
|
571
|
+
'hermes-agent': ['AGENTS.md'],
|
|
566
572
|
dsh: ['AGENTS.md'],
|
|
567
573
|
'deepseek-harness': ['AGENTS.md'],
|
|
568
574
|
openclaw: ['MEMORY.md', 'AGENTS.md']
|
|
@@ -591,15 +597,39 @@ function readConnectedClients(clientsPath) {
|
|
|
591
597
|
}
|
|
592
598
|
|
|
593
599
|
return null;
|
|
594
|
-
} catch {
|
|
595
|
-
|
|
600
|
+
} catch (err) {
|
|
601
|
+
throw new Error(`Cannot read connected instruction clients: ${err.code || err.message}`);
|
|
596
602
|
}
|
|
597
603
|
}
|
|
598
604
|
|
|
605
|
+
function instructionFiles(instructionsFile, skillsDir) {
|
|
606
|
+
const base = path.dirname(instructionsFile);
|
|
607
|
+
const agent = skillsDir ? path.dirname(skillsDir) : path.join(base, '.agent');
|
|
608
|
+
const clients = readConnectedClients(path.join(agent, 'skills', 'total-recall', 'config', 'clients.json'));
|
|
609
|
+
return [...new Set([instructionsFile, ...[...(clients || [])].flatMap(client =>
|
|
610
|
+
(CLIENT_SHIMS[client] || []).map(file => path.join(base, file)))])];
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function describeInstruction(file, base) {
|
|
614
|
+
return { file: path.relative(base, file), target: path.relative(base, fs.realpathSync(file)), sha256: sourceHash(file) };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Derived hashes certify exact output bytes and link bindings, never runtime consumption. */
|
|
618
|
+
function instructionArtifactsMatch(files, base, manifestFile) {
|
|
619
|
+
try {
|
|
620
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
621
|
+
if (manifest.version !== 1 || !Array.isArray(manifest.files) || manifest.files.length !== files.length) return false;
|
|
622
|
+
return files.every((file, index) => {
|
|
623
|
+
const current = describeInstruction(file, base), previous = manifest.files[index];
|
|
624
|
+
return previous?.file === current.file && previous.target === current.target && previous.sha256 === current.sha256;
|
|
625
|
+
});
|
|
626
|
+
} catch { return false; }
|
|
627
|
+
}
|
|
628
|
+
|
|
599
629
|
/**
|
|
600
630
|
* Compile shims asynchronously.
|
|
601
631
|
*/
|
|
602
|
-
async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir, derivedDir, force = false } = {}) {
|
|
632
|
+
async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultDir, derivedDir, force = false, files = instructionFiles(instructionsFile, skillsDir) } = {}) {
|
|
603
633
|
const agentDir = path.dirname(instructionsFile);
|
|
604
634
|
// `instructionsFile` is always `<agentDir>/INSTRUCTIONS.md`, so agentDir is
|
|
605
635
|
// already the directory the surface belongs in. Do not derive it again.
|
|
@@ -616,12 +646,6 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
|
|
|
616
646
|
// (rebuild.mjs: `<agentDir>/INSTRUCTIONS.md` with skillsDir `<agentDir>/skills`;
|
|
617
647
|
// plugin-store.mjs likewise), so the peel was never correct for either.
|
|
618
648
|
const baseDir = agentDir;
|
|
619
|
-
let injectedCount = 0;
|
|
620
|
-
|
|
621
|
-
// Always write the canonical INSTRUCTIONS.md
|
|
622
|
-
if (await writeShim(path.join(baseDir, 'INSTRUCTIONS.md'), skillsDir, nodes, { vaultDir, derivedDir, force })) {
|
|
623
|
-
injectedCount++;
|
|
624
|
-
}
|
|
625
649
|
|
|
626
650
|
// Determine which client shims to write.
|
|
627
651
|
// `baseDir/.agent/skills/...` only holds for a PROJECT brain. For the global
|
|
@@ -631,9 +655,6 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
|
|
|
631
655
|
// stopped updating. Derive the brain dir from the caller's skillsDir instead,
|
|
632
656
|
// which is correct for both layers. Fall back to the legacy guess only when
|
|
633
657
|
// skillsDir was not supplied.
|
|
634
|
-
const brainDirForConfig = skillsDir ? path.dirname(skillsDir) : path.join(baseDir, '.agent');
|
|
635
|
-
const clientsPath = path.join(brainDirForConfig, 'skills', 'total-recall', 'config', 'clients.json');
|
|
636
|
-
const connectedClients = readConnectedClients(clientsPath);
|
|
637
658
|
|
|
638
659
|
// Client shims are written BESIDE INSTRUCTIONS.md, in the same directory, so
|
|
639
660
|
// the surface directory is decided in exactly one place: `instructionsFile`.
|
|
@@ -645,20 +666,9 @@ async function compilePointers(instructionsFile, skillsDir, nodes = [], { vaultD
|
|
|
645
666
|
// `instructionsFile`; this function must not re-derive or relocate it. Doing
|
|
646
667
|
// so previously wrote every project shim into `<repo>/.agent/AGENTS.md`, where
|
|
647
668
|
// no harness looks, while still reporting a successful compilation.
|
|
648
|
-
|
|
649
|
-
// Only write shims for connected clients
|
|
650
|
-
for (const client of connectedClients) {
|
|
651
|
-
const files = CLIENT_SHIMS[client];
|
|
652
|
-
if (!files) continue;
|
|
653
|
-
for (const file of files) {
|
|
654
|
-
if (await writeShim(path.join(baseDir, file), skillsDir, nodes, { vaultDir, derivedDir, force })) {
|
|
655
|
-
injectedCount++;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
669
|
+
for (const file of files) await writeShim(file, skillsDir, nodes, { vaultDir, derivedDir, force });
|
|
660
670
|
|
|
661
|
-
return
|
|
671
|
+
return files.length;
|
|
662
672
|
}
|
|
663
673
|
|
|
664
674
|
/**
|
|
@@ -709,13 +719,17 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
|
|
|
709
719
|
// Commands and skills feed the surface too; a change to either must recompile
|
|
710
720
|
// even when no memory node changed.
|
|
711
721
|
const inputsHashFile = path.join(derivedDir, 'surface-inputs-hash.txt');
|
|
712
|
-
const
|
|
722
|
+
const files = instructionFiles(instructionsFile, skillsDir);
|
|
723
|
+
const surfaceDir = path.dirname(instructionsFile);
|
|
724
|
+
const artifactsFile = path.join(derivedDir, 'surface-artifacts.json');
|
|
725
|
+
const inputsHash = sourceHash(SURFACE_SOURCE) + catalog.fingerprint + JSON.stringify(files) + surfaceInputsHash({ commandDirs: commandDirsFor(skillsDir), skillsDir });
|
|
713
726
|
|
|
714
727
|
if (!force && fs.existsSync(hashFile)) {
|
|
715
728
|
const storedHash = fs.readFileSync(hashFile, 'utf8').trim();
|
|
716
729
|
const storedGlobal = fs.existsSync(globalHashFile) ? fs.readFileSync(globalHashFile, 'utf8').trim() : '';
|
|
717
730
|
const storedInputs = fs.existsSync(inputsHashFile) ? fs.readFileSync(inputsHashFile, 'utf8').trim() : '';
|
|
718
|
-
if (storedHash === currentHash && storedGlobal === globalHash && storedInputs === inputsHash
|
|
731
|
+
if (storedHash === currentHash && storedGlobal === globalHash && storedInputs === inputsHash &&
|
|
732
|
+
instructionArtifactsMatch(files, surfaceDir, artifactsFile)) {
|
|
719
733
|
return {
|
|
720
734
|
nodesProcessed: nodes.length,
|
|
721
735
|
skillsInjected: 0,
|
|
@@ -728,7 +742,7 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
|
|
|
728
742
|
}
|
|
729
743
|
|
|
730
744
|
// 1. Write pointer and active rules to all instruction shims
|
|
731
|
-
const skillsInjected = await compilePointers(instructionsFile, skillsDir, ruleNodes, { vaultDir, derivedDir, force });
|
|
745
|
+
const skillsInjected = await compilePointers(instructionsFile, skillsDir, ruleNodes, { vaultDir, derivedDir, force, files });
|
|
732
746
|
|
|
733
747
|
// 2. Build derived indexes (powers semantic search API)
|
|
734
748
|
if (!fs.existsSync(derivedDir)) {
|
|
@@ -820,17 +834,9 @@ export async function compileSurface({ vaultDir, skillsDir, derivedDir, instruct
|
|
|
820
834
|
atomicWrite(hashFile, currentHash);
|
|
821
835
|
atomicWrite(globalHashFile, globalHash);
|
|
822
836
|
atomicWrite(inputsHashFile, inputsHash);
|
|
837
|
+
atomicWrite(artifactsFile, JSON.stringify({ version: 1, files: files.map(file => describeInstruction(file, surfaceDir)) }, null, 2));
|
|
823
838
|
writeProjectionManifest(derivedDir, currentHash);
|
|
824
839
|
|
|
825
|
-
// 6. Generate live OKF Index and Log
|
|
826
|
-
try {
|
|
827
|
-
const { generateLiveIndex, generateLiveLog } = await import('./okf-adapter.mjs');
|
|
828
|
-
generateLiveIndex(vaultDir);
|
|
829
|
-
generateLiveLog(vaultDir);
|
|
830
|
-
} catch (err) {
|
|
831
|
-
logger.warn('surface', `Failed to generate live OKF index/log: ${err.message}`);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
840
|
return {
|
|
835
841
|
nodesProcessed: nodes.length,
|
|
836
842
|
skillsInjected,
|
|
@@ -875,7 +881,8 @@ function writeProjectionManifest(derivedDir, vaultHash) {
|
|
|
875
881
|
{ file: 'local-search.json', disposable: true },
|
|
876
882
|
{ file: 'embeddings.jsonl', disposable: true },
|
|
877
883
|
{ file: 'cli-semantic-index.json', disposable: true },
|
|
878
|
-
{ file: 'vault-hash.txt', disposable: true }
|
|
884
|
+
{ file: 'vault-hash.txt', disposable: true },
|
|
885
|
+
{ file: 'surface-artifacts.json', disposable: true }
|
|
879
886
|
],
|
|
880
887
|
rebuild_command: 'npx total-recall compile'
|
|
881
888
|
};
|