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/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Messaging Bridge hook adapter.
|
|
3
|
+
* Official doc: https://docs.openclaw.ai/plugins/hooks
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
|
|
9
|
+
const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
|
|
10
|
+
|
|
11
|
+
export const openclawAdapter = {
|
|
12
|
+
id: 'openclaw',
|
|
13
|
+
level: 'enforced',
|
|
14
|
+
source: {
|
|
15
|
+
url: 'https://docs.openclaw.ai/plugins/hooks',
|
|
16
|
+
retrieved: '2026-11-04',
|
|
17
|
+
note: 'OpenClaw before_tool_call plugin hooks with fail-closed timeout'
|
|
18
|
+
},
|
|
19
|
+
limits: [
|
|
20
|
+
'OpenClaw before_tool_call fails closed on timeout or exception',
|
|
21
|
+
'Returns { block: true, blockReason: reason } on deny'
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
parse(input = {}) {
|
|
25
|
+
const event = input.event || 'before_tool_call';
|
|
26
|
+
const toolName = input.toolName || input.tool_name || input.tool || '';
|
|
27
|
+
const params = input.params || input.args || {};
|
|
28
|
+
const cwd = input.cwd || process.cwd();
|
|
29
|
+
|
|
30
|
+
let tool = 'other';
|
|
31
|
+
let command;
|
|
32
|
+
let filePath;
|
|
33
|
+
|
|
34
|
+
if (toolName === 'execute' || toolName === 'Bash' || toolName === 'shell') {
|
|
35
|
+
tool = 'shell';
|
|
36
|
+
command = params.command || params.cmd || '';
|
|
37
|
+
} else if (toolName === 'write' || toolName === 'edit' || toolName === 'Edit' || toolName === 'Write') {
|
|
38
|
+
tool = 'write';
|
|
39
|
+
filePath = params.path || params.file_path || '';
|
|
40
|
+
} else if (toolName === 'read' || toolName === 'Read') {
|
|
41
|
+
tool = 'read';
|
|
42
|
+
filePath = params.path || params.file_path || '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
event,
|
|
47
|
+
tool,
|
|
48
|
+
command,
|
|
49
|
+
path: filePath,
|
|
50
|
+
cwd,
|
|
51
|
+
raw: input
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
emit(decision, event, cwd = process.cwd()) {
|
|
56
|
+
if (decision?.decision === 'allow') {
|
|
57
|
+
const payload = { block: false };
|
|
58
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
|
|
62
|
+
const payload = {
|
|
63
|
+
block: true,
|
|
64
|
+
blockReason: reason,
|
|
65
|
+
fix: decision?.fix || ACK_COMMAND
|
|
66
|
+
};
|
|
67
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async run({ stdin, decideWith }) {
|
|
71
|
+
const parsed = this.parse(stdin);
|
|
72
|
+
const decision = decideWith({
|
|
73
|
+
tool: parsed.tool,
|
|
74
|
+
command: parsed.command,
|
|
75
|
+
path: parsed.path,
|
|
76
|
+
cwd: parsed.cwd
|
|
77
|
+
});
|
|
78
|
+
return this.emit(decision, parsed.event, parsed.cwd);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
install({ home = os.homedir(), dryRun = false, remove = false, off = false } = {}) {
|
|
82
|
+
const isRemove = remove || off;
|
|
83
|
+
const openclawDir = path.join(home, '.openclaw');
|
|
84
|
+
const hooksPath = path.join(openclawDir, 'hooks.json');
|
|
85
|
+
const added = [];
|
|
86
|
+
const removed = [];
|
|
87
|
+
|
|
88
|
+
let hooksConfig = { hooks: {} };
|
|
89
|
+
if (fs.existsSync(hooksPath)) {
|
|
90
|
+
try {
|
|
91
|
+
hooksConfig = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
|
|
92
|
+
} catch (err) {
|
|
93
|
+
throw new Error(`${hooksPath} is not valid JSON; fix it before installing hooks: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
hooksConfig.hooks = hooksConfig.hooks || {};
|
|
97
|
+
hooksConfig.hooks.before_tool_call = hooksConfig.hooks.before_tool_call || [];
|
|
98
|
+
|
|
99
|
+
if (isRemove) {
|
|
100
|
+
const beforeLen = hooksConfig.hooks.before_tool_call.length;
|
|
101
|
+
hooksConfig.hooks.before_tool_call = hooksConfig.hooks.before_tool_call.filter(e =>
|
|
102
|
+
!String(e.command || '').includes('preflight')
|
|
103
|
+
);
|
|
104
|
+
if (hooksConfig.hooks.before_tool_call.length < beforeLen) {
|
|
105
|
+
removed.push('managed before_tool_call gate');
|
|
106
|
+
}
|
|
107
|
+
if (!dryRun && fs.existsSync(hooksPath)) {
|
|
108
|
+
fs.writeFileSync(hooksPath, `${JSON.stringify(hooksConfig, null, 2)}\n`);
|
|
109
|
+
}
|
|
110
|
+
return { settings: hooksPath, added: [], removed };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const gateCommand = 'total-recall preflight gate --harness openclaw';
|
|
114
|
+
const hasPreTool = hooksConfig.hooks.before_tool_call.some(e =>
|
|
115
|
+
String(e.command || '').includes('preflight gate --harness openclaw')
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (!hasPreTool) {
|
|
119
|
+
hooksConfig.hooks.before_tool_call.push({
|
|
120
|
+
matcher: '*',
|
|
121
|
+
command: gateCommand,
|
|
122
|
+
timeout: 10
|
|
123
|
+
});
|
|
124
|
+
added.push('managed before_tool_call gate');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!dryRun && added.length) {
|
|
128
|
+
fs.mkdirSync(openclawDir, { recursive: true });
|
|
129
|
+
if (fs.existsSync(hooksPath)) {
|
|
130
|
+
fs.copyFileSync(hooksPath, `${hooksPath}.bak-total-recall`);
|
|
131
|
+
}
|
|
132
|
+
fs.writeFileSync(hooksPath, `${JSON.stringify(hooksConfig, null, 2)}\n`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { settings: hooksPath, added };
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
verify({ home = os.homedir() } = {}) {
|
|
139
|
+
const hooksPath = path.join(home, '.openclaw', 'hooks.json');
|
|
140
|
+
let installed = false;
|
|
141
|
+
if (fs.existsSync(hooksPath)) {
|
|
142
|
+
try {
|
|
143
|
+
const config = JSON.parse(fs.readFileSync(hooksPath, 'utf8') || '{}');
|
|
144
|
+
const preHooks = config.hooks?.before_tool_call || [];
|
|
145
|
+
installed = preHooks.some(e => String(e.command || '').includes('preflight gate --harness openclaw'));
|
|
146
|
+
} catch { /* unparseable */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const syntheticDeniedCall = {
|
|
150
|
+
event: 'before_tool_call',
|
|
151
|
+
toolName: 'execute',
|
|
152
|
+
params: { command: 'rm -rf /tmp/foo' },
|
|
153
|
+
cwd: '/nonexistent-directory-without-receipt'
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const out = this.emit(
|
|
157
|
+
{ decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
|
|
158
|
+
'before_tool_call',
|
|
159
|
+
syntheticDeniedCall.cwd
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const exercised = out.exitCode === 2 && out.stdout.includes('"block":true');
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
installed,
|
|
166
|
+
exercised,
|
|
167
|
+
level: 'enforced',
|
|
168
|
+
native: out.stdout.trim()
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export default openclawAdapter;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Coding Agent extension hook adapter.
|
|
3
|
+
* Official doc: https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/docs/extensions.md
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
|
|
9
|
+
const ACK_COMMAND = 'total-recall tr-preflight --ack --task "<task>" --action read,edit,test';
|
|
10
|
+
|
|
11
|
+
export const piAdapter = {
|
|
12
|
+
id: 'pi',
|
|
13
|
+
level: 'enforced (unverified live)',
|
|
14
|
+
source: {
|
|
15
|
+
url: 'https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/docs/extensions.md',
|
|
16
|
+
retrieved: '2026-11-04',
|
|
17
|
+
note: 'Pi Coding Agent tool_call extensions in ~/.pi/agent/extensions/'
|
|
18
|
+
},
|
|
19
|
+
limits: [
|
|
20
|
+
'Pi loaded extensions must fail closed explicitly',
|
|
21
|
+
'Returns { block: true, reason } on denied calls'
|
|
22
|
+
],
|
|
23
|
+
|
|
24
|
+
parse(input = {}) {
|
|
25
|
+
const event = input.event || 'tool_call';
|
|
26
|
+
const toolName = input.toolName || input.tool || '';
|
|
27
|
+
const toolInput = input.input || input.params || {};
|
|
28
|
+
const cwd = input.cwd || process.cwd();
|
|
29
|
+
|
|
30
|
+
let tool = 'other';
|
|
31
|
+
let command;
|
|
32
|
+
let filePath;
|
|
33
|
+
|
|
34
|
+
if (toolName === 'execute' || toolName === 'Bash' || toolName === 'shell') {
|
|
35
|
+
tool = 'shell';
|
|
36
|
+
command = toolInput.command || toolInput.cmd || '';
|
|
37
|
+
} else if (toolName === 'edit' || toolName === 'write' || toolName === 'patch') {
|
|
38
|
+
tool = 'write';
|
|
39
|
+
filePath = toolInput.path || toolInput.file || '';
|
|
40
|
+
} else if (toolName === 'read') {
|
|
41
|
+
tool = 'read';
|
|
42
|
+
filePath = toolInput.path || toolInput.file || '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
event,
|
|
47
|
+
tool,
|
|
48
|
+
command,
|
|
49
|
+
path: filePath,
|
|
50
|
+
cwd,
|
|
51
|
+
raw: input
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
emit(decision, event, cwd = process.cwd()) {
|
|
56
|
+
if (decision?.decision === 'allow') {
|
|
57
|
+
const payload = { block: false };
|
|
58
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const reason = decision?.reason || `TR-PREFLIGHT gate: action denied. Run: ${decision?.fix || ACK_COMMAND}`;
|
|
62
|
+
const payload = {
|
|
63
|
+
block: true,
|
|
64
|
+
reason,
|
|
65
|
+
fix: decision?.fix || ACK_COMMAND
|
|
66
|
+
};
|
|
67
|
+
return { stdout: `${JSON.stringify(payload)}\n`, exitCode: 2 };
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async run({ stdin, decideWith }) {
|
|
71
|
+
const parsed = this.parse(stdin);
|
|
72
|
+
const decision = decideWith({
|
|
73
|
+
tool: parsed.tool,
|
|
74
|
+
command: parsed.command,
|
|
75
|
+
path: parsed.path,
|
|
76
|
+
cwd: parsed.cwd
|
|
77
|
+
});
|
|
78
|
+
return this.emit(decision, parsed.event, parsed.cwd);
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
install({ home = os.homedir(), dryRun = false } = {}) {
|
|
82
|
+
const piDir = path.join(home, '.pi', 'agent', 'extensions');
|
|
83
|
+
const hookPath = path.join(piDir, 'total-recall-preflight.json');
|
|
84
|
+
const added = [];
|
|
85
|
+
|
|
86
|
+
const hookDef = {
|
|
87
|
+
name: 'total-recall-preflight',
|
|
88
|
+
command: 'total-recall preflight gate --harness pi',
|
|
89
|
+
timeout: 5
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (!dryRun) {
|
|
93
|
+
fs.mkdirSync(piDir, { recursive: true });
|
|
94
|
+
fs.writeFileSync(hookPath, `${JSON.stringify(hookDef, null, 2)}\n`);
|
|
95
|
+
added.push('managed Pi extension gate');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { settings: hookPath, added };
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
verify({ home = os.homedir() } = {}) {
|
|
102
|
+
const hookPath = path.join(home, '.pi', 'agent', 'extensions', 'total-recall-preflight.json');
|
|
103
|
+
const installed = fs.existsSync(hookPath);
|
|
104
|
+
|
|
105
|
+
const syntheticDeniedCall = {
|
|
106
|
+
event: 'tool_call',
|
|
107
|
+
toolName: 'execute',
|
|
108
|
+
input: { command: 'rm -rf /tmp/foo' },
|
|
109
|
+
cwd: '/nonexistent-directory-without-receipt'
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const out = this.emit(
|
|
113
|
+
{ decision: 'deny', reason: 'receipt missing', fix: ACK_COMMAND },
|
|
114
|
+
'tool_call',
|
|
115
|
+
syntheticDeniedCall.cwd
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const exercised = out.exitCode === 2 && out.stdout.includes('"block":true');
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
installed,
|
|
122
|
+
exercised,
|
|
123
|
+
level: 'enforced (unverified live)',
|
|
124
|
+
native: out.stdout.trim()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default piAdapter;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Run the repository's own quality gates through the code-quality plugin. No configured gate is not a pass. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
export function runQualityGate(directory, { tier = 'fast', runner = spawnSync, timeoutMs = 600000 } = {}) {
|
|
5
|
+
const result = runner(process.env.TOTAL_RECALL_BIN || 'total-recall', ['code-quality-check', '--tier', tier, '--json'], { cwd: directory, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 });
|
|
6
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
|
|
7
|
+
if (result.error || result.status === null) return { status: 'unavailable', passed: false, tier, summary: `The gate did not run: ${result.error?.message || 'terminated'}` };
|
|
8
|
+
let report = null;
|
|
9
|
+
try { report = JSON.parse(result.stdout); } catch { /* a text report */ }
|
|
10
|
+
if (/No code-quality state/.test(report?.error || output)) return { status: 'unconfigured', passed: false, tier, summary: String(report?.error || output).slice(0, 500) };
|
|
11
|
+
return { status: result.status === 0 ? 'passed' : 'failed', passed: result.status === 0, tier, summary: output.slice(-2000), report };
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Agents wrap their answer in prose and code. Find the last well-formed object that carries the expected key. */
|
|
2
|
+
export function lastJsonWith(text, key) {
|
|
3
|
+
const source = String(text || '');
|
|
4
|
+
for (let start = source.lastIndexOf('{'); start >= 0; start = source.lastIndexOf('{', start - 1)) {
|
|
5
|
+
let depth = 0, inString = false;
|
|
6
|
+
for (let i = start; i < source.length; i += 1) {
|
|
7
|
+
const c = source[i];
|
|
8
|
+
if (inString) { if (c === '\\') i += 1; else if (c === '"') inString = false; continue; }
|
|
9
|
+
if (c === '"') inString = true;
|
|
10
|
+
else if (c === '{') depth += 1;
|
|
11
|
+
else if (c === '}') {
|
|
12
|
+
depth -= 1;
|
|
13
|
+
if (depth === 0) {
|
|
14
|
+
try { const value = JSON.parse(source.slice(start, i + 1)); if (value && Object.hasOwn(value, key)) return value; } catch { /* keep looking */ }
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (start === 0) break;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -1,90 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* and proposes friction-reducing rule/memory proposals.
|
|
2
|
+
* Execution receipts and the learning loop.
|
|
3
|
+
* Receipts are an append-only SSSS ledger. Scorecards, quota walls and proposals fold from it.
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import os from 'node:os';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
10
6
|
import { computeEmpiricalScorecard } from '../capability/index.mjs';
|
|
7
|
+
import { receiptsFile } from './paths.mjs';
|
|
11
8
|
|
|
12
|
-
function
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
fs.mkdirSync(baseDir, { recursive: true });
|
|
16
|
-
return path.join(baseDir, 'meta-harness-runs.json');
|
|
9
|
+
async function store() {
|
|
10
|
+
const { PluginStateStore } = await import('./ssss-store.mjs');
|
|
11
|
+
return new PluginStateStore(receiptsFile(), 'meta-harness');
|
|
17
12
|
}
|
|
13
|
+
export const readReceipts = async () => (await store()).read([]);
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
export function recordRunReceipt(receipt = {}) {
|
|
23
|
-
const ledgerPath = getLedgerPath();
|
|
24
|
-
let entries = [];
|
|
25
|
-
if (fs.existsSync(ledgerPath)) {
|
|
26
|
-
try {
|
|
27
|
-
entries = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
|
|
28
|
-
} catch {
|
|
29
|
-
entries = [];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
15
|
+
export async function recordRunReceipt(receipt = {}) {
|
|
16
|
+
if (!receipt.harness) throw new Error('A run receipt must name its harness');
|
|
33
17
|
const entry = {
|
|
34
|
-
id: receipt.id || `run-${Date.now()}`,
|
|
18
|
+
id: receipt.id || `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
35
19
|
timestamp: new Date().toISOString(),
|
|
36
|
-
harness: receipt.harness
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
harness: receipt.harness,
|
|
21
|
+
role: receipt.role || null,
|
|
22
|
+
node: receipt.node || 'local',
|
|
23
|
+
task: String(receipt.task || '').slice(0, 300),
|
|
24
|
+
complexityScore: receipt.complexityScore ?? null,
|
|
25
|
+
success: receipt.success === true,
|
|
40
26
|
durationMs: receipt.durationMs || 0,
|
|
27
|
+
failureKind: receipt.failureKind || null,
|
|
28
|
+
error: receipt.error ? String(receipt.error).slice(0, 500) : null,
|
|
29
|
+
resetsAt: receipt.resetsAt || null,
|
|
30
|
+
gate: receipt.gate || null,
|
|
41
31
|
escalatedFrom: receipt.escalatedFrom || null,
|
|
42
|
-
failures: receipt.failures || {}
|
|
43
32
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Keep last 1,000 runs
|
|
47
|
-
if (entries.length > 1000) {
|
|
48
|
-
entries = entries.slice(-1000);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
fs.writeFileSync(ledgerPath, JSON.stringify(entries, null, 2), 'utf8');
|
|
52
|
-
return { ok: true, recorded: entry.id, totalRuns: entries.length };
|
|
33
|
+
(await store()).append(entry);
|
|
34
|
+
return entry;
|
|
53
35
|
}
|
|
54
36
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
let entries = [];
|
|
61
|
-
if (fs.existsSync(ledgerPath)) {
|
|
62
|
-
try {
|
|
63
|
-
entries = JSON.parse(fs.readFileSync(ledgerPath, 'utf8'));
|
|
64
|
-
} catch {
|
|
65
|
-
entries = [];
|
|
66
|
-
}
|
|
37
|
+
/** A harness is walled from its last quota failure until the reported reset, or until a later success proves it clear. */
|
|
38
|
+
export function activeQuotaWalls(entries = [], now = Date.now(), unknownResetMs = 60 * 60 * 1000) {
|
|
39
|
+
const latest = new Map();
|
|
40
|
+
for (const run of entries) {
|
|
41
|
+
if (run.success || run.failureKind === 'quota') latest.set(run.harness, run);
|
|
67
42
|
}
|
|
43
|
+
const walls = {};
|
|
44
|
+
for (const [harness, run] of latest) {
|
|
45
|
+
if (run.success) continue;
|
|
46
|
+
const observed = Date.parse(run.timestamp);
|
|
47
|
+
const until = run.resetsAt ? Date.parse(run.resetsAt) : observed + unknownResetMs;
|
|
48
|
+
if (until > now) walls[harness] = { until: new Date(until).toISOString(), resetReported: Boolean(run.resetsAt), observedAt: run.timestamp, message: run.error };
|
|
49
|
+
}
|
|
50
|
+
return walls;
|
|
51
|
+
}
|
|
68
52
|
|
|
69
|
-
|
|
53
|
+
export function proposalsFrom(scorecard, entries = []) {
|
|
70
54
|
const proposals = [];
|
|
71
|
-
|
|
72
|
-
// Identify harnesses with elevated failure rates (>25%)
|
|
73
55
|
for (const [harness, stats] of Object.entries(scorecard)) {
|
|
74
|
-
if (stats.total >= 3 && stats.passRate < 75) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
recommendation: `Auto-escalate ${harness} to frontier model or apply strict preflight gate.`
|
|
80
|
-
});
|
|
81
|
-
}
|
|
56
|
+
if (stats.total >= 3 && stats.passRate < 75) proposals.push({
|
|
57
|
+
id: `meta-harness-low-pass-${harness}`, targetHarness: harness,
|
|
58
|
+
reason: `${harness} passed ${stats.passRate}% of ${stats.total} attributable runs.`,
|
|
59
|
+
recommendation: `Route complex work away from ${harness}, or require a gate before accepting its output.`,
|
|
60
|
+
});
|
|
82
61
|
}
|
|
62
|
+
const gateFailures = new Map();
|
|
63
|
+
for (const run of entries) if (run.failureKind === 'gate') gateFailures.set(run.harness, (gateFailures.get(run.harness) || 0) + 1);
|
|
64
|
+
for (const [harness, count] of gateFailures) if (count >= 3) proposals.push({
|
|
65
|
+
id: `meta-harness-gate-failures-${harness}`, targetHarness: harness,
|
|
66
|
+
reason: `${harness} output failed quality gates ${count} times.`,
|
|
67
|
+
recommendation: `Review recent ${harness} branches for a recurring defect and record the rule that prevents it.`,
|
|
68
|
+
});
|
|
69
|
+
return proposals;
|
|
70
|
+
}
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
72
|
+
/** A harness whose last run failed for a missing login or key stays out of the pool for an hour, then gets another chance. */
|
|
73
|
+
export function activeCredentialBlocks(entries = [], now = Date.now(), holdMs = 60 * 60 * 1000) {
|
|
74
|
+
const latest = new Map();
|
|
75
|
+
for (const run of entries) if (run.success || run.failureKind === 'auth') latest.set(run.harness, run);
|
|
76
|
+
const blocks = {};
|
|
77
|
+
for (const [harness, run] of latest) if (!run.success && Date.parse(run.timestamp) + holdMs > now) blocks[harness] = { observedAt: run.timestamp, message: run.error };
|
|
78
|
+
return blocks;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** With `emit`, proposals are written through the memory owner. A failed write is reported, never hidden. */
|
|
82
|
+
export async function learnFromHistory({ emit = false, runner = spawnSync } = {}) {
|
|
83
|
+
const entries = await readReceipts();
|
|
84
|
+
const scorecard = computeEmpiricalScorecard(entries);
|
|
85
|
+
const proposals = proposalsFrom(scorecard, entries);
|
|
86
|
+
const emitted = [];
|
|
87
|
+
if (emit) for (const proposal of proposals) {
|
|
88
|
+
const result = runner(process.env.TOTAL_RECALL_BIN || 'total-recall', ['remember', 'facts', `${proposal.reason} ${proposal.recommendation}`, '--project', '--tags', `meta-harness,${proposal.id}`], { encoding: 'utf8', timeout: 30000 });
|
|
89
|
+
emitted.push({ id: proposal.id, ok: result.status === 0, error: result.status === 0 ? undefined : String(result.stderr || result.error || 'remember failed').slice(0, 300) });
|
|
90
|
+
}
|
|
91
|
+
return { ok: emitted.every(row => row.ok), totalHistoryRuns: entries.length, scorecard, walls: activeQuotaWalls(entries), credentialBlocks: activeCredentialBlocks(entries), proposals, ...(emit ? { emitted } : {}) };
|
|
90
92
|
}
|