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
package/src/cli/agent.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { listAgents, spawnAgent, killAgent, getAgentLogs } from '../core/agent-manager.mjs';
|
|
4
4
|
import { detectHarnesses } from '../core/meta-harness.mjs';
|
|
5
|
+
import { checkReceipt, findRoot, ACK_COMMAND } from '../core/preflight-receipt.mjs';
|
|
5
6
|
|
|
6
7
|
function printHelp() {
|
|
7
8
|
console.log(`
|
|
@@ -33,7 +34,7 @@ Auth:
|
|
|
33
34
|
else from the Total Recall secret store, so SSH/launchd sessions without a login keychain work.
|
|
34
35
|
|
|
35
36
|
Available Harnesses:
|
|
36
|
-
agy, claude, codex,
|
|
37
|
+
agy, claude, codex, dsh, hermes, openclaw
|
|
37
38
|
|
|
38
39
|
Examples:
|
|
39
40
|
npx total-recall agent list
|
|
@@ -144,6 +145,16 @@ export async function run(argv) {
|
|
|
144
145
|
process.exit(1);
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
if (!targetNode) {
|
|
149
|
+
const root = findRoot(cwd || process.cwd());
|
|
150
|
+
const receipt = checkReceipt(root);
|
|
151
|
+
if (!receipt.ok) {
|
|
152
|
+
console.error(`\n❌ Agent spawn refused by TR-PREFLIGHT gate: ${receipt.problems[0] || 'check failed'} (directory: ${root}).`);
|
|
153
|
+
console.error(` Read the complete context capsule, then run: ${ACK_COMMAND}\n`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
const nodeStr = targetNode ? ` on mesh node \x1b[1;33m${targetNode}\x1b[0m` : '';
|
|
148
159
|
if (!isJson) console.log(`\n🚀 Spawning agent on harness: \x1b[1;36m${harness}\x1b[0m${nodeStr}...`);
|
|
149
160
|
try {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { parseLayerFlag } from './agent-dir.mjs';
|
|
2
|
+
import { loadContextNodes } from './context.mjs';
|
|
3
|
+
import { curatedRules, selectRules } from '../core/context-policy.mjs';
|
|
4
|
+
import { compactRules } from '../core/context-compactor.mjs';
|
|
5
|
+
import { cosineSimilarity, getEmbedding, loadEmbeddingsIndex } from '../core/embeddings.mjs';
|
|
6
|
+
import { legacyRuleContributions } from '../core/surface.mjs';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_BUDGET = 4000;
|
|
9
|
+
const EMBED_TIMEOUT_MS = 8000;
|
|
10
|
+
|
|
11
|
+
const USAGE = `Usage: total-recall compact "task" [--budget 4000] [--action edit,test] [--expand slug,slug] [--format text|json] [--project|--global]
|
|
12
|
+
Fits the applicable rules into --budget estimated tokens, ranked against the task. Rules keep their own words: full text, a leading excerpt, or a slug under Referenced. Read any of them in full with --expand. The output is partial by design; \`total-recall context\` prints the complete capsule.`;
|
|
13
|
+
|
|
14
|
+
function parseArgs(args) {
|
|
15
|
+
const options = { budget: DEFAULT_BUDGET, actions: [], expand: [], format: 'text' };
|
|
16
|
+
for (let i = 1; i < args.length; i++) {
|
|
17
|
+
const flag = args[i];
|
|
18
|
+
const value = args[++i];
|
|
19
|
+
if (flag === '--budget') options.budget = Number(value);
|
|
20
|
+
else if (flag === '--action') options.actions = value?.split(',').filter(Boolean) || [];
|
|
21
|
+
else if (flag === '--expand') options.expand = value?.split(',').filter(Boolean) || [];
|
|
22
|
+
else if (flag === '--format' && ['json', 'text'].includes(value)) options.format = value;
|
|
23
|
+
else throw new Error(`Unknown compact option: ${flag}`);
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isFinite(options.budget) || options.budget < 1) throw new Error('--budget must be a positive number of tokens');
|
|
26
|
+
return options;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loadVectors(derivedDirs) {
|
|
30
|
+
const vectors = {};
|
|
31
|
+
for (const dir of derivedDirs) {
|
|
32
|
+
try {
|
|
33
|
+
for (const [slug, entry] of Object.entries(loadEmbeddingsIndex(dir))) vectors[slug] ??= entry.embedding;
|
|
34
|
+
} catch { /* a brain without an index simply contributes no vectors */ }
|
|
35
|
+
}
|
|
36
|
+
return vectors;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function embedTask(task) {
|
|
40
|
+
let timer;
|
|
41
|
+
const timeout = new Promise(resolve => { timer = setTimeout(() => resolve(null), EMBED_TIMEOUT_MS); });
|
|
42
|
+
try {
|
|
43
|
+
return await Promise.race([getEmbedding(task), timeout]);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
} finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function lexicalScore(terms, text) {
|
|
52
|
+
if (!terms.length) return 0;
|
|
53
|
+
const haystack = text.toLowerCase();
|
|
54
|
+
return terms.filter(term => haystack.includes(term)).length / terms.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Rank the applicable rules against the task and fit them into `budget`. Shared by `compact` and `context`. */
|
|
58
|
+
export async function compactNodes({ task, actions = [], budget, expand = [], complete = false, rank = true, nodes, skillsDir, derivedDirs, header }) {
|
|
59
|
+
const rules = selectRules(curatedRules(nodes), { actions, projectRoot: process.cwd() });
|
|
60
|
+
const vectors = loadVectors(derivedDirs);
|
|
61
|
+
const taskVector = rank ? await embedTask(task) : null;
|
|
62
|
+
const terms = String(task).toLowerCase().match(/[\p{L}\p{N}_-]{3,}/gu) || [];
|
|
63
|
+
const semantic = Boolean(taskVector);
|
|
64
|
+
|
|
65
|
+
const score = (slug, text) => semantic && vectors[slug] ? cosineSimilarity(taskVector, vectors[slug]) : lexicalScore(terms, text);
|
|
66
|
+
const items = rules.map(node => {
|
|
67
|
+
const body = (node._directive || node.body || node.content || '').trim();
|
|
68
|
+
return {
|
|
69
|
+
slug: node.slug, body, priority: node.priority, importance: node.importance,
|
|
70
|
+
text: node._directive ? `[${node.slug}] ${node._directive}` : `## ${node.title || node.slug} [${node.slug}]\n\n${body}`,
|
|
71
|
+
vector: vectors[node.slug], score: score(node.slug, `${node.title || ''} ${body}`),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
for (const { id, text } of legacyRuleContributions(skillsDir)) {
|
|
75
|
+
items.push({ slug: id, body: text, text, score: lexicalScore(terms, text) });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ranking = semantic ? 'semantic' : 'lexical';
|
|
79
|
+
return { ranking, ...compactRules(items, { budget, expand, complete, header: (counts, tokens) => header(counts, tokens, ranking) }) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default async function compact(args = []) {
|
|
83
|
+
if (!args.length || args.includes('--help') || args.includes('-h')) {
|
|
84
|
+
console.log(USAGE);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { layer, remainingArgs } = parseLayerFlag(args);
|
|
88
|
+
const task = remainingArgs[0];
|
|
89
|
+
const { budget, actions, expand, format } = parseArgs(remainingArgs);
|
|
90
|
+
const { nodes, skillsDir, derivedDirs } = loadContextNodes(layer);
|
|
91
|
+
const header = (counts, tokens, ranking) => [
|
|
92
|
+
`compacted:true tokens:${tokens} budget:${budget} rules:${counts.rules} full:${counts.full} summarized:${counts.summarized} referenced:${counts.referenced} duplicate:${counts.duplicate} omitted:${counts.omitted} ranking:${ranking}`,
|
|
93
|
+
'PARTIAL: read any rule in full with --expand <slug>; total-recall context prints the complete capsule.',
|
|
94
|
+
].join('\n');
|
|
95
|
+
|
|
96
|
+
let result;
|
|
97
|
+
try {
|
|
98
|
+
result = await compactNodes({ task, actions, budget, expand, nodes, skillsDir, derivedDirs, header });
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.log(error.requiredBudget ? `compacted:false reason:budget-too-small budget:${budget} required_budget:${error.requiredBudget}` : `compacted:false reason:${error.message}`);
|
|
101
|
+
process.exitCode = 2;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const { context, counts, tokens, tiers, ranking } = result;
|
|
105
|
+
console.log(format === 'json'
|
|
106
|
+
? JSON.stringify({ compacted: true, ranking, budget, tokens, counts, tiers, context })
|
|
107
|
+
: context);
|
|
108
|
+
}
|
package/src/cli/connect.mjs
CHANGED
|
@@ -14,18 +14,24 @@ export const CLIENTS = {
|
|
|
14
14
|
vscode: {
|
|
15
15
|
label: 'VS Code Copilot',
|
|
16
16
|
mode: 'file',
|
|
17
|
+
enforcement: 'advisory',
|
|
18
|
+
enforcementReason: 'No tool hook mechanism; rules provided via copilot-instructions.md',
|
|
17
19
|
target: path.join('.github', 'copilot-instructions.md'),
|
|
18
20
|
render: instructions => `<!-- Total Recall — auto-generated by \`npx total-recall connect vscode\` -->\n<!-- Do not edit; re-run connect to refresh -->\n\n${instructions}`
|
|
19
21
|
},
|
|
20
22
|
pi: {
|
|
21
23
|
label: 'Pi Coding Agent',
|
|
22
24
|
mode: 'global-symlink',
|
|
25
|
+
enforcement: 'enforced (unverified live)',
|
|
26
|
+
enforcementReason: 'tool_call extension blocks unreceipted tool calls with exit 2',
|
|
23
27
|
globalTarget: path.join(os.homedir(), '.pi', 'agent', 'AGENTS.md'),
|
|
24
28
|
localTarget: 'AGENTS.md',
|
|
25
29
|
},
|
|
26
30
|
hermes: {
|
|
27
31
|
label: 'Hermes Agent',
|
|
28
32
|
mode: 'symlink',
|
|
33
|
+
enforcement: 'enforced',
|
|
34
|
+
enforcementReason: 'pre_tool_call hook denies unreceipted tool calls with exit 2',
|
|
29
35
|
// Hermes loads the AGENTS.md chain (git root to cwd) as context files.
|
|
30
36
|
// ~/.hermes/memories/MEMORY.md is the agent's own notes store, read only
|
|
31
37
|
// from HERMES_HOME; Total Recall never writes it.
|
|
@@ -35,6 +41,8 @@ export const CLIENTS = {
|
|
|
35
41
|
'hermes-agent': {
|
|
36
42
|
label: 'Hermes Agent',
|
|
37
43
|
mode: 'symlink',
|
|
44
|
+
enforcement: 'enforced',
|
|
45
|
+
enforcementReason: 'pre_tool_call hook denies unreceipted tool calls with exit 2',
|
|
38
46
|
// Hermes loads the AGENTS.md chain (git root to cwd) as context files.
|
|
39
47
|
// ~/.hermes/memories/MEMORY.md is the agent's own notes store, read only
|
|
40
48
|
// from HERMES_HOME; Total Recall never writes it.
|
|
@@ -44,6 +52,8 @@ export const CLIENTS = {
|
|
|
44
52
|
dsh: {
|
|
45
53
|
label: 'DeepSeek Harness (dsh)',
|
|
46
54
|
mode: 'dsh',
|
|
55
|
+
enforcement: 'enforced',
|
|
56
|
+
enforcementReason: 'PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
47
57
|
globalMemoryPath: path.join(os.homedir(), '.dsh', 'memory', 'MEMORY.md'),
|
|
48
58
|
globalUserPath: path.join(os.homedir(), '.dsh', 'memory', 'user.md'),
|
|
49
59
|
localTarget: 'AGENTS.md',
|
|
@@ -60,6 +70,8 @@ export const CLIENTS = {
|
|
|
60
70
|
'deepseek-harness': {
|
|
61
71
|
label: 'DeepSeek Harness',
|
|
62
72
|
mode: 'dsh',
|
|
73
|
+
enforcement: 'enforced',
|
|
74
|
+
enforcementReason: 'PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
63
75
|
globalMemoryPath: path.join(os.homedir(), '.dsh', 'memory', 'MEMORY.md'),
|
|
64
76
|
globalUserPath: path.join(os.homedir(), '.dsh', 'memory', 'user.md'),
|
|
65
77
|
localTarget: 'AGENTS.md',
|
|
@@ -69,12 +81,16 @@ export const CLIENTS = {
|
|
|
69
81
|
openclaw: {
|
|
70
82
|
label: 'OpenClaw',
|
|
71
83
|
mode: 'openclaw',
|
|
84
|
+
enforcement: 'enforced',
|
|
85
|
+
enforcementReason: 'before_tool_call hook denies unreceipted tool calls with exit 2',
|
|
72
86
|
memoryTarget: 'MEMORY.md',
|
|
73
87
|
agentsTarget: 'AGENTS.md',
|
|
74
88
|
},
|
|
75
89
|
cursor: {
|
|
76
90
|
label: 'Cursor',
|
|
77
91
|
mode: 'file',
|
|
92
|
+
enforcement: 'enforced (unverified live)',
|
|
93
|
+
enforcementReason: 'hooks.json failClosed PreToolUse hook denies unreceipted tool calls',
|
|
78
94
|
target: path.join('.cursor', 'rules', 'total-recall.mdc'),
|
|
79
95
|
render: instructions => [
|
|
80
96
|
'---',
|
|
@@ -89,6 +105,8 @@ export const CLIENTS = {
|
|
|
89
105
|
cline: {
|
|
90
106
|
label: 'Cline',
|
|
91
107
|
mode: 'file',
|
|
108
|
+
enforcement: 'enforced (unverified live)',
|
|
109
|
+
enforcementReason: 'beforeTool hook cancels unreceipted tool calls',
|
|
92
110
|
// Cline uses a .clinerules/ directory (primary); plain markdown, no frontmatter.
|
|
93
111
|
target: path.join('.clinerules', 'total-recall.md'),
|
|
94
112
|
render: instructions => instructions,
|
|
@@ -98,6 +116,8 @@ export const CLIENTS = {
|
|
|
98
116
|
'claude-code': {
|
|
99
117
|
label: 'Claude Code',
|
|
100
118
|
mode: 'symlink',
|
|
119
|
+
enforcement: 'enforced',
|
|
120
|
+
enforcementReason: 'PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
101
121
|
target: 'CLAUDE.md',
|
|
102
122
|
writeSlashCommands: true,
|
|
103
123
|
installHooks: true,
|
|
@@ -108,6 +128,8 @@ export const CLIENTS = {
|
|
|
108
128
|
codex: {
|
|
109
129
|
label: 'Codex',
|
|
110
130
|
mode: 'symlink',
|
|
131
|
+
enforcement: 'enforced',
|
|
132
|
+
enforcementReason: 'hooks.json PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
111
133
|
target: 'AGENTS.md',
|
|
112
134
|
// Codex discovers repository skills in <project>/.agents/skills.
|
|
113
135
|
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
|
|
@@ -115,6 +137,8 @@ export const CLIENTS = {
|
|
|
115
137
|
grok: {
|
|
116
138
|
label: 'Grok Build (xAI)',
|
|
117
139
|
mode: 'symlink',
|
|
140
|
+
enforcement: 'enforced (unverified live)',
|
|
141
|
+
enforcementReason: 'hooks/ PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
118
142
|
target: 'AGENTS.md',
|
|
119
143
|
// Grok discovers repository skills in <project>/.agents/skills.
|
|
120
144
|
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
|
|
@@ -122,34 +146,46 @@ export const CLIENTS = {
|
|
|
122
146
|
muse: {
|
|
123
147
|
label: 'Muse Code',
|
|
124
148
|
mode: 'symlink',
|
|
149
|
+
enforcement: 'advisory',
|
|
150
|
+
enforcementReason: 'No tool hook mechanism; rules provided via context files',
|
|
125
151
|
target: 'AGENTS.md'
|
|
126
152
|
},
|
|
127
153
|
replit: {
|
|
128
154
|
label: 'Replit Agent',
|
|
129
155
|
mode: 'file',
|
|
156
|
+
enforcement: 'advisory',
|
|
157
|
+
enforcementReason: 'No tool hook mechanism; rules provided via context files',
|
|
130
158
|
target: 'replit.md',
|
|
131
159
|
render: instructions => `<!-- Total Recall — auto-generated by \`npx total-recall connect replit\` -->\n<!-- Do not edit; re-run connect to refresh -->\n\n${instructions}`
|
|
132
160
|
},
|
|
133
161
|
lovable: {
|
|
134
162
|
label: 'Lovable',
|
|
135
163
|
mode: 'symlink',
|
|
164
|
+
enforcement: 'advisory',
|
|
165
|
+
enforcementReason: 'No tool hook mechanism; rules provided via context files',
|
|
136
166
|
target: 'AGENTS.md'
|
|
137
167
|
},
|
|
138
168
|
windsurf: {
|
|
139
169
|
label: 'Windsurf / Devin Desktop',
|
|
140
170
|
mode: 'file',
|
|
171
|
+
enforcement: 'enforced (unverified live)',
|
|
172
|
+
enforcementReason: 'hooks.json PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
141
173
|
target: path.join('.windsurf', 'rules', 'total-recall.md'),
|
|
142
174
|
render: instructions => instructions
|
|
143
175
|
},
|
|
144
176
|
devin: {
|
|
145
177
|
label: 'Devin Desktop',
|
|
146
178
|
mode: 'file',
|
|
179
|
+
enforcement: 'enforced (unverified live)',
|
|
180
|
+
enforcementReason: 'hooks.json PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
147
181
|
target: path.join('.devin', 'rules', 'total-recall.md'),
|
|
148
182
|
render: instructions => instructions
|
|
149
183
|
},
|
|
150
184
|
antigravity: {
|
|
151
185
|
label: 'Antigravity',
|
|
152
186
|
mode: 'symlink',
|
|
187
|
+
enforcement: 'enforced',
|
|
188
|
+
enforcementReason: 'PreToolUse hook denies unreceipted tool calls with exit 2',
|
|
153
189
|
target: '.agents/rules/AGENTS.md',
|
|
154
190
|
// Antigravity reads workspace skills from .agents/skills and exposes native /name invocation
|
|
155
191
|
skillsProjection: { scope: 'project', dir: path.join('.agents', 'skills') }
|
|
@@ -157,6 +193,8 @@ export const CLIENTS = {
|
|
|
157
193
|
aider: {
|
|
158
194
|
label: 'Aider',
|
|
159
195
|
mode: 'file',
|
|
196
|
+
enforcement: 'dispatch-only',
|
|
197
|
+
enforcementReason: 'No pre-tool hooks; preflight enforced when invoked via Total Recall dispatch',
|
|
160
198
|
target: '.aider.rules.md',
|
|
161
199
|
render: instructions => instructions,
|
|
162
200
|
after: [
|
|
@@ -169,17 +207,23 @@ export const CLIENTS = {
|
|
|
169
207
|
'http-api': {
|
|
170
208
|
label: 'HTTP host app',
|
|
171
209
|
mode: 'api',
|
|
210
|
+
enforcement: 'advisory',
|
|
211
|
+
enforcementReason: 'HTTP API integration; caller enforces preflight',
|
|
172
212
|
// Any HTTP host: OpenAI-compatible API + PAT. Not tied to a product repo.
|
|
173
213
|
},
|
|
174
214
|
obsidian: {
|
|
175
215
|
label: 'Obsidian',
|
|
176
216
|
mode: 'vault',
|
|
217
|
+
enforcement: 'advisory',
|
|
218
|
+
enforcementReason: 'Direct vault projection; no agent execution harness',
|
|
177
219
|
// Symlinks memory-vault into the Obsidian vault as "Total Recall/"
|
|
178
220
|
folderName: 'Total Recall'
|
|
179
221
|
},
|
|
180
222
|
generic: {
|
|
181
223
|
label: 'Generic OpenAI-Compatible Client',
|
|
182
|
-
mode: 'api'
|
|
224
|
+
mode: 'api',
|
|
225
|
+
enforcement: 'advisory',
|
|
226
|
+
enforcementReason: 'Generic client; no native pre-tool enforcement'
|
|
183
227
|
}
|
|
184
228
|
};
|
|
185
229
|
|
|
@@ -358,6 +402,8 @@ function registerClient(brainDir, clientName, preset, projectionPath) {
|
|
|
358
402
|
registry.clients[clientName] = {
|
|
359
403
|
label: preset.label,
|
|
360
404
|
mode: preset.mode,
|
|
405
|
+
enforcement: preset.enforcement || 'advisory',
|
|
406
|
+
enforcementReason: preset.enforcementReason || null,
|
|
361
407
|
projectionPath: projectionPath || null,
|
|
362
408
|
connectedAt: new Date().toISOString()
|
|
363
409
|
};
|
|
@@ -755,12 +801,18 @@ export default async function connect(args) {
|
|
|
755
801
|
client: opts.client,
|
|
756
802
|
label: preset.label,
|
|
757
803
|
mode: preset.mode,
|
|
804
|
+
enforcement: preset.enforcement || 'advisory',
|
|
805
|
+
enforcement_reason: preset.enforcementReason || null,
|
|
758
806
|
brain_config_path: brainConfigPath,
|
|
759
807
|
api: details,
|
|
760
808
|
projection: null,
|
|
761
809
|
notes: []
|
|
762
810
|
};
|
|
763
811
|
|
|
812
|
+
if (preset.enforcement === 'advisory' || preset.enforcement === 'dispatch-only') {
|
|
813
|
+
result.notes.push(` ℹ️ Enforcement tier: ${preset.enforcement} — ${preset.enforcementReason}`);
|
|
814
|
+
}
|
|
815
|
+
|
|
764
816
|
if (preset.mode === 'file') {
|
|
765
817
|
const instructions = readInstructions(cwd, agentDir);
|
|
766
818
|
if (!instructions) {
|
|
@@ -949,6 +1001,9 @@ export default async function connect(args) {
|
|
|
949
1001
|
}
|
|
950
1002
|
|
|
951
1003
|
console.log(`\n Total Recall connected: ${preset.label}`);
|
|
1004
|
+
if (preset.enforcement) {
|
|
1005
|
+
console.log(` Enforcement: ${preset.enforcement} (${preset.enforcementReason || ''})`);
|
|
1006
|
+
}
|
|
952
1007
|
if (result.projection) {
|
|
953
1008
|
const action = result.projection.action;
|
|
954
1009
|
const target = result.projection.targetPath;
|
package/src/cli/context.mjs
CHANGED
|
@@ -2,15 +2,64 @@ import path from 'node:path';
|
|
|
2
2
|
import { getBothBrains, parseLayerFlag } from './agent-dir.mjs';
|
|
3
3
|
import { getNodes } from '../core/vault-cache.mjs';
|
|
4
4
|
import { mergeGlobalRuleNodes, legacyRuleContributions } from '../core/surface.mjs';
|
|
5
|
-
import { compileContext, capsuleResponse, renderCapsule } from '../core/context-compiler.mjs';
|
|
5
|
+
import { compileContext, capsuleResponse, renderCapsule, PREFLIGHT_NOTICE } from '../core/context-compiler.mjs';
|
|
6
6
|
import { isBrainEnabled } from '../core/brain-registry.mjs';
|
|
7
7
|
import { surfaceInputsHash } from '../core/command-surface.mjs';
|
|
8
8
|
import { readConfigValue } from './config.mjs';
|
|
9
9
|
import { recordCapsule } from '../core/context-telemetry.mjs';
|
|
10
10
|
|
|
11
|
+
/** About 28 KB: harnesses save output over about 30 KB to a file instead of showing it, which agents then cannot read (audit C-005, F-019). */
|
|
12
|
+
export const CAPSULE_LIMIT = 7000;
|
|
13
|
+
const JSON_ESCAPE_SHARE = 0.97;
|
|
14
|
+
const JSON_WRAPPER_TOKENS = 128;
|
|
15
|
+
|
|
16
|
+
/** The merged rule nodes a capsule draws from, for the selected layer. */
|
|
17
|
+
export function loadContextNodes(layer) {
|
|
18
|
+
const brains = getBothBrains();
|
|
19
|
+
const own = layer !== 'global' && brains.project && isBrainEnabled(brains.project.brainDir) ? brains.project.brainDir : null;
|
|
20
|
+
const global = layer !== 'project' && brains.global && isBrainEnabled(brains.global.brainDir) ? brains.global.brainDir : null;
|
|
21
|
+
if (!own && !global) throw new Error('No enabled brain in selected scope');
|
|
22
|
+
const nodes = own ? mergeGlobalRuleNodes(getNodes(path.join(own, 'memory-vault')), global ? getNodes(path.join(global, 'memory-vault')) : []) :
|
|
23
|
+
getNodes(path.join(global, 'memory-vault')).map(n => ({ ...n, _layer: 'global' }));
|
|
24
|
+
return { nodes, skillsDir: path.dirname(own || global),
|
|
25
|
+
derivedDirs: [own, global].filter(Boolean).map(dir => path.join(dir, 'memory-derived')) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The first lines of an automatically compacted capsule. `rules verify` measures with it too, so both agree on what fits. */
|
|
29
|
+
export const compactedHeader = (version, budget) => (counts, tokens, ranking) => [
|
|
30
|
+
`ready:true compacted:true tokens:${tokens} budget:${budget} version:${version} rules:${counts.rules} full:${counts.full} summarized:${counts.summarized} referenced:${counts.referenced} duplicate:${counts.duplicate} ranking:${ranking}`,
|
|
31
|
+
PREFLIGHT_NOTICE,
|
|
32
|
+
`COMPACTED: every rule below still applies. Read a referenced rule with: total-recall compact "<task>" --expand <slug>`,
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
/** Compact the over-budget capsule, or explain the budget it needs. `fullRequired` is what the uncompacted capsule would take. */
|
|
36
|
+
async function printCompacted({ query, actions, budget, format, version, fullRequired, nodes, skillsDir, derivedDirs }) {
|
|
37
|
+
const { compactNodes } = await import('./compact.mjs');
|
|
38
|
+
// JSON escaping and its wrapper count against the budget too.
|
|
39
|
+
const target = format === 'json' ? Math.floor(budget * JSON_ESCAPE_SHARE) - JSON_WRAPPER_TOKENS : budget;
|
|
40
|
+
const header = compactedHeader(version, budget);
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
result = await compactNodes({ task: query, actions, budget: Math.max(target, 1), complete: true, nodes, skillsDir, derivedDirs, header });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!error.requiredBudget) throw error;
|
|
46
|
+
const floor = format === 'json' ? Math.ceil((error.requiredBudget + JSON_WRAPPER_TOKENS) / JSON_ESCAPE_SHARE) : error.requiredBudget;
|
|
47
|
+
const requiredBudget = Math.min(fullRequired, floor);
|
|
48
|
+
console.log(format === 'json'
|
|
49
|
+
? JSON.stringify({ ready: false, reason: 'budget-overflow', context: '', stats: { version, budget, required_budget: requiredBudget, overage: requiredBudget - budget, rules: error.ruleCount, token_measurement: 'estimated_chars_divided_by_four' } })
|
|
50
|
+
: `ready:false reason:budget-overflow budget:${budget} required_budget:${requiredBudget} overage:${requiredBudget - budget} rules:${error.ruleCount} version:${version}\nThis budget cannot hold all ${error.ruleCount} applicable rules even compacted; \`total-recall rules audit\` lists them by size. Retry with --budget ${requiredBudget}; do not act until ready:true.`);
|
|
51
|
+
process.exitCode = 2;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.log(format === 'json'
|
|
55
|
+
? JSON.stringify({ ready: true, compacted: true, ranking: result.ranking, context: result.context, counts: result.counts,
|
|
56
|
+
stats: { version, budget, total_tokens: result.tokens, token_measurement: 'estimated_chars_divided_by_four' } })
|
|
57
|
+
: result.context);
|
|
58
|
+
}
|
|
59
|
+
|
|
11
60
|
export default async function context(args = []) {
|
|
12
61
|
if (!args.length || args.includes('--help')) {
|
|
13
|
-
console.log(
|
|
62
|
+
console.log(`Usage: total-recall context "task" [--action edit,test,publish] [--budget ${CAPSULE_LIMIT}] [--format text|json] [--knowledge] [--debug] [--project|--global]\nLocal rules only by default. Unknown applicability is required. Over budget, the capsule compacts automatically: absolute rules keep at least a summary and every other rule keeps at least its slug. Configured budgets are capped at ${CAPSULE_LIMIT}; pass a larger --budget to print the full capsule. Exit 2 means the budget cannot hold every rule; do not act until ready:true. --knowledge adds supporting documents; --debug exposes inventories within the same budget. Refresh on task/action/project or policy changes.`);
|
|
14
63
|
return;
|
|
15
64
|
}
|
|
16
65
|
const { layer, remainingArgs } = parseLayerFlag(args);
|
|
@@ -24,7 +73,7 @@ export default async function context(args = []) {
|
|
|
24
73
|
}
|
|
25
74
|
return undefined;
|
|
26
75
|
})();
|
|
27
|
-
let actions = [], budget = configuredBudget ??
|
|
76
|
+
let actions = [], budget = Math.min(configuredBudget ?? CAPSULE_LIMIT, CAPSULE_LIMIT), format = 'text', includeKnowledge = false, debug = false;
|
|
28
77
|
for (let i = 1; i < remainingArgs.length; i++) {
|
|
29
78
|
const flag = remainingArgs[i];
|
|
30
79
|
if (flag === '--knowledge') { includeKnowledge = true; continue; }
|
|
@@ -35,14 +84,8 @@ export default async function context(args = []) {
|
|
|
35
84
|
else if (flag === '--format' && ['json', 'text'].includes(value)) format = value;
|
|
36
85
|
else throw new Error(`Unknown context option: ${flag}`);
|
|
37
86
|
}
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const global = layer !== 'project' && brains.global && isBrainEnabled(brains.global.brainDir) ? brains.global.brainDir : null;
|
|
41
|
-
if (!own && !global) throw new Error('No enabled brain in selected scope');
|
|
42
|
-
const nodes = own ? mergeGlobalRuleNodes(getNodes(path.join(own, 'memory-vault')), global ? getNodes(path.join(global, 'memory-vault')) : []) :
|
|
43
|
-
getNodes(path.join(global, 'memory-vault')).map(n => ({ ...n, _layer: 'global' }));
|
|
44
|
-
const skillsDir = path.dirname(own || global);
|
|
45
|
-
const derivedDir = own ? path.join(own, 'memory-derived') : (global ? path.join(global, 'memory-derived') : null);
|
|
87
|
+
const { nodes, skillsDir, derivedDirs } = loadContextNodes(layer);
|
|
88
|
+
const derivedDir = derivedDirs[0];
|
|
46
89
|
const result = await compileContext({ query, actions, nodes, includeKnowledge, budget: { total: budget }, projectRoot: process.cwd(),
|
|
47
90
|
derivedDir, contributions: legacyRuleContributions(skillsDir), versionInputs: surfaceInputsHash({ skillsDir }) });
|
|
48
91
|
if (debug && format === 'text') throw new Error('--debug requires --format json');
|
|
@@ -55,18 +98,7 @@ export default async function context(args = []) {
|
|
|
55
98
|
derivedDir, action: actions, query, consumer: 'cli',
|
|
56
99
|
});
|
|
57
100
|
if (!response.ready && !debug) {
|
|
58
|
-
|
|
59
|
-
// same rules again. Keep the full set in the compiler/API, and admit it only
|
|
60
|
-
// after the caller explicitly raises the budget or curates applicability.
|
|
61
|
-
const requiredBudget = response.stats.total_tokens + 32;
|
|
62
|
-
const diagnostic = { ready: false, reason: 'budget-overflow', context: '', stats: {
|
|
63
|
-
version: response.stats.version, required_count: response.stats.required_count,
|
|
64
|
-
budget, required_budget: requiredBudget,
|
|
65
|
-
token_measurement: 'estimated_chars_divided_by_four',
|
|
66
|
-
} };
|
|
67
|
-
console.log(format === 'json' ? JSON.stringify(diagnostic) :
|
|
68
|
-
`ready:false reason:budget-overflow budget:${budget} required_budget:${requiredBudget} required_rules:${response.stats.required_count} version:${response.stats.version}\nRetry with --budget ${requiredBudget}; do not act until ready:true.\nTo shorten the rules for good: total-recall rules audit, then rules draft/apply.`);
|
|
69
|
-
process.exitCode = 2;
|
|
101
|
+
await printCompacted({ query, actions, budget, format, version: response.stats.version, fullRequired: response.stats.total_tokens + 32, nodes, skillsDir, derivedDirs });
|
|
70
102
|
return;
|
|
71
103
|
}
|
|
72
104
|
console.log(renderCapsule(response, format));
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -204,13 +204,16 @@ export default async function doctor() {
|
|
|
204
204
|
|
|
205
205
|
if (agents.length > 0) {
|
|
206
206
|
let healthyCount = 0;
|
|
207
|
+
const { HARNESS_SPECS } = await import('../core/meta-harness.mjs').catch(() => ({ HARNESS_SPECS: {} }));
|
|
207
208
|
for (const agent of agents) {
|
|
208
209
|
const exists = commandExists(agent.binary);
|
|
210
|
+
const spec = HARNESS_SPECS[agent.name] || HARNESS_SPECS[agent.binary];
|
|
211
|
+
const enfTier = spec?.enforcement || 'advisory';
|
|
209
212
|
if (exists) {
|
|
210
|
-
logOk(`Agent ${agent.name.padEnd(8)}: Available (Binary: ${agent.binary})`);
|
|
213
|
+
logOk(`Agent ${agent.name.padEnd(8)}: Available (Binary: ${agent.binary}) [${enfTier}]`);
|
|
211
214
|
healthyCount++;
|
|
212
215
|
} else {
|
|
213
|
-
logWarn(`Agent ${agent.name.padEnd(8)}: NOT FOUND in PATH (Binary: ${agent.binary})`);
|
|
216
|
+
logWarn(`Agent ${agent.name.padEnd(8)}: NOT FOUND in PATH (Binary: ${agent.binary}) [${enfTier}]`);
|
|
214
217
|
warnings++;
|
|
215
218
|
}
|
|
216
219
|
}
|
package/src/cli/forget.mjs
CHANGED
|
@@ -33,6 +33,8 @@ function printHelp() {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export default async function forget(args) {
|
|
36
|
+
// Checked first: a destructive command must never act on a request for help.
|
|
37
|
+
if (args.includes('--help') || args.includes('-h')) { printHelp(); return; }
|
|
36
38
|
const { layer: explicitLayer, remainingArgs } = parseLayerFlag(args);
|
|
37
39
|
|
|
38
40
|
// Check for --project-all
|