total-recall-brain 3.41.4 → 3.41.6
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 +10 -6
- package/docs/reference/cli-reference.md +18 -0
- package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
- package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +11 -0
- package/frontend/src/App.tsx +47 -122
- package/frontend/src/components/plugins/plugin-frame.ts +1 -0
- package/frontend/src/index.css +152 -86
- package/frontend/src/pages/HelpPage.tsx +13 -10
- package/frontend/src/pages/MemoryPage.tsx +28 -30
- package/frontend/src/pages/NetworkPage.tsx +3 -3
- package/frontend/src/pages/PluginsPage.tsx +6 -5
- package/frontend/src/pages/RulesPage.tsx +16 -12
- package/package.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
- package/src/cli/agent.mjs +12 -1
- package/src/cli/compact.mjs +108 -0
- package/src/cli/connect.mjs +56 -1
- package/src/cli/context.mjs +55 -23
- package/src/cli/doctor.mjs +5 -2
- package/src/cli/forget.mjs +2 -0
- package/src/cli/harness.mjs +62 -50
- package/src/cli/memory-entry.mjs +2 -2
- package/src/cli/preflight.mjs +152 -16
- package/src/cli/release.mjs +386 -0
- package/src/cli/remember.mjs +19 -1
- package/src/cli/rules.mjs +54 -12
- package/src/cli/startup.mjs +1 -1
- package/src/cli/status.mjs +12 -2
- package/src/core/claude-hooks.mjs +41 -3
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-semantic-index.mjs +3 -2
- package/src/core/conflict-detector.mjs +23 -12
- package/src/core/context-compactor.mjs +145 -0
- package/src/core/context-policy.mjs +20 -0
- package/src/core/duplicate-guard.mjs +350 -0
- package/src/core/meta-harness.mjs +31 -11
- package/src/core/preflight-gate.mjs +2 -2
- package/src/core/preflight-receipt.mjs +29 -1
- package/src/core/scaffold-plugins.mjs +7 -0
- package/src/core/stdio-flush.mjs +23 -0
- package/src/core/surface.mjs +33 -10
- package/src/server/invoke-durability.mjs +39 -0
- package/src/server/invoke-write-child.mjs +25 -0
- package/src/server/routes/invoke.mjs +7 -0
- package/src/server/routes/plugins.mjs +2 -2
- package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
- package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
- package/frontend/src/components/DaemonLogsTab.tsx +0 -54
- package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
- package/frontend/src/components/TaskQueueTab.tsx +0 -193
- package/frontend/src/components/VoiceInput.tsx +0 -112
- package/frontend/src/pages/ChatPage.tsx +0 -1059
- package/frontend/src/pages/CollabPage.tsx +0 -470
- package/frontend/src/pages/DesignDocsPage.tsx +0 -621
- package/frontend/src/pages/FilesPage.tsx +0 -393
- package/frontend/src/pages/HealthPage.tsx +0 -359
- package/frontend/src/pages/InboxPage.tsx +0 -83
- package/frontend/src/pages/InstructionsPage.tsx +0 -583
- package/frontend/src/pages/IntegrationsPage.tsx +0 -761
- package/frontend/src/pages/MeshPage.css +0 -58
- package/frontend/src/pages/MeshPage.tsx +0 -1219
- package/frontend/src/pages/OkfPage.tsx +0 -535
- package/frontend/src/pages/SandboxPage.tsx +0 -113
- package/frontend/src/pages/SkillsPage.tsx +0 -1994
- package/frontend/src/pages/TasksPage.tsx +0 -427
- package/frontend/src/pages/UsagePage.tsx +0 -702
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* total-recall release — publish a package, propagate it, and verify every consumer.
|
|
3
|
+
*
|
|
4
|
+
* The missing capability this fills: publishing to npm and verifying that the
|
|
5
|
+
* published artifact actually reached every registered consumer were manual
|
|
6
|
+
* steps outside any TR command. A release that is only on npm is not released,
|
|
7
|
+
* and a release whose consumers were never verified is a claim, not a fact.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx total-recall release plan <pkg> [--version X.Y.Z]
|
|
11
|
+
* npx total-recall release publish <pkg> [--version X.Y.Z] [--otp N]
|
|
12
|
+
* npx total-recall release propagate [--apply]
|
|
13
|
+
* npx total-recall release verify <pkg> [--version X.Y.Z]
|
|
14
|
+
* npx total-recall release all <pkg> [--version X.Y.Z] [--apply] [--otp N]
|
|
15
|
+
*
|
|
16
|
+
* Never publishes without --apply. Never trusts the publish command's own
|
|
17
|
+
* report: verification reads the registry and each consumer's resolved
|
|
18
|
+
* installation independently.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { BRAIN_PACKAGES, updateAllBrains } from '../core/brain-updates.mjs';
|
|
25
|
+
import { listUpdateRoots, rootsFromEnv } from '../core/package-auto-update.mjs';
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PACKAGES = BRAIN_PACKAGES;
|
|
28
|
+
|
|
29
|
+
/** @param {string[]} args */
|
|
30
|
+
export function parseArgs(args) {
|
|
31
|
+
const opts = {
|
|
32
|
+
action: null,
|
|
33
|
+
packages: [],
|
|
34
|
+
version: null,
|
|
35
|
+
apply: false,
|
|
36
|
+
otp: null,
|
|
37
|
+
json: false,
|
|
38
|
+
help: false,
|
|
39
|
+
root: null,
|
|
40
|
+
timeoutMs: 20 * 60 * 1000,
|
|
41
|
+
};
|
|
42
|
+
const positional = [];
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const a = args[i];
|
|
45
|
+
if (a === '--help' || a === '-h') opts.help = true;
|
|
46
|
+
else if (a === '--apply') opts.apply = true;
|
|
47
|
+
else if (a === '--json') opts.json = true;
|
|
48
|
+
else if (a === '--version' && args[i + 1]) opts.version = args[++i];
|
|
49
|
+
else if (a === '--otp' && args[i + 1]) opts.otp = args[++i];
|
|
50
|
+
else if (a === '--root' && args[i + 1]) opts.root = args[++i];
|
|
51
|
+
else if (a === '--timeout' && args[i + 1]) opts.timeoutMs = Number(args[++i]);
|
|
52
|
+
else if (!a.startsWith('-')) positional.push(a);
|
|
53
|
+
}
|
|
54
|
+
opts.action = positional.shift() || null;
|
|
55
|
+
opts.packages = positional;
|
|
56
|
+
return opts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ACTIONS = ['plan', 'publish', 'propagate', 'verify', 'all'];
|
|
60
|
+
|
|
61
|
+
function printHelp() {
|
|
62
|
+
console.log(`
|
|
63
|
+
total-recall release — publish, propagate and verify a package release
|
|
64
|
+
|
|
65
|
+
Usage:
|
|
66
|
+
total-recall release plan <pkg> [--version X.Y.Z]
|
|
67
|
+
total-recall release publish <pkg> [--version X.Y.Z] [--otp N] --apply
|
|
68
|
+
total-recall release propagate [--apply] [--json]
|
|
69
|
+
total-recall release verify <pkg> [--version X.Y.Z] [--json]
|
|
70
|
+
total-recall release all <pkg> [--version X.Y.Z] [--apply] [--otp N] [--json]
|
|
71
|
+
|
|
72
|
+
Actions:
|
|
73
|
+
plan Resolve the release version and report gate/consumer readiness. No writes.
|
|
74
|
+
publish Run the package's prepublish gate, then npm publish. Requires --apply.
|
|
75
|
+
propagate Pin the published version across every registered consumer (update --all-brains).
|
|
76
|
+
verify Independently confirm the registry artifact and each consumer's resolved install.
|
|
77
|
+
all plan → publish → propagate → verify, stopping at the first failure.
|
|
78
|
+
|
|
79
|
+
Rules enforced here:
|
|
80
|
+
• Publishing requires --apply; plan/verify never write.
|
|
81
|
+
• Verification never trusts the publish or propagate report: it re-reads the
|
|
82
|
+
npm registry and each consumer's own resolved package.json.
|
|
83
|
+
• A version that is only on npm is reported as incomplete, not as released.
|
|
84
|
+
|
|
85
|
+
Options:
|
|
86
|
+
--apply Perform writes (required for publish/propagate).
|
|
87
|
+
--version X.Y.Z Expect or assert a specific version (defaults to the source package.json).
|
|
88
|
+
--otp N npm one-time password, passed through to npm publish.
|
|
89
|
+
--root PATH Source repo for the package (defaults to $PWD).
|
|
90
|
+
--json Machine-readable output.
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Read a package's source metadata without assuming a monorepo layout. */
|
|
95
|
+
export function readSourcePackage(root, pkg) {
|
|
96
|
+
const candidates = [path.join(root, 'package.json')];
|
|
97
|
+
// A workspace source may live one level down (packages/<dir>/package.json).
|
|
98
|
+
for (const dir of ['packages', 'apps']) {
|
|
99
|
+
const base = path.join(root, dir);
|
|
100
|
+
if (!fs.existsSync(base)) continue;
|
|
101
|
+
for (const entry of fs.readdirSync(base)) {
|
|
102
|
+
candidates.push(path.join(base, entry, 'package.json'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const file of candidates) {
|
|
106
|
+
if (!fs.existsSync(file)) continue;
|
|
107
|
+
try {
|
|
108
|
+
const pkgJson = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
109
|
+
if (pkgJson.name === pkg) return { file, dir: path.dirname(file), pkgJson };
|
|
110
|
+
} catch {
|
|
111
|
+
/* ignore malformed candidates rather than failing the whole release */
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** npm registry read. This is the independent source of truth for "published". */
|
|
118
|
+
export function publishedVersion(pkg, { exec = spawnSync } = {}) {
|
|
119
|
+
const res = exec('npm', ['view', pkg, 'version'], { encoding: 'utf8', timeout: 60000 });
|
|
120
|
+
if (res.status !== 0) return { ok: false, version: null, error: (res.stderr || '').trim() || 'npm view failed' };
|
|
121
|
+
return { ok: true, version: (res.stdout || '').trim(), error: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Wait for a just-published version to become readable from the registry. */
|
|
125
|
+
export async function awaitPublished(pkg, version, { attempts = 8, delayMs = 20000, exec = spawnSync, sleep } = {}) {
|
|
126
|
+
const pause = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
127
|
+
let last = null;
|
|
128
|
+
for (let i = 0; i < attempts; i++) {
|
|
129
|
+
const read = publishedVersion(pkg, { exec });
|
|
130
|
+
last = read;
|
|
131
|
+
if (read.ok && read.version === version) return { ok: true, version: read.version, attempts: i + 1 };
|
|
132
|
+
if (i < attempts - 1) await pause(delayMs);
|
|
133
|
+
}
|
|
134
|
+
return { ok: false, version: last?.version ?? null, error: last?.error ?? `registry still reports ${last?.version}`, attempts };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Every consumer root TR knows about: the project registry plus TR_SYNC_REPOS.
|
|
139
|
+
* Used by verify so it never depends on the propagate step's own bookkeeping.
|
|
140
|
+
*/
|
|
141
|
+
export function consumerRoots(opts = {}) {
|
|
142
|
+
// Explicit roots are authoritative: passing them means "verify exactly these",
|
|
143
|
+
// which is what makes this callable from tests and from --root. Without them,
|
|
144
|
+
// fall back to the whole registered fleet plus TR_SYNC_REPOS.
|
|
145
|
+
const explicit = (opts.roots || []).filter(Boolean);
|
|
146
|
+
const source = explicit.length
|
|
147
|
+
? explicit
|
|
148
|
+
: [...listUpdateRoots(opts).map((row) => (typeof row === 'string' ? row : row.root)), ...rootsFromEnv()];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const root of source) {
|
|
152
|
+
if (!root) continue;
|
|
153
|
+
const resolved = path.resolve(root);
|
|
154
|
+
if (seen.has(resolved)) continue;
|
|
155
|
+
seen.add(resolved);
|
|
156
|
+
out.push(resolved);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Read the version a consumer actually resolves for a package (declared + installed). */
|
|
162
|
+
export function consumerState(root, pkg, version = null) {
|
|
163
|
+
const declaredFiles = [];
|
|
164
|
+
const pkgFile = path.join(root, 'package.json');
|
|
165
|
+
let declared = null;
|
|
166
|
+
if (fs.existsSync(pkgFile)) {
|
|
167
|
+
try {
|
|
168
|
+
const j = JSON.parse(fs.readFileSync(pkgFile, 'utf8'));
|
|
169
|
+
const all = { ...(j.dependencies || {}), ...(j.devDependencies || {}), ...(j.optionalDependencies || {}) };
|
|
170
|
+
declared = all[pkg] ?? null;
|
|
171
|
+
if (declared) declaredFiles.push(pkgFile);
|
|
172
|
+
} catch {
|
|
173
|
+
/* unreadable package.json is reported as unknown, not as satisfied */
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!declared) return { root, declared: null, installed: null, satisfied: false, reason: 'not-declared' };
|
|
177
|
+
|
|
178
|
+
// Resolve the version a spec pins. Handles plain versions ("0.14.4"),
|
|
179
|
+
// npm: aliases ("npm:@scope/pkg@0.14.4") and ranges. A range is satisfied by
|
|
180
|
+
// the installed version rather than by string equality.
|
|
181
|
+
const spec = String(declared).trim();
|
|
182
|
+
let want = null;
|
|
183
|
+
let range = false;
|
|
184
|
+
if (spec.startsWith('npm:')) {
|
|
185
|
+
const afterAt = spec.slice(4);
|
|
186
|
+
const idx = afterAt.lastIndexOf('@');
|
|
187
|
+
want = idx > 0 ? afterAt.slice(idx + 1) : null;
|
|
188
|
+
} else if (/^[~^]|^[<>]=?|\|\||\s-\s|\*|x$/i.test(spec)) {
|
|
189
|
+
range = true;
|
|
190
|
+
} else {
|
|
191
|
+
want = spec;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const installedCandidates = [
|
|
195
|
+
path.join(root, 'node_modules', pkg, 'package.json'),
|
|
196
|
+
// npm: aliases install under the alias directory, which is what .bin shims run.
|
|
197
|
+
...aliasDirs(root, pkg),
|
|
198
|
+
];
|
|
199
|
+
let installed = null;
|
|
200
|
+
for (const file of installedCandidates) {
|
|
201
|
+
if (!fs.existsSync(file)) continue;
|
|
202
|
+
try {
|
|
203
|
+
installed = JSON.parse(fs.readFileSync(file, 'utf8')).version ?? null;
|
|
204
|
+
if (installed) break;
|
|
205
|
+
} catch {
|
|
206
|
+
/* keep looking */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const satisfied = range ? Boolean(installed) && installed === version : Boolean(want && installed === want);
|
|
210
|
+
return {
|
|
211
|
+
root,
|
|
212
|
+
declared,
|
|
213
|
+
want,
|
|
214
|
+
range,
|
|
215
|
+
installed,
|
|
216
|
+
satisfied,
|
|
217
|
+
reason: !installed ? 'not-installed' : satisfied ? 'ok' : 'installed-mismatch',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Alias install dirs for a package, e.g. @ssss/cli -> node_modules/@ssss/cli. */
|
|
222
|
+
function aliasDirs(root, pkg) {
|
|
223
|
+
const dirs = [];
|
|
224
|
+
const nm = path.join(root, 'node_modules');
|
|
225
|
+
if (!fs.existsSync(nm)) return dirs;
|
|
226
|
+
// Known alias pair for the SSSS CLI: the alias dir is what .bin/ssss executes.
|
|
227
|
+
if (pkg === '@gregiteen/ssss-cli') {
|
|
228
|
+
dirs.push(path.join(nm, '@ssss', 'cli', 'package.json'));
|
|
229
|
+
}
|
|
230
|
+
return dirs;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function verifyConsumers(pkg, version, opts = {}) {
|
|
234
|
+
const roots = consumerRoots(opts);
|
|
235
|
+
const states = roots.map((root) => consumerState(root, pkg, version));
|
|
236
|
+
const relevant = states.filter((s) => s.declared);
|
|
237
|
+
const failing = relevant.filter((s) => !s.satisfied);
|
|
238
|
+
return {
|
|
239
|
+
pkg,
|
|
240
|
+
version,
|
|
241
|
+
roots_considered: roots.length,
|
|
242
|
+
consumers: relevant.length,
|
|
243
|
+
failing: failing.length,
|
|
244
|
+
failures: failing.map((s) => ({ root: s.root, declared: s.declared, installed: s.installed, reason: s.reason })),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runPrepublish(dir, pkgJson, { exec = spawnSync, timeoutMs } = {}) {
|
|
249
|
+
if (!pkgJson?.scripts?.prepublishOnly) return { ok: true, skipped: true };
|
|
250
|
+
const manager = fs.existsSync(path.join(dir, 'pnpm-lock.yaml')) ? 'pnpm' : 'npm';
|
|
251
|
+
const res = exec(manager, ['run', 'prepublishOnly'], { cwd: dir, encoding: 'utf8', stdio: 'pipe', timeout: timeoutMs });
|
|
252
|
+
return { ok: res.status === 0, skipped: false, output: `${res.stdout || ''}${res.stderr || ''}`.trim() };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function doPublish(dir, { otp, exec = spawnSync, timeoutMs } = {}) {
|
|
256
|
+
const args = ['publish'];
|
|
257
|
+
if (otp) args.push('--otp', otp);
|
|
258
|
+
const res = exec('npm', args, { cwd: dir, encoding: 'utf8', stdio: 'pipe', timeout: timeoutMs });
|
|
259
|
+
return { ok: res.status === 0, output: `${res.stdout || ''}${res.stderr || ''}`.trim() };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {string[]} argv full process argv (bin convention)
|
|
264
|
+
*/
|
|
265
|
+
export async function run(argv = process.argv, deps = {}) {
|
|
266
|
+
const opts = parseArgs(argv.slice(3));
|
|
267
|
+
const log = opts.json ? () => {} : (...a) => console.log(...a);
|
|
268
|
+
const emit = (obj) => { if (opts.json) console.log(JSON.stringify(obj, null, 2)); };
|
|
269
|
+
|
|
270
|
+
if (opts.help || !opts.action) {
|
|
271
|
+
printHelp();
|
|
272
|
+
if (!opts.action && !opts.help) process.exitCode = 1;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!ACTIONS.includes(opts.action)) {
|
|
276
|
+
console.error(`Unknown release action: ${opts.action}. Expected one of: ${ACTIONS.join(', ')}`);
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const report = { action: opts.action, packages: {}, ok: false, steps: [] };
|
|
282
|
+
const fail = (message) => {
|
|
283
|
+
report.error = message;
|
|
284
|
+
report.ok = false;
|
|
285
|
+
if (opts.json) emit(report); else console.error(`\n ✗ ${message}\n`);
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// propagate and verify act on the whole fleet and need no package argument.
|
|
290
|
+
if (opts.action === 'propagate') {
|
|
291
|
+
const dryRun = !opts.apply;
|
|
292
|
+
log(`\n Propagating ${DEFAULT_PACKAGES.join(' + ')} across every registered consumer${dryRun ? ' (plan)' : ''}...`);
|
|
293
|
+
const summary = await (deps.updateAllBrains || updateAllBrains)({ dryRun, force: !dryRun, skipThrottle: !dryRun });
|
|
294
|
+
report.steps.push({ step: 'propagate', dryRun, summary });
|
|
295
|
+
report.ok = (summary.failed || 0) === 0;
|
|
296
|
+
if (opts.json) { emit(report); return; }
|
|
297
|
+
log(`\n roots: ${summary.root_count ?? '—'} consumers: ${summary.consumer_count ?? '—'} failed: ${summary.failed ?? 0}`);
|
|
298
|
+
if (dryRun) log(' Run with --apply to install.\n');
|
|
299
|
+
if (!report.ok) process.exitCode = 1;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const packages = opts.packages.length ? opts.packages : DEFAULT_PACKAGES;
|
|
304
|
+
if (!packages.length) return fail('No package given.');
|
|
305
|
+
|
|
306
|
+
for (const pkg of packages) {
|
|
307
|
+
const root = opts.root || process.cwd();
|
|
308
|
+
const source = readSourcePackage(root, pkg);
|
|
309
|
+
const entry = { pkg, root, source: source?.file ?? null };
|
|
310
|
+
report.packages[pkg] = entry;
|
|
311
|
+
|
|
312
|
+
if (opts.action === 'verify') {
|
|
313
|
+
const version = opts.version || source?.pkgJson?.version;
|
|
314
|
+
if (!version) return fail(`${pkg}: no --version and no source package.json found under ${root}`);
|
|
315
|
+
entry.version = version;
|
|
316
|
+
const reg = await awaitPublished(pkg, version, { attempts: 1, delayMs: 0 });
|
|
317
|
+
entry.registry = reg;
|
|
318
|
+
const consumers = verifyConsumers(pkg, version, { roots: opts.root ? [opts.root] : [] });
|
|
319
|
+
entry.consumers = consumers;
|
|
320
|
+
if (!reg.ok) return fail(`${pkg}@${version} is not readable from the npm registry (${reg.error}).`);
|
|
321
|
+
if (consumers.failing > 0) {
|
|
322
|
+
return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers not satisfied. First: ${consumers.failures[0].root} (${consumers.failures[0].installed ?? 'not installed'}).`);
|
|
323
|
+
}
|
|
324
|
+
log(` ✓ ${pkg}@${version} verified: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!source) return fail(`${pkg}: source package.json not found under ${root}. Pass --root <repo>.`);
|
|
329
|
+
const version = opts.version || source.pkgJson.version;
|
|
330
|
+
entry.version = version;
|
|
331
|
+
if (opts.version && source.pkgJson.version !== opts.version) {
|
|
332
|
+
return fail(`${pkg}: source is ${source.pkgJson.version} but --version ${opts.version} was requested. Bump the source first.`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (opts.action === 'plan') {
|
|
336
|
+
const reg = publishedVersion(pkg);
|
|
337
|
+
entry.registry = reg;
|
|
338
|
+
entry.gates = source.pkgJson.scripts?.prepublishOnly ? 'present' : 'absent';
|
|
339
|
+
entry.alreadyPublished = reg.ok && reg.version === version;
|
|
340
|
+
log(` ${pkg}: source ${version}, registry ${reg.version ?? '—'}${entry.alreadyPublished ? ' (already published)' : ''}, prepublish gate ${entry.gates}`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (opts.action === 'publish' || opts.action === 'all') {
|
|
345
|
+
const reg = publishedVersion(pkg);
|
|
346
|
+
entry.registry_before = reg;
|
|
347
|
+
if (reg.ok && reg.version === version) {
|
|
348
|
+
log(` · ${pkg}@${version} already published — skipping publish`);
|
|
349
|
+
entry.published = 'already';
|
|
350
|
+
} else {
|
|
351
|
+
if (!opts.apply) return fail(`publish requires --apply (would publish ${pkg}@${version}).`);
|
|
352
|
+
const gate = runPrepublish(source.dir, source.pkgJson, { timeoutMs: opts.timeoutMs });
|
|
353
|
+
entry.gate = gate.ok ? 'passed' : 'failed';
|
|
354
|
+
report.steps.push({ step: 'prepublish', pkg, ok: gate.ok, skipped: gate.skipped });
|
|
355
|
+
if (!gate.ok) return fail(`${pkg}: prepublishOnly gate failed.\n${gate.output}`);
|
|
356
|
+
const pub = doPublish(source.dir, { otp: opts.otp, timeoutMs: opts.timeoutMs });
|
|
357
|
+
entry.publish_output = pub.output;
|
|
358
|
+
if (!pub.ok) return fail(`${pkg}: npm publish failed.\n${pub.output}`);
|
|
359
|
+
const confirmed = await (deps.awaitPublished || awaitPublished)(pkg, version, deps);
|
|
360
|
+
entry.registry_after = confirmed;
|
|
361
|
+
if (!confirmed.ok) return fail(`${pkg}@${version} published but the registry still reports ${confirmed.version}. Treat as NOT released.`);
|
|
362
|
+
entry.published = 'yes';
|
|
363
|
+
log(` ✓ ${pkg}@${version} published and confirmed on the registry`);
|
|
364
|
+
}
|
|
365
|
+
if (opts.action === 'publish') continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (opts.action === 'all') {
|
|
369
|
+
const dryRun = !opts.apply;
|
|
370
|
+
const summary = await (deps.updateAllBrains || updateAllBrains)({ dryRun, force: !dryRun, skipThrottle: !dryRun });
|
|
371
|
+
report.steps.push({ step: 'propagate', pkg, summary });
|
|
372
|
+
if ((summary.failed || 0) > 0) return fail(`${pkg}: propagation reported ${summary.failed} failure(s).`);
|
|
373
|
+
const consumers = verifyConsumers(pkg, version, {});
|
|
374
|
+
entry.consumers = consumers;
|
|
375
|
+
if (consumers.failing > 0) {
|
|
376
|
+
return fail(`${pkg}@${version}: ${consumers.failing}/${consumers.consumers} consumers still not satisfied after propagation.`);
|
|
377
|
+
}
|
|
378
|
+
log(` ✓ ${pkg}@${version}: registry + ${consumers.consumers}/${consumers.consumers} consumers`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
report.ok = true;
|
|
383
|
+
if (opts.json) emit(report);
|
|
384
|
+
else if (opts.action === 'plan') log('\n Plan only — nothing published or changed.\n');
|
|
385
|
+
else log('');
|
|
386
|
+
}
|
package/src/cli/remember.mjs
CHANGED
|
@@ -234,7 +234,7 @@ export default async function remember(args) {
|
|
|
234
234
|
i++;
|
|
235
235
|
}
|
|
236
236
|
} else if (arg === '--no-dedup') {
|
|
237
|
-
//
|
|
237
|
+
// Skips the duplicate guard below; remember never archives another node.
|
|
238
238
|
} else if (arg === '--allow-long') {
|
|
239
239
|
allowLong = true;
|
|
240
240
|
} else if (arg === '--universal') {
|
|
@@ -324,6 +324,24 @@ export default async function remember(args) {
|
|
|
324
324
|
node.immutable = true;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
// Hermes-grade fail-closed duplicate guard: prevent writing duplicate rules/invariants
|
|
328
|
+
if (!layerArgs.includes('--force') && !layerArgs.includes('--no-dedup')) {
|
|
329
|
+
try {
|
|
330
|
+
const { assertNoDuplicateRule } = await import('../core/duplicate-guard.mjs');
|
|
331
|
+
assertNoDuplicateRule(node, {
|
|
332
|
+
vaultDir,
|
|
333
|
+
derivedDir,
|
|
334
|
+
globalVaultDir: path.join(getBothBrains().global.brainDir, 'memory-vault'),
|
|
335
|
+
projectRoot: (project && project.projectRoot) ? project.projectRoot : process.cwd(),
|
|
336
|
+
});
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err.code === 'ERR_DUPLICATE_RULE') {
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
327
345
|
// Saving memory never optimizes or archives other canonical documents.
|
|
328
346
|
|
|
329
347
|
const vaultResult = await writeNodeValidatedAsync(node, vaultDir);
|
package/src/cli/rules.mjs
CHANGED
|
@@ -15,17 +15,49 @@ const POLICY_SLUG = 'context-policy';
|
|
|
15
15
|
const ALL_ACTIONS = [...CONTEXT_ACTIONS];
|
|
16
16
|
const DEFAULT_BUDGET = 4000;
|
|
17
17
|
|
|
18
|
+
const explicitBudget = (flags) => {
|
|
19
|
+
const explicit = Number(flags.budget);
|
|
20
|
+
return Number.isFinite(explicit) && explicit > 0 ? explicit : undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
18
23
|
/** Budget for capsule checks: --budget, else the context_budget setting, else 4000. */
|
|
19
24
|
function resolveBudget(flags) {
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
return explicitBudget(flags) ?? configuredBudget() ?? DEFAULT_BUDGET;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function configuredBudget() {
|
|
22
29
|
for (const layer of ['project', 'global']) {
|
|
23
30
|
try {
|
|
24
31
|
const value = readConfigValue('context_budget', layer);
|
|
25
32
|
if (Number.isFinite(value) && value > 0) return value;
|
|
26
33
|
} catch { /* fall through to the next layer */ }
|
|
27
34
|
}
|
|
28
|
-
return
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The budget `context` applies when none is given. */
|
|
39
|
+
async function effectiveBudget() {
|
|
40
|
+
const { CAPSULE_LIMIT } = await import('./context.mjs');
|
|
41
|
+
return Math.min(configuredBudget() ?? CAPSULE_LIMIT, CAPSULE_LIMIT);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Whether `context` can really serve each capsule: it compacts automatically, so raw size alone is not a failure (CAPSULE-R15). */
|
|
45
|
+
async function checkCompactable(capsules, budget, problems) {
|
|
46
|
+
const { loadContextNodes, compactedHeader } = await import('./context.mjs');
|
|
47
|
+
const { compactNodes } = await import('./compact.mjs');
|
|
48
|
+
const { nodes, skillsDir, derivedDirs } = loadContextNodes();
|
|
49
|
+
const header = compactedHeader('0'.repeat(64), budget);
|
|
50
|
+
for (const capsule of capsules) {
|
|
51
|
+
capsule.compacted = capsule.tokens > budget;
|
|
52
|
+
try {
|
|
53
|
+
await compactNodes({ task: '', actions: capsule.actions.split(','), budget, complete: true, rank: false, nodes, skillsDir, derivedDirs, header });
|
|
54
|
+
capsule.withinBudget = true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!error.requiredBudget) throw error;
|
|
57
|
+
capsule.withinBudget = false;
|
|
58
|
+
problems.push(`capsule for ${capsule.actions} cannot fit ${budget} tokens even compacted; it needs ${error.requiredBudget}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
function help() {
|
|
@@ -37,7 +69,7 @@ function help() {
|
|
|
37
69
|
audit [--budget N] [--top N] [--json] Capsule size per action, largest rules, curation health
|
|
38
70
|
draft [--all] [--actions a,b] [--out F] Write a policy scaffold (original text as directives) to shorten
|
|
39
71
|
apply <file> [--dry-run] [--allow-exclude] Validate a policy file and merge it into this repo's policy node
|
|
40
|
-
verify [--budget N] [--json] Exit 1 on stale/orphan curation or
|
|
72
|
+
verify [--budget N] [--json] Exit 1 on stale/orphan curation or a capsule context cannot serve even compacted; --budget checks raw size
|
|
41
73
|
prune [--apply] [--archive] [--json] Drop stale policy entries; list (or archive) expired/superseded/duplicate rules
|
|
42
74
|
|
|
43
75
|
Workflow: audit -> draft -> shorten each directive by hand (keep every operative
|
|
@@ -107,9 +139,10 @@ function parseFlags(args, valued = []) {
|
|
|
107
139
|
return { flags, rest };
|
|
108
140
|
}
|
|
109
141
|
|
|
110
|
-
function audit(args) {
|
|
142
|
+
async function audit(args) {
|
|
111
143
|
const { flags } = parseFlags(args, ['budget', 'top']);
|
|
112
|
-
const
|
|
144
|
+
const explicit = explicitBudget(flags);
|
|
145
|
+
const budget = explicit ?? await effectiveBudget();
|
|
113
146
|
const state = loadState();
|
|
114
147
|
const curated = curatedRules(state.nodes);
|
|
115
148
|
const rows = curated.filter(n => RULE_CATEGORIES.has(n.category) && n.status === 'active').map(n => {
|
|
@@ -119,7 +152,12 @@ function audit(args) {
|
|
|
119
152
|
return { id, original, current, curated: Boolean(n._directive), excluded: Boolean(n._contextExcluded) };
|
|
120
153
|
});
|
|
121
154
|
const sz = sizes(state);
|
|
122
|
-
const
|
|
155
|
+
const capsules = sz.filter(s => s.actions !== '(none)');
|
|
156
|
+
// Without --budget, `over` means what it means to `context`: not servable even compacted.
|
|
157
|
+
const problems = [];
|
|
158
|
+
if (explicit) for (const s of capsules) s.withinBudget = s.tokens <= budget;
|
|
159
|
+
else await checkCompactable(capsules, budget, problems);
|
|
160
|
+
const over = capsules.filter(s => !s.withinBudget);
|
|
123
161
|
const report = {
|
|
124
162
|
policy: state.health, rules: rows.length, curated: rows.filter(r => r.curated).length,
|
|
125
163
|
excluded: rows.filter(r => r.excluded).length, stale: state.stale, orphan: state.orphan,
|
|
@@ -131,9 +169,11 @@ function audit(args) {
|
|
|
131
169
|
if (state.stale.length) console.log(`Stale curation (rule edited since; full text in use): ${state.stale.join(', ')}`);
|
|
132
170
|
if (state.orphan.length) console.log(`Orphan curation (rule gone): ${state.orphan.join(', ')}`);
|
|
133
171
|
console.log(`\nCapsule tokens by action (budget ${budget}):`);
|
|
134
|
-
for (const s of sz) console.log(` ${s.
|
|
172
|
+
for (const s of sz) console.log(` ${s.withinBudget === false ? 'OVER' : s.compacted ? 'comp' : 'ok '} ${String(s.tokens).padStart(6)} ${s.actions} (${s.count} rules)`);
|
|
173
|
+
if (sz.some(s => s.compacted)) console.log(' comp = larger than the budget; `context` compacts it automatically and every rule still applies');
|
|
135
174
|
console.log('\nLargest rules (current/original tokens):');
|
|
136
175
|
for (const r of report.largest) console.log(` ${String(r.current).padStart(5)}/${String(r.original).padEnd(5)} ${r.id}${r.curated ? ' (curated)' : ''}`);
|
|
176
|
+
for (const p of problems) console.log(`\n${p}`);
|
|
137
177
|
if (over.length) console.log('\nOver budget: run `total-recall rules draft --out policy.json`, shorten the directives, then `rules apply policy.json`.');
|
|
138
178
|
}
|
|
139
179
|
|
|
@@ -224,20 +264,22 @@ async function apply(args) {
|
|
|
224
264
|
console.log(`Applied ${Object.keys(clean).length} entries (${Object.keys(merged.rules).length} total). Run \`total-recall rules verify\` after the vault recompiles.`);
|
|
225
265
|
}
|
|
226
266
|
|
|
227
|
-
function verify(args) {
|
|
267
|
+
async function verify(args) {
|
|
228
268
|
const { flags } = parseFlags(args, ['budget']);
|
|
229
|
-
const
|
|
269
|
+
const explicit = explicitBudget(flags);
|
|
270
|
+
const budget = explicit ?? await effectiveBudget();
|
|
230
271
|
const state = loadState();
|
|
231
272
|
const problems = [];
|
|
232
273
|
if (state.health === 'ambiguous' || state.health === 'malformed') problems.push(`policy is ${state.health}; curation is ignored`);
|
|
233
274
|
for (const id of state.stale) problems.push(`stale curation: ${id} (rule edited; full text in use)`);
|
|
234
275
|
for (const id of state.orphan) problems.push(`orphan curation: ${id} (rule no longer exists)`);
|
|
235
276
|
const capsules = sizes(state).filter(s => s.actions !== '(none)').map(s => ({ actions: s.actions, tokens: s.tokens, rules: s.count, withinBudget: s.tokens <= budget }));
|
|
236
|
-
for (const s of capsules) if (!s.withinBudget) problems.push(`capsule for ${s.actions} is ${s.tokens} tokens > ${budget}`);
|
|
277
|
+
if (explicit) for (const s of capsules) { if (!s.withinBudget) problems.push(`capsule for ${s.actions} is ${s.tokens} tokens > ${budget}`); }
|
|
278
|
+
else await checkCompactable(capsules, budget, problems);
|
|
237
279
|
const ok = problems.length === 0;
|
|
238
280
|
if (flags.json) { console.log(JSON.stringify({ ok, health: state.health, budget, capsules, problems }, null, 1)); if (!ok) process.exitCode = 1; return; }
|
|
239
281
|
if (!ok) { for (const p of problems) console.log(`✖ ${p}`); process.exitCode = 1; return; }
|
|
240
|
-
console.log(`✔ policy ${state.health}; every action capsule within ${budget} tokens`);
|
|
282
|
+
console.log(`✔ policy ${state.health}; every action capsule within ${budget} tokens${explicit ? '' : ' (compacted where needed)'}`);
|
|
241
283
|
}
|
|
242
284
|
|
|
243
285
|
async function prune(args) {
|
package/src/cli/startup.mjs
CHANGED
|
@@ -41,7 +41,7 @@ export default async function startup(args = []) {
|
|
|
41
41
|
const availH = harnesses.filter(h => h.available);
|
|
42
42
|
const readyH = availH.filter(h => h.authed);
|
|
43
43
|
if (availH.length) {
|
|
44
|
-
console.log(`Harnesses: ${readyH.length}/${availH.length} ready (${readyH.map(h => `${h.id}: 5h ${h.rolling5hRemainingPct}`).join(', ')})`);
|
|
44
|
+
console.log(`Harnesses: ${readyH.length}/${availH.length} ready (${readyH.map(h => `${h.id}: 5h ${h.rolling5hRemainingPct} [${h.enforcement || 'enforced'}]`).join(', ')})`);
|
|
45
45
|
}
|
|
46
46
|
for (const action of (report.actions || [])) console.log(`Action: ${action.component} ${action.action} (${action.accepted === true ? 'accepted; see observed readiness' : action.reason || 'failed'})`);
|
|
47
47
|
}
|
package/src/cli/status.mjs
CHANGED
|
@@ -78,7 +78,16 @@ function projectionsStatus(clients, instructionsMtime) {
|
|
|
78
78
|
fresh = projMtime >= instructionsMtime;
|
|
79
79
|
} catch { fresh = false; }
|
|
80
80
|
}
|
|
81
|
-
return {
|
|
81
|
+
return {
|
|
82
|
+
name,
|
|
83
|
+
label: info.label,
|
|
84
|
+
mode: info.mode,
|
|
85
|
+
enforcement: info.enforcement || 'advisory',
|
|
86
|
+
enforcementReason: info.enforcementReason || null,
|
|
87
|
+
connectedAt: info.connectedAt,
|
|
88
|
+
projectionPath: info.projectionPath,
|
|
89
|
+
fresh
|
|
90
|
+
};
|
|
82
91
|
});
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -312,7 +321,8 @@ export default async function statusCmd(args) {
|
|
|
312
321
|
if (c.mode === 'file' || c.mode === 'symlink') {
|
|
313
322
|
freshnessTag = c.fresh === null ? ' (no projection)' : c.fresh ? ' ✅ fresh' : ' ⚠️ stale — run: total-recall connect ' + c.name;
|
|
314
323
|
}
|
|
315
|
-
|
|
324
|
+
const enfTag = c.enforcement ? ` [${c.enforcement}]` : '';
|
|
325
|
+
console.log(` ${c.label.padEnd(18)} [${c.mode}]${enfTag}${freshnessTag}`);
|
|
316
326
|
}
|
|
317
327
|
} else {
|
|
318
328
|
console.log(' Connected clients: (none — run total-recall connect <client>)');
|
|
@@ -21,18 +21,24 @@ const GATE_MATCHER = 'Bash|RemoteTrigger|CronCreate|mcp__scheduled-tasks__.*|mcp
|
|
|
21
21
|
|
|
22
22
|
const SKILL_COMMAND = 'f="$CLAUDE_PROJECT_DIR/.agent/skills/total-recall/SKILL.md"; [ -f "$f" ] || f="$HOME/.agent/skills/total-recall/SKILL.md"; if [ -f "$f" ]; then echo "MANDATORY CONTEXT: the Total Recall skill ($f), loaded by a SessionStart hook. Follow it; use the total-recall CLI as documented here."; cat "$f"; fi';
|
|
23
23
|
|
|
24
|
+
export const PREFLIGHT_GATE_COMMAND = 'total-recall preflight gate --harness claude';
|
|
25
|
+
export const PREFLIGHT_REMIND_COMMAND = 'total-recall preflight remind';
|
|
26
|
+
export const STOPGAP_FILE = 'total-recall-preflight.mjs';
|
|
27
|
+
|
|
24
28
|
const hasCommand = (entries, needle) => (entries || []).some((e) => (e.hooks || []).some((h) => String(h.command || h.statusMessage || '').includes(needle)));
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* @param {{ home?: string, dryRun?: boolean }} [opts]
|
|
28
|
-
* @returns {{ gate: string, settings: string, added: string[] }}
|
|
32
|
+
* @returns {{ gate: string, settings: string, added: string[], removed: string[] }}
|
|
29
33
|
*/
|
|
30
34
|
export function installClaudeCodeHooks(opts = {}) {
|
|
31
35
|
const home = opts.home || os.homedir();
|
|
32
36
|
const claudeDir = path.join(home, '.claude');
|
|
33
37
|
const hookPath = path.join(claudeDir, 'hooks', GATE_FILE);
|
|
38
|
+
const stopgapPath = path.join(claudeDir, 'hooks', STOPGAP_FILE);
|
|
34
39
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
35
40
|
const added = [];
|
|
41
|
+
const removed = [];
|
|
36
42
|
|
|
37
43
|
const template = fs.readFileSync(GATE_TEMPLATE, 'utf8');
|
|
38
44
|
const current = fs.existsSync(hookPath) ? fs.readFileSync(hookPath, 'utf8') : null;
|
|
@@ -46,6 +52,28 @@ export function installClaudeCodeHooks(opts = {}) {
|
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
const hooks = settings.hooks || (settings.hooks = {});
|
|
55
|
+
|
|
56
|
+
// Clean up 2026-11-02 stopgap entries from settings and filesystem (ENFORCE-R14)
|
|
57
|
+
for (const event of ['PreToolUse', 'UserPromptSubmit', 'SessionStart', 'PostToolUse']) {
|
|
58
|
+
if (Array.isArray(hooks[event])) {
|
|
59
|
+
const beforeLen = hooks[event].length;
|
|
60
|
+
hooks[event] = hooks[event].filter(entry => {
|
|
61
|
+
const sub = entry.hooks || [];
|
|
62
|
+
return !sub.some(h => String(h.command || '').includes(STOPGAP_FILE));
|
|
63
|
+
});
|
|
64
|
+
if (hooks[event].length < beforeLen) {
|
|
65
|
+
removed.push(`stopgap ${event} hook`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!opts.dryRun && fs.existsSync(stopgapPath)) {
|
|
71
|
+
try {
|
|
72
|
+
fs.unlinkSync(stopgapPath);
|
|
73
|
+
removed.push('stopgap hook script');
|
|
74
|
+
} catch { /* ignore */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
const gateCommand = `node "$HOME/.claude/hooks/${GATE_FILE}"`;
|
|
50
78
|
if (!hasCommand(hooks.SessionStart, SKILL_MARK) && !hasCommand(hooks.SessionStart, 'total-recall/SKILL.md')) {
|
|
51
79
|
(hooks.SessionStart ||= []).push({ hooks: [{ type: 'command', command: SKILL_COMMAND, timeout: 10, statusMessage: SKILL_MARK }] });
|
|
@@ -60,11 +88,21 @@ export function installClaudeCodeHooks(opts = {}) {
|
|
|
60
88
|
added.push('PostToolUse recorder');
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
|
|
91
|
+
// Managed preflight enforcement hooks (ENFORCE-R1, ENFORCE-R2, ENFORCE-R14)
|
|
92
|
+
if (!hasCommand(hooks.PreToolUse, 'preflight gate --harness claude')) {
|
|
93
|
+
(hooks.PreToolUse ||= []).push({ matcher: '*', hooks: [{ type: 'command', command: PREFLIGHT_GATE_COMMAND, timeout: 5 }] });
|
|
94
|
+
added.push('managed PreToolUse preflight gate');
|
|
95
|
+
}
|
|
96
|
+
if (!hasCommand(hooks.UserPromptSubmit, 'preflight remind')) {
|
|
97
|
+
(hooks.UserPromptSubmit ||= []).push({ hooks: [{ type: 'command', command: PREFLIGHT_REMIND_COMMAND, timeout: 5 }] });
|
|
98
|
+
added.push('managed UserPromptSubmit preflight reminder');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!opts.dryRun && (added.length || removed.length)) {
|
|
64
102
|
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
65
103
|
if (current !== template) fs.writeFileSync(hookPath, template, { mode: 0o755 });
|
|
66
104
|
if (fs.existsSync(settingsPath)) fs.copyFileSync(settingsPath, `${settingsPath}.bak-total-recall`);
|
|
67
105
|
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
68
106
|
}
|
|
69
|
-
return { gate: hookPath, settings: settingsPath, added };
|
|
107
|
+
return { gate: hookPath, settings: settingsPath, added, removed };
|
|
70
108
|
}
|