total-recall-brain 3.41.6 → 3.42.0
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/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +3118 -17
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +84 -40
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/exit-after-flush.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-api-rotation.mjs +61 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +15 -7
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +51 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +67 -25
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +52 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +21 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/compat-harness.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +48 -22
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +9 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +21 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +9 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +10 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +30 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +19 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +222 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-worker.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +70 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +60 -19
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +32 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +24 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +88 -125
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +110 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +95 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +13 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +78 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/context-provider.mjs +113 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/src/cli/agent.mjs +11 -6
- package/src/cli/compact.mjs +3 -3
- package/src/cli/context.mjs +52 -19
- package/src/cli/harness.mjs +65 -147
- package/src/cli/init.mjs +4 -2
- package/src/cli/preflight.mjs +56 -13
- package/src/core/agent-manager.mjs +23 -329
- package/src/core/context-compactor.mjs +2 -1
- package/src/core/context-compiler.mjs +16 -25
- package/src/core/context-providers.mjs +69 -0
- package/src/core/meta-harness.mjs +27 -749
- package/src/core/plugin-runner.mjs +12 -5
- package/src/core/plugin-runtime.mjs +4 -2
- package/src/core/runtime.mjs +30 -604
- package/src/core/task-context.mjs +122 -0
- package/src/server/routes/context.mjs +4 -1
- package/scaffold/.agent/skills/decision/SKILL.md +0 -20
- package/scaffold/.agent/skills/decision/references/integration.md +0 -25
- package/scaffold/.agent/skills/decision/references/use-cases.md +0 -20
- package/scaffold/.agent/skills/meta-harness/SKILL.md +0 -15
- package/scaffold/.agent/skills/meta-harness/references/optimized/index.md +0 -20
- package/scaffold/.agent/skills/meta-harness/references/optimized/manifest.json +0 -79
- package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +0 -135
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-02.md +0 -17
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-03.md +0 -8
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-04.md +0 -2
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-05.md +0 -6
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-06.md +0 -14
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-07.md +0 -8
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-08.md +0 -4
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +0 -20
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-10.md +0 -6
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-11.md +0 -5
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-12.md +0 -7
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-13.md +0 -4
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-14.md +0 -7
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-15.md +0 -9
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-16.md +0 -5
|
@@ -30,7 +30,7 @@ export const EXTENDED_ADAPTERS = [
|
|
|
30
30
|
{ id: 'ollama', name: 'Ollama', binary: 'ollama', args: ['run'], input: 'stdin', docs: 'https://ollama.com/library' },
|
|
31
31
|
{ id: 'openclaw', name: 'OpenClaw', binary: 'openclaw', args: ['agent', '--message'], input: 'argument', docs: 'https://openclaw.ai/docs' },
|
|
32
32
|
{ id: 'aider', name: 'Aider', binary: 'aider', args: ['--yes', '--message'], input: 'argument', docs: 'https://aider.chat/docs' },
|
|
33
|
-
{ id: 'goose', name: 'Goose', binary: 'goose', args: ['run', '--text'], input: 'argument', docs: 'https://
|
|
33
|
+
{ id: 'goose', name: 'Goose', binary: 'goose', args: ['run', '--text'], input: 'argument', docs: 'https://goose-docs.ai/' },
|
|
34
34
|
{ id: 'openhands', name: 'OpenHands', binary: 'openhands', args: ['run'], input: 'argument', docs: 'https://docs.openhands.dev/' },
|
|
35
35
|
{ id: 'cline', name: 'Cline', binary: 'cline', args: ['task', '--json'], input: 'argument', docs: 'https://cline.bot/docs' }
|
|
36
36
|
];
|
|
@@ -43,6 +43,35 @@ export function getCustomAdaptersDirs() {
|
|
|
43
43
|
];
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function adapterPath(directory, id) {
|
|
47
|
+
if (typeof id !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(id)) throw new Error('Invalid adapter identifier');
|
|
48
|
+
const file = path.join(directory, `${id}.json`);
|
|
49
|
+
try { if (fs.lstatSync(file).isSymbolicLink()) throw new Error('Adapter files cannot be symbolic links'); }
|
|
50
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Validate executable declarations before persisting or loading them; preserve their scoped contracts. */
|
|
55
|
+
export function validateCustomAdapter(spec) {
|
|
56
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) throw new Error('Adapter specification must be an object');
|
|
57
|
+
adapterPath('.', spec.id);
|
|
58
|
+
if (typeof spec.binary !== 'string' || !spec.binary.trim() || /[\0\r\n]/.test(spec.binary)) throw new Error('Adapter must declare a binary');
|
|
59
|
+
for (const key of ['args', 'auth']) if (spec[key] !== undefined && (!Array.isArray(spec[key]) || spec[key].some(arg => typeof arg !== 'string' || /\0/.test(arg)))) throw new Error(`Invalid adapter ${key}`);
|
|
60
|
+
if (spec.input !== undefined && !['stdin', 'argument', 'file'].includes(spec.input)) throw new Error('Invalid adapter input mode');
|
|
61
|
+
if (spec.credentials !== undefined) {
|
|
62
|
+
if (!spec.credentials || typeof spec.credentials !== 'object' || Array.isArray(spec.credentials)) throw new Error('Invalid adapter credentials');
|
|
63
|
+
for (const [name, keys] of Object.entries(spec.credentials)) {
|
|
64
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name) || !Array.isArray(keys) || !keys.length || keys.some(key => typeof key !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(key))) throw new Error('Adapter credentials must map environment names to auth-store key names');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (spec.docs != null) {
|
|
68
|
+
const url = new URL(spec.docs);
|
|
69
|
+
if (url.protocol !== 'https:' || url.username || url.password) throw new Error('Adapter documentation must use an HTTPS URL without credentials');
|
|
70
|
+
}
|
|
71
|
+
for (const key of ['costScore', 'qualityScore']) if (spec[key] !== undefined && !Number.isFinite(spec[key])) throw new Error(`Invalid adapter ${key}`);
|
|
72
|
+
return { ...spec, name: spec.name || spec.id, args: spec.args || ['-p'], input: spec.input || 'argument', docs: spec.docs || null };
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
export function loadCustomAdapters() {
|
|
47
76
|
const custom = [];
|
|
48
77
|
for (const dir of getCustomAdaptersDirs()) {
|
|
@@ -52,22 +81,18 @@ export function loadCustomAdapters() {
|
|
|
52
81
|
if (!file.endsWith('.json')) continue;
|
|
53
82
|
const fullPath = path.join(dir, file);
|
|
54
83
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
if (fs.lstatSync(fullPath).isSymbolicLink()) throw new Error('Adapter files cannot be symbolic links');
|
|
85
|
+
const spec = validateCustomAdapter(JSON.parse(fs.readFileSync(fullPath, 'utf8')));
|
|
86
|
+
if (spec) {
|
|
57
87
|
custom.push({
|
|
58
|
-
|
|
59
|
-
name: spec.name || spec.id,
|
|
60
|
-
binary: spec.binary,
|
|
61
|
-
args: Array.isArray(spec.args) ? spec.args : ['-p'],
|
|
62
|
-
input: spec.input || 'argument',
|
|
63
|
-
docs: spec.docs || null,
|
|
88
|
+
...spec,
|
|
64
89
|
custom: true,
|
|
65
90
|
sourcePath: fullPath
|
|
66
91
|
});
|
|
67
92
|
}
|
|
68
|
-
} catch {}
|
|
93
|
+
} catch (error) { throw new Error(`Invalid adapter declaration ${file}: ${error.message}`); }
|
|
69
94
|
}
|
|
70
|
-
} catch {}
|
|
95
|
+
} catch (error) { throw new Error(`Cannot load adapters from ${dir}: ${error.message}`); }
|
|
71
96
|
}
|
|
72
97
|
return custom;
|
|
73
98
|
}
|
|
@@ -99,6 +124,13 @@ export function findBinary(name, env = process.env) {
|
|
|
99
124
|
return null;
|
|
100
125
|
}
|
|
101
126
|
|
|
127
|
+
/** Process groups this process started and has not finished, so termination can stop them. */
|
|
128
|
+
const ACTIVE_GROUPS = new Set();
|
|
129
|
+
export function stopActiveProcesses(signal = 'SIGTERM') {
|
|
130
|
+
for (const pid of ACTIVE_GROUPS) { try { process.kill(process.platform === 'win32' ? pid : -pid, signal); } catch { /* already gone */ } }
|
|
131
|
+
return ACTIVE_GROUPS.size;
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000, env = agentEnvironment(), maxBytes = 2 * 1024 * 1024 } = {}) {
|
|
103
135
|
return new Promise(resolve => {
|
|
104
136
|
let stdout = '', stderr = '', bytes = 0, reason = null, child, timer, escalation, settled = false;
|
|
@@ -106,6 +138,7 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
|
|
|
106
138
|
const finish = (exitCode, signal) => {
|
|
107
139
|
if (settled) return;
|
|
108
140
|
settled = true; clearTimeout(timer);
|
|
141
|
+
if (child?.pid) ACTIVE_GROUPS.delete(child.pid);
|
|
109
142
|
// A parent can exit on TERM while a descendant still holds the process group.
|
|
110
143
|
// Finish the owned group cleanup before releasing an aborted operation.
|
|
111
144
|
if (reason && escalation) {
|
|
@@ -116,6 +149,7 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
|
|
|
116
149
|
const stop = cause => { if (reason) return; reason = cause; kill('SIGTERM'); escalation = setTimeout(() => kill('SIGKILL'), 500); };
|
|
117
150
|
try { child = spawn(binary, args, { cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
118
151
|
catch { finish(null); return; }
|
|
152
|
+
if (child.pid) ACTIVE_GROUPS.add(child.pid);
|
|
119
153
|
timer = setTimeout(() => stop('timeout'), timeoutMs);
|
|
120
154
|
child.on('error', () => { reason = 'spawn-error'; finish(null); });
|
|
121
155
|
child.stdin.on('error', () => {});
|
|
@@ -133,7 +167,12 @@ const frameReason = frame => frame.finished_reason || frame.status || frame.type
|
|
|
133
167
|
export function parseResetTime(text, now = new Date()) {
|
|
134
168
|
const span = String(text).match(/resets? in\s+(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?/i);
|
|
135
169
|
if (span && (span[1] || span[2] || span[3])) return new Date(now.getTime() + ((+span[1] || 0) * 3600 + (+span[2] || 0) * 60 + (+span[3] || 0)) * 1000).toISOString();
|
|
136
|
-
const
|
|
170
|
+
const dated = String(text).match(/(?:try again at|resets)\s+([A-Z][a-z]{2,8})\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4}),?\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
|
|
171
|
+
if (dated) {
|
|
172
|
+
const at = new Date(`${dated[1]} ${dated[2]}, ${dated[3]} ${dated[4]}:${dated[5] || '00'} ${dated[6]}`);
|
|
173
|
+
if (!Number.isNaN(at.getTime())) return at.toISOString();
|
|
174
|
+
}
|
|
175
|
+
const clock = String(text).match(/(?:try again at|resets)\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
|
|
137
176
|
if (clock) {
|
|
138
177
|
const at = new Date(now); at.setSeconds(0, 0);
|
|
139
178
|
at.setHours((+clock[1] % 12) + (/pm/i.test(clock[3]) ? 12 : 0), +clock[2] || 0);
|
|
@@ -144,7 +183,7 @@ export function parseResetTime(text, now = new Date()) {
|
|
|
144
183
|
}
|
|
145
184
|
export function classifyFailure(message, now) {
|
|
146
185
|
const text = String(message || '');
|
|
147
|
-
if (/resource_exhausted|quota|usage limit|rate.?limit|too many requests|\b429\b|out of credits|insufficient credits/i.test(text)) return { failureKind: 'quota', resetsAt: parseResetTime(text, now) };
|
|
186
|
+
if (/resource_exhausted|quota|usage limit|rate.?limit|too many requests|\b429\b|out of credits|insufficient credits|session limit|requires more credits|can only afford|\b402\b/i.test(text)) return { failureKind: 'quota', resetsAt: parseResetTime(text, now) };
|
|
148
187
|
if (/not logged in|not authenticated|unauthenticated|please (log|sign) in|\b401\b|invalid api key|missing_credential|no api key/i.test(text)) return { failureKind: 'auth' };
|
|
149
188
|
return { failureKind: 'agent' };
|
|
150
189
|
}
|
|
@@ -172,11 +211,16 @@ export function parseAgentResult(id, result) {
|
|
|
172
211
|
catch { return result.exitCode === 0 ? { ok: false, error: 'Agent output is not valid structured data', failureKind: 'agent' } : agentFailure(null, stderr || `Agent process exited ${result.exitCode}`); }
|
|
173
212
|
}
|
|
174
213
|
frames = (frames || []).filter(frame => frame && typeof frame === 'object');
|
|
214
|
+
const last = frames.at(-1);
|
|
215
|
+
const metadata = {
|
|
216
|
+
exitCode: result.exitCode ?? null,
|
|
217
|
+
model: frames.findLast(frame => typeof frame.model === 'string')?.model ?? null,
|
|
218
|
+
finishedReason: last?.finished_reason ?? last?.messages?.at(-1)?.stopReason ?? last?.stopReason ?? last?.type ?? null,
|
|
219
|
+
};
|
|
175
220
|
const failed = frames.find(frame => frameError(frame) || frame.is_error || ['error', 'turn.failed'].includes(frame.type) || ['FAILED', 'ERROR', 'CANCELED', 'WAITING'].includes(frame.status) || ['print_timeout', 'interrupted', 'error'].includes(frame.finished_reason));
|
|
176
|
-
if (failed) return agentFailure(frameError(failed) || frameReason(failed), stderr);
|
|
221
|
+
if (failed) return { ...agentFailure(frameError(failed) || frameReason(failed), stderr), ...metadata };
|
|
177
222
|
if (result.exitCode !== 0) return agentFailure(null, stderr || `Agent process exited ${result.exitCode}`);
|
|
178
223
|
if (!frames.length) return { ok: false, error: 'Agent returned no result', failureKind: 'agent' };
|
|
179
|
-
const last = frames.at(-1);
|
|
180
224
|
let text;
|
|
181
225
|
if (id === 'codex' && last.type === 'turn.completed') text = frames.filter(frame => frame.type === 'item.completed' && frame.item?.type === 'agent_message').map(frame => frame.item.text).join('\n');
|
|
182
226
|
if (['claude', 'cursor', 'grok'].includes(id) && last.type === 'result' && last.subtype === 'success') text = last.result;
|
|
@@ -192,7 +236,7 @@ export function parseAgentResult(id, result) {
|
|
|
192
236
|
// notifications. Preserve the actual log rather than invent a headless schema.
|
|
193
237
|
// Exit zero certifies a finished turn only; it never certifies the requested work.
|
|
194
238
|
if (id === 'muse') return { ok: true, response: result.stdout.trim(), events: frames, completionEvidence: 'headless-process-exit', commandExecutionVerified: false };
|
|
195
|
-
return typeof text === 'string' && text.trim() ? { ok: true, response: text, commandExecutionVerified: false } : { ok: false, error: 'Agent did not produce a recognized completed result', failureKind: 'agent' };
|
|
239
|
+
return typeof text === 'string' && text.trim() ? { ok: true, response: text, commandExecutionVerified: false, ...metadata } : { ok: false, error: 'Agent did not produce a recognized completed result', failureKind: 'agent', ...metadata };
|
|
196
240
|
}
|
|
197
241
|
|
|
198
242
|
/** Rebuild the instruction payload from the selected node/workspace, never a cached prompt. */
|
|
@@ -201,26 +245,34 @@ export async function totalRecallBootstrap(request, { probe = runProcess, env =
|
|
|
201
245
|
if (!binary) throw notReady('Total Recall is not installed on the selected node');
|
|
202
246
|
// Rules come from the operator's project, even when the agent itself works in a temporary worktree.
|
|
203
247
|
const cwd = request.contextCwd || request.cwd;
|
|
204
|
-
const invoke = async args => {
|
|
205
|
-
const result = await probe(binary, args, { cwd, timeoutMs: 120000, maxBytes: 4 * 1024 * 1024, env: agentEnvironment(env) });
|
|
248
|
+
const invoke = async (args, at = cwd) => {
|
|
249
|
+
const result = await probe(binary, args, { cwd: at, timeoutMs: 120000, maxBytes: 4 * 1024 * 1024, env: agentEnvironment(env) });
|
|
206
250
|
if (result.reason || result.exitCode !== 0) {
|
|
207
251
|
let detail = String(result.stderr || '').trim();
|
|
208
252
|
try { detail = JSON.parse(result.stdout).problems?.join('; ') || detail; } catch { /* plain output */ }
|
|
209
|
-
throw notReady(`Total Recall ${args[0]} failed in ${
|
|
253
|
+
throw notReady(`Total Recall ${args[0]} failed in ${at}, so dispatch was refused${detail ? `: ${detail.slice(0, 300)}` : ''}`);
|
|
210
254
|
}
|
|
211
255
|
return result.stdout;
|
|
212
256
|
};
|
|
213
257
|
const actions = request.permissions?.write ? 'read,edit,test' : 'read';
|
|
214
258
|
const task = String(request.contextTask || request.prompt).slice(0, 2000);
|
|
215
|
-
const capsule = JSON.parse(await invoke(['context', task, '--action', actions, '--
|
|
259
|
+
const capsule = JSON.parse(await invoke(['context', task, '--action', actions, '--format', 'json']));
|
|
216
260
|
if (capsule.ready !== true || typeof capsule.context !== 'string') throw notReady('Required Total Recall context is not ready');
|
|
217
261
|
await invoke(['preflight', 'ack', '--task', task, '--action', actions, '--json']);
|
|
262
|
+
// A receipt is per checkout, and the agent's own gate reads the one where it works.
|
|
263
|
+
if (request.cwd !== cwd) await invoke(['preflight', 'ack', '--task', task, '--action', actions, '--json'], request.cwd);
|
|
218
264
|
const help = await invoke(['--help']);
|
|
219
265
|
const plugins = JSON.parse(await invoke(['plugin', 'library', '--json']));
|
|
220
266
|
const commands = JSON.parse(await invoke(['composable-cli', 'library', '--json']));
|
|
221
267
|
const docs = JSON.parse(await invoke(['meta-harness', 'docs-ensure', '--agent', request.agent, '--json']));
|
|
222
268
|
if (docs.ok !== true) throw notReady('Official agent documentation is unavailable or stale');
|
|
223
|
-
|
|
269
|
+
let references;
|
|
270
|
+
if (request.bootstrapDirectory) {
|
|
271
|
+
const file = path.join(request.bootstrapDirectory, 'cli-reference.json');
|
|
272
|
+
fs.writeFileSync(file, JSON.stringify({ help, plugins, commands }), { mode: 0o600 });
|
|
273
|
+
references = `Complete CLI, installed-plugin and composed-command inventories: ${file}. Retrieve only the applicable operations; use total-recall <namespace> --help for current arguments.`;
|
|
274
|
+
} else references = `Complete CLI help:\n${help}\nInstalled plugin command library:\n${JSON.stringify(plugins)}\nComposed command library:\n${JSON.stringify(commands)}`;
|
|
275
|
+
return `Requester: the operator of this Total Recall workspace.\nThe Total Recall preflight is already acknowledged for this checkout, so do not run it again; a sandboxed session cannot write its receipt.\nUse Total Recall and its owning plugin CLI for this task. Context was assembled automatically for this task on this node. Official documentation is cached at ${docs.directory}; read applicable references before changing an integration.\n\nTask context:\n${capsule.context}\n\nCLI reference:\n${references}\n\nRequested task:\n${request.prompt}`;
|
|
224
276
|
}
|
|
225
277
|
|
|
226
278
|
/** Authentication is `true` or `false` only when a real probe says so. Everything else stays 'unknown' until a run proves it. */
|
|
@@ -250,6 +302,9 @@ export async function inspectAgents({ probe = runProcess, env = process.env, ids
|
|
|
250
302
|
const names = String(list.stdout || '').split('\n').slice(1).map(line => line.trim().split(/\s+/)[0]).filter(Boolean);
|
|
251
303
|
item.chatModels = names.filter(name => !/embed|bge-|minilm|rerank/i.test(name));
|
|
252
304
|
if (!item.chatModels.length) item.blockedBy = 'no-chat-model';
|
|
305
|
+
} else if (adapter.auth?.length) {
|
|
306
|
+
const auth = await probe(binary, adapter.auth, { timeoutMs: 15000, env: agentEnvironment(env) });
|
|
307
|
+
item.authenticated = auth.reason ? 'unknown' : auth.exitCode === 0;
|
|
253
308
|
}
|
|
254
309
|
}
|
|
255
310
|
agents.push(item);
|
|
@@ -259,7 +314,8 @@ export async function inspectAgents({ probe = runProcess, env = process.env, ids
|
|
|
259
314
|
|
|
260
315
|
export async function dispatchAgent(request, { probe = runProcess, env = process.env, bootstrap = totalRecallBootstrap } = {}) {
|
|
261
316
|
const adapter = getActiveAdapters().find(item => item.id === request.agent);
|
|
262
|
-
if (!adapter || !path.isAbsolute(request.cwd || '') || typeof request.prompt !== 'string' || !request.prompt.trim()
|
|
317
|
+
if (!adapter || !path.isAbsolute(request.cwd || '') || typeof request.prompt !== 'string' || !request.prompt.trim()) throw new Error('Explicit supported agent, absolute workspace and prompt required');
|
|
318
|
+
if (Buffer.byteLength(request.prompt) > 65536) throw notReady(`The prompt is ${Buffer.byteLength(request.prompt)} bytes and the limit is 65536, so it was not sent`);
|
|
263
319
|
if (!fs.statSync(request.cwd).isDirectory()) throw new Error('Selected workspace is not a directory');
|
|
264
320
|
const binary = findBinary(adapter.binary, env);
|
|
265
321
|
if (!binary) throw notReady(`${adapter.name} is not installed on the selected node`);
|
|
@@ -270,7 +326,9 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
270
326
|
if (state.blockedBy === 'no-chat-model') throw notReady(`${adapter.name} has no chat model installed on this node`);
|
|
271
327
|
if (state.authenticated === 'unknown' && request.allowUnknownAuth !== true) throw notReady(`${adapter.name} has no login probe; select it explicitly to verify it through execution`);
|
|
272
328
|
if (request.permissions?.write !== true && !READ_ONLY_ADAPTERS.includes(adapter.id) && request.isolated !== true) throw notReady(`${adapter.name} has no read-only mode, so a read-only run must use a disposable checkout`);
|
|
273
|
-
const
|
|
329
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'meta-harness-prompt-'));
|
|
330
|
+
try {
|
|
331
|
+
const prompt = await bootstrap({ ...request, bootstrapDirectory: temporary }, { probe, env });
|
|
274
332
|
const credentials = {};
|
|
275
333
|
for (const [name, candidates] of Object.entries(adapter.credentials || {})) {
|
|
276
334
|
const auth = findBinary('total-recall', env);
|
|
@@ -283,6 +341,23 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
283
341
|
credentials[name] = value;
|
|
284
342
|
}
|
|
285
343
|
const args = [...adapter.args];
|
|
344
|
+
if (request.model !== undefined) {
|
|
345
|
+
if (typeof request.model !== 'string' || !request.model.trim() || /[\0\r\n]/.test(request.model)) throw new Error('Invalid requested model');
|
|
346
|
+
if (adapter.id === 'ollama') throw new Error('Select the local chat model through its configured runtime');
|
|
347
|
+
args.push('--model', request.model);
|
|
348
|
+
}
|
|
349
|
+
if (request.tools !== undefined || request.settingSources !== undefined) {
|
|
350
|
+
if (adapter.id !== 'claude') throw notReady(`${adapter.name} cannot enforce the requested tools/settings restriction`);
|
|
351
|
+
const tools = request.tools;
|
|
352
|
+
if (tools !== undefined) {
|
|
353
|
+
if (typeof tools !== 'string' && (!Array.isArray(tools) || tools.some(tool => typeof tool !== 'string'))) throw new Error('Invalid tool restriction');
|
|
354
|
+
args.push('--tools', tools === 'none' ? '' : Array.isArray(tools) ? tools.join(',') : tools);
|
|
355
|
+
}
|
|
356
|
+
if (request.settingSources !== undefined) {
|
|
357
|
+
if (typeof request.settingSources !== 'string' || request.settingSources.split(',').some(source => !['', 'user', 'project', 'local'].includes(source))) throw new Error('Invalid setting sources');
|
|
358
|
+
args.push('--setting-sources', request.settingSources);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
286
361
|
const allowWrites = request.permissions?.write === true;
|
|
287
362
|
if (adapter.id === 'codex') args[args.indexOf('--sandbox') + 1] = allowWrites ? 'workspace-write' : 'read-only';
|
|
288
363
|
if (adapter.id === 'claude') args[args.indexOf('--permission-mode') + 1] = allowWrites ? 'acceptEdits' : 'plan';
|
|
@@ -290,15 +365,19 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
290
365
|
if (adapter.id === 'muse' && !allowWrites) args.push('--disable-write');
|
|
291
366
|
if (adapter.id === 'pi') args[args.indexOf('--tools') + 1] = allowWrites ? 'read,bash,edit,write' : 'read';
|
|
292
367
|
if (adapter.id === 'ollama') args.push(env.TR_OLLAMA_MODEL || state.chatModels[0]);
|
|
293
|
-
|
|
368
|
+
const input = adapter.input === 'stdin' ? prompt : '';
|
|
294
369
|
if (adapter.input === 'argument' || adapter.input === 'file') {
|
|
295
|
-
temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'meta-harness-prompt-'));
|
|
296
370
|
const file = path.join(temporary, 'prompt.txt'); fs.writeFileSync(file, prompt, { mode: 0o600 });
|
|
297
371
|
if (adapter.input === 'file') args.push('--prompt-file', file);
|
|
298
|
-
else args.push(`Read ${file} completely before taking action. It contains the required Total Recall task context,
|
|
372
|
+
else args.push(`Read ${file} completely before taking action. It contains the required Total Recall task context, CLI reference location, official documentation location and the requested task. Carry out that task using the supplied context.`);
|
|
299
373
|
}
|
|
300
|
-
|
|
301
|
-
|
|
374
|
+
const execution = await probe(binary, args, { cwd: request.cwd, input, timeoutMs: request.timeoutMs || 900000, env: { ...agentEnvironment(env), ...credentials } });
|
|
375
|
+
const parsed = parseAgentResult(adapter.id, execution);
|
|
376
|
+
const frames = parsed.events || [];
|
|
377
|
+
return { agent: adapter.id, ...parsed, exitCode: execution.exitCode ?? null,
|
|
378
|
+
model: parsed.model ?? frames.find(frame => frame.model)?.model ?? null,
|
|
379
|
+
finishedReason: parsed.finishedReason ?? frames.findLast(frame => frame.finished_reason || frame.stopReason)?.finished_reason ?? execution.reason ?? null };
|
|
380
|
+
} finally { fs.rmSync(temporary, { recursive: true, force: true }); }
|
|
302
381
|
}
|
|
303
382
|
|
|
304
383
|
export async function runWorker(request) {
|
|
@@ -308,7 +387,9 @@ export async function runWorker(request) {
|
|
|
308
387
|
if (!path.isAbsolute(request.cwd || '') || !Array.isArray(request.argv) || request.argv.some(arg => typeof arg !== 'string')) throw new Error('Explicit workspace and argv required');
|
|
309
388
|
const binary = findBinary('total-recall');
|
|
310
389
|
if (!binary) throw new Error('Total Recall CLI is not installed on the selected node');
|
|
311
|
-
const
|
|
390
|
+
const timeoutMs = request.timeoutMs ?? 300000;
|
|
391
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3600000) throw new Error('Direct operation timeout must be between 1 ms and one hour');
|
|
392
|
+
const result = await runProcess(binary, request.argv.slice(1), { cwd: request.cwd, timeoutMs, maxBytes: 16 * 1024 * 1024 });
|
|
312
393
|
return { ok: result.exitCode === 0 && !result.reason, output: result.stdout, error: result.reason || (result.exitCode ? 'CLI command failed' : undefined), exitCode: result.exitCode, commandExecutionVerified: result.exitCode === 0 && !result.reason };
|
|
313
394
|
}
|
|
314
395
|
throw new Error('Unknown meta-harness worker operation');
|
|
@@ -16,6 +16,9 @@ export const headCommit = cwd => must(cwd, ['rev-parse', 'HEAD'], 'Reading HEAD'
|
|
|
16
16
|
export const isDirty = cwd => git(cwd, ['status', '--porcelain']).stdout.trim().length > 0;
|
|
17
17
|
export const diffBetween = (cwd, base, head) => must(cwd, ['diff', `${base}...${head}`], 'Reading the diff');
|
|
18
18
|
|
|
19
|
+
/** Checkouts this process created and has not removed, so termination can clean them up. */
|
|
20
|
+
const ACTIVE_WORKTREES = new Map();
|
|
21
|
+
|
|
19
22
|
/** `branch` creates a new branch from `ref`. Without it, the checkout is detached at `ref` (read-only use). */
|
|
20
23
|
export function createWorktree(cwd, { branch, ref = 'HEAD', prefix = 'meta-harness-' } = {}) {
|
|
21
24
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
@@ -25,16 +28,39 @@ export function createWorktree(cwd, { branch, ref = 'HEAD', prefix = 'meta-harne
|
|
|
25
28
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
26
29
|
throw new Error(`Creating a worktree failed: ${String(result.stderr).trim().slice(0, 300)}`);
|
|
27
30
|
}
|
|
31
|
+
ACTIVE_WORKTREES.set(directory, { cwd, branch: branch || null });
|
|
32
|
+
seedMandatorySkills(cwd, directory);
|
|
28
33
|
return directory;
|
|
29
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* An agent's own Total Recall preflight needs these two files at the repository root, and they are
|
|
37
|
+
* usually gitignored, so a fresh checkout lacks them and every agent inside it would report itself blocked.
|
|
38
|
+
*/
|
|
39
|
+
const MANDATORY_SKILLS = ['.agent/skills/total-recall/SKILL.md', '.agent/skills/ssss/SKILL.md'];
|
|
40
|
+
function seedMandatorySkills(cwd, directory) {
|
|
41
|
+
const root = git(cwd, ['rev-parse', '--show-toplevel']).stdout.trim();
|
|
42
|
+
for (const file of MANDATORY_SKILLS) {
|
|
43
|
+
const source = path.join(root, file), target = path.join(directory, file);
|
|
44
|
+
if (fs.existsSync(source) && !fs.existsSync(target)) {
|
|
45
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
46
|
+
fs.copyFileSync(source, target);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
30
50
|
/** Gates need dependencies. Link them only for the gate, after the agent has finished, so no agent can write through the link. */
|
|
31
51
|
export function linkDependencies(cwd, directory) {
|
|
32
52
|
const modules = path.join(cwd, 'node_modules');
|
|
33
53
|
if (fs.existsSync(modules) && !fs.existsSync(path.join(directory, 'node_modules'))) fs.symlinkSync(modules, path.join(directory, 'node_modules'));
|
|
54
|
+
// The repository's gate configuration is usually gitignored too, and without it the gate could not run here.
|
|
55
|
+
const root = git(cwd, ['rev-parse', '--show-toplevel']).stdout.trim();
|
|
56
|
+
const quality = path.join(root, '.agent/skills/code-quality');
|
|
57
|
+
const target = path.join(directory, '.agent/skills/code-quality');
|
|
58
|
+
if (fs.existsSync(quality) && !fs.existsSync(target)) fs.cpSync(quality, target, { recursive: true, filter: source => !source.includes(`${path.sep}reports`) });
|
|
34
59
|
}
|
|
35
60
|
export const deleteBranch = (cwd, branch) => git(cwd, ['branch', '-D', branch]).status === 0;
|
|
36
61
|
export function removeWorktree(cwd, directory) {
|
|
37
62
|
if (!directory) return;
|
|
63
|
+
ACTIVE_WORKTREES.delete(directory);
|
|
38
64
|
try { if (fs.lstatSync(path.join(directory, 'node_modules')).isSymbolicLink()) fs.rmSync(path.join(directory, 'node_modules')); } catch { /* no link */ }
|
|
39
65
|
git(cwd, ['worktree', 'remove', '--force', directory], 60000);
|
|
40
66
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
@@ -42,6 +68,8 @@ export function removeWorktree(cwd, directory) {
|
|
|
42
68
|
/** Commit whatever the agent left. Returns the commit, or null when it changed nothing. */
|
|
43
69
|
export function commitAll(directory, message) {
|
|
44
70
|
must(directory, ['add', '-A', '--', '.', ':!node_modules'], 'Staging agent changes');
|
|
71
|
+
// Seeded skills are scaffolding, not the agent's work. A repository that tracks them keeps normal history.
|
|
72
|
+
for (const file of MANDATORY_SKILLS) if (git(directory, ['cat-file', '-e', `HEAD:${file}`]).status !== 0) git(directory, ['rm', '--cached', '-q', '--ignore-unmatch', '--', file]);
|
|
45
73
|
if (git(directory, ['diff', '--cached', '--quiet']).status === 0) return null;
|
|
46
74
|
const staged = git(directory, ['diff', '--cached', '--name-only']).stdout.split('\n').filter(Boolean);
|
|
47
75
|
const secret = staged.filter(file => SECRET_FILE.test(path.basename(file)));
|
|
@@ -49,3 +77,10 @@ export function commitAll(directory, message) {
|
|
|
49
77
|
must(directory, ['commit', '-m', message], 'Committing agent changes');
|
|
50
78
|
return headCommit(directory);
|
|
51
79
|
}
|
|
80
|
+
/** A stopped run's checkouts and unfinished branches go with it. A branch already handed back has no active checkout. */
|
|
81
|
+
export function removeActiveWorktrees() {
|
|
82
|
+
for (const [directory, { cwd, branch }] of [...ACTIVE_WORKTREES]) {
|
|
83
|
+
removeWorktree(cwd, directory);
|
|
84
|
+
if (branch) deleteBranch(cwd, branch);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -37,6 +37,12 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
37
37
|
this.reviewVerdict = null;
|
|
38
38
|
this.reviewing = false;
|
|
39
39
|
this.reviewBase = 'main';
|
|
40
|
+
this.reviewAuthor = '';
|
|
41
|
+
this.coordinationInput = '';
|
|
42
|
+
this.coordinationMode = 'pipeline';
|
|
43
|
+
this.coordinationResult = null;
|
|
44
|
+
this.documentation = null;
|
|
45
|
+
this.changelog = null;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
connectedCallback() {
|
|
@@ -56,13 +62,15 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
56
62
|
await this.refreshBenchmarks();
|
|
57
63
|
} else if (screen === 'threads' && this.threads.length === 0) {
|
|
58
64
|
await this.refreshThreads();
|
|
65
|
+
} else if (screen === 'docs') {
|
|
66
|
+
await this.refreshDocsPanel();
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
async refreshFleet() {
|
|
63
71
|
if (!this.host) return;
|
|
64
72
|
try {
|
|
65
|
-
const res = await this.host.run('status', ['--json']);
|
|
73
|
+
const res = await this.host.run('status', ['--node', this.selectedNode, '--json']);
|
|
66
74
|
let parsed = [];
|
|
67
75
|
if (Array.isArray(res)) {
|
|
68
76
|
parsed = res;
|
|
@@ -110,6 +118,7 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
110
118
|
if (this.screen === 'quotas') return this.refreshQuotas();
|
|
111
119
|
if (this.screen === 'benchmarks') return this.refreshBenchmarks();
|
|
112
120
|
if (this.screen === 'threads') return this.refreshThreads();
|
|
121
|
+
if (this.screen === 'docs') return this.refreshDocsPanel();
|
|
113
122
|
return this.refreshFleet();
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -144,12 +153,12 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
|
|
147
|
-
async executeRoute(task) {
|
|
156
|
+
async executeRoute(task, execute = false) {
|
|
148
157
|
if (!this.host || !task) return;
|
|
149
158
|
this.executing = true;
|
|
150
159
|
this.render();
|
|
151
160
|
try {
|
|
152
|
-
const res = await this.host.run('route', [task, '--json']);
|
|
161
|
+
const res = await this.host.run('route', [task, '--node', this.selectedNode, ...(execute ? ['--execute', '--gate', '--background'] : []), '--json']);
|
|
153
162
|
this.routeResult = typeof res === 'string' ? JSON.parse(res) : res;
|
|
154
163
|
} catch (err) {
|
|
155
164
|
this.routeResult = { ok: false, error: err.message };
|
|
@@ -182,7 +191,11 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
182
191
|
this.reviewing = true;
|
|
183
192
|
this.render();
|
|
184
193
|
try {
|
|
185
|
-
const
|
|
194
|
+
const thread = this.threads.find(item => item.id === this.selectedThreadId);
|
|
195
|
+
const implementer = thread?.turns?.findLast(turn => /implementer/i.test(turn.role || ''))?.agent;
|
|
196
|
+
const author = this.reviewAuthor || implementer || (thread?.platform !== 'universal' ? thread?.platform : null);
|
|
197
|
+
if (!author) throw new Error('Select the author before requesting independent review');
|
|
198
|
+
const res = await this.host.run('review', [`${base}...HEAD`, '--author', author, '--node', this.selectedNode, ...(thread ? ['--thread', thread.id] : []), '--json']);
|
|
186
199
|
this.reviewVerdict = typeof res === 'string' ? JSON.parse(res) : res;
|
|
187
200
|
} catch (err) {
|
|
188
201
|
this.reviewVerdict = { ok: false, error: err.message };
|
|
@@ -216,7 +229,8 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
216
229
|
this.executing = true;
|
|
217
230
|
this.render();
|
|
218
231
|
try {
|
|
219
|
-
await this.host.run('docs-ensure', ['--json']);
|
|
232
|
+
await this.host.run('docs-ensure', [...(this.selectedAgent ? ['--agent', this.selectedAgent] : []), '--json']);
|
|
233
|
+
if (this.screen === 'docs') await this.refreshDocsPanel();
|
|
220
234
|
await this.refreshFleet();
|
|
221
235
|
} catch {
|
|
222
236
|
await this.refreshFleet();
|
|
@@ -226,6 +240,36 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
226
240
|
}
|
|
227
241
|
}
|
|
228
242
|
|
|
243
|
+
async executeCoordination(execute = false) {
|
|
244
|
+
if (!this.host || !this.coordinationInput.trim() || this.executing) return;
|
|
245
|
+
this.executing = true;
|
|
246
|
+
this.render();
|
|
247
|
+
try {
|
|
248
|
+
const value = await this.host.run('coordinate', [this.coordinationInput, '--mode', this.coordinationMode, '--node', this.selectedNode, ...(execute ? ['--execute', '--gate', '--background'] : []), '--json']);
|
|
249
|
+
this.coordinationResult = typeof value === 'string' ? JSON.parse(value) : value;
|
|
250
|
+
} catch (error) { this.coordinationResult = { ok: false, error: error.message }; }
|
|
251
|
+
finally { this.executing = false; this.render(); }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async refreshDocsPanel() {
|
|
255
|
+
if (!this.host) return;
|
|
256
|
+
this.loadError = null;
|
|
257
|
+
// docs-status exits 1 while any source is stale. That is a real status to show, not a failure to read it.
|
|
258
|
+
const asStatus = text => { try { const parsed = JSON.parse(String(text).trim()); return Array.isArray(parsed?.agents) ? parsed : null; } catch { return null; } };
|
|
259
|
+
try {
|
|
260
|
+
const value = await this.host.run('docs-status', ['--json']);
|
|
261
|
+
this.documentation = typeof value === 'string' ? JSON.parse(value) : value;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
this.documentation = asStatus(error?.message);
|
|
264
|
+
if (!this.documentation) this.loadError = error?.message || 'Documentation status could not be read';
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const changelog = this.selectedAgent ? await this.host.run('changelog', [this.selectedAgent, '--json']) : null;
|
|
268
|
+
this.changelog = typeof changelog === 'string' ? JSON.parse(changelog) : changelog;
|
|
269
|
+
} catch (error) { this.changelog = null; this.loadError = this.loadError || error?.message || 'Changelog could not be read'; }
|
|
270
|
+
this.render();
|
|
271
|
+
}
|
|
272
|
+
|
|
229
273
|
render() {
|
|
230
274
|
const root = this.shadowRoot;
|
|
231
275
|
if (!root) return;
|
|
@@ -332,6 +376,7 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
332
376
|
/* Screen Navigation Tabs */
|
|
333
377
|
.tabs-bar {
|
|
334
378
|
display: flex;
|
|
379
|
+
flex-wrap: wrap;
|
|
335
380
|
gap: 8px;
|
|
336
381
|
margin-bottom: 14px;
|
|
337
382
|
border-bottom: 1px solid var(--border, var(--face-line));
|
|
@@ -625,8 +670,11 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
625
670
|
|
|
626
671
|
.input-row {
|
|
627
672
|
display: flex;
|
|
673
|
+
flex-wrap: wrap;
|
|
674
|
+
align-items: center;
|
|
628
675
|
gap: 8px;
|
|
629
676
|
}
|
|
677
|
+
.input-row .task-input { min-width: 0; }
|
|
630
678
|
|
|
631
679
|
.task-input {
|
|
632
680
|
flex: 1;
|
|
@@ -831,6 +879,8 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
831
879
|
<button class="tab-btn" data-active="${this.screen === 'benchmarks'}" data-screen="benchmarks" type="button">Benchmarks</button>
|
|
832
880
|
<button class="tab-btn" data-active="${this.screen === 'router'}" data-screen="router" type="button">Smart Router</button>
|
|
833
881
|
<button class="tab-btn" data-active="${this.screen === 'threads'}" data-screen="threads" type="button">Threads & Review</button>
|
|
882
|
+
<button class="tab-btn" data-active="${this.screen === 'coordination'}" data-screen="coordination" type="button">Coordination</button>
|
|
883
|
+
<button class="tab-btn" data-active="${this.screen === 'docs'}" data-screen="docs" type="button">Docs & Changes</button>
|
|
834
884
|
</div>
|
|
835
885
|
|
|
836
886
|
<div class="node-bar">
|
|
@@ -1001,6 +1051,7 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
1001
1051
|
<div class="input-row">
|
|
1002
1052
|
<input class="task-input" id="task-input" type="text" placeholder="Enter task to evaluate complexity and optimal harness..." value="${escapeHtml(this.routeInput)}" />
|
|
1003
1053
|
<button class="btn-action" id="btn-route" type="button" ${this.executing ? 'disabled' : ''}>Evaluate Route</button>
|
|
1054
|
+
<button class="btn-action" id="btn-route-run" type="button" ${this.executing ? 'disabled' : ''}>Run with gates</button>
|
|
1004
1055
|
</div>
|
|
1005
1056
|
|
|
1006
1057
|
${this.routeResult ? `
|
|
@@ -1029,6 +1080,36 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
1029
1080
|
</div>
|
|
1030
1081
|
` : ''}
|
|
1031
1082
|
|
|
1083
|
+
${this.screen === 'coordination' ? `
|
|
1084
|
+
<div class="router-panel">
|
|
1085
|
+
<label for="coordination-task">Task</label>
|
|
1086
|
+
<textarea class="task-input" id="coordination-task" rows="3">${escapeHtml(this.coordinationInput)}</textarea>
|
|
1087
|
+
<div class="input-row">
|
|
1088
|
+
<label for="coordination-mode">Workflow</label>
|
|
1089
|
+
<select class="task-input" id="coordination-mode">${['pipeline', 'council', 'race'].map(mode => `<option value="${mode}" ${this.coordinationMode === mode ? 'selected' : ''}>${mode === 'pipeline' ? 'Plan, implement and verify' : mode === 'council' ? 'Parallel independent assessment' : 'Parallel verified candidates'}</option>`).join('')}</select>
|
|
1090
|
+
<button class="btn-action" id="btn-coordinate-plan" ${this.executing ? 'disabled' : ''}>Preview plan</button>
|
|
1091
|
+
<button class="btn-action" id="btn-coordinate-run" ${this.executing ? 'disabled' : ''}>Run with gates</button>
|
|
1092
|
+
</div>
|
|
1093
|
+
${this.coordinationResult ? `<div class="inspector-panel">
|
|
1094
|
+
<h4>${escapeHtml(this.coordinationResult.error || (this.coordinationResult.background ? 'Run started' : this.coordinationResult.ok ? 'Workflow ready' : 'Workflow failed'))}</h4>
|
|
1095
|
+
${this.coordinationResult.pid ? `<p>Process ${escapeHtml(this.coordinationResult.pid)}. Follow its progress in Threads.</p>` : ''}
|
|
1096
|
+
${(this.coordinationResult.stages || []).map(stage => `<div class="property-box"><strong>${escapeHtml(stage.role || stage.name || stage.agent)}</strong><span>${escapeHtml(stage.agent || DASH)} · ${stage.passed === true ? 'Passed' : stage.passed === false ? 'Failed' : 'Planned'}</span></div>`).join('')}
|
|
1097
|
+
${this.coordinationResult.branch ? `<p>Branch: ${escapeHtml(this.coordinationResult.branch)}</p>` : ''}
|
|
1098
|
+
</div>` : '<p>Preview the assigned agents, then start isolated work with verification.</p>'}
|
|
1099
|
+
</div>
|
|
1100
|
+
` : ''}
|
|
1101
|
+
|
|
1102
|
+
${this.screen === 'docs' ? `
|
|
1103
|
+
<div class="router-panel">
|
|
1104
|
+
<h4>Documentation coverage</h4>
|
|
1105
|
+
<div class="property-grid">${(this.documentation?.agents || []).map(agent => `<div class="property-box"><strong>${escapeHtml(agent.id)}</strong><span>${agent.fresh ? 'Current' : agent.complete ? 'Stale' : 'Unavailable'} · ${escapeHtml(agent.pages)} pages</span><span>${escapeHtml(agent.error || agent.checkedAt || DASH)}</span></div>`).join('') || '<p>No documentation report loaded.</p>'}</div>
|
|
1106
|
+
<label for="docs-agent">Harness</label>
|
|
1107
|
+
<select class="task-input" id="docs-agent">${this.agents.map(agent => `<option value="${escapeHtml(agent.id)}" ${this.selectedAgent === agent.id ? 'selected' : ''}>${escapeHtml(agent.name || agent.id)}</option>`).join('')}</select>
|
|
1108
|
+
<button class="btn-action" id="btn-changelog">Read changes</button>
|
|
1109
|
+
${this.changelog ? `<div class="inspector-panel"><h4>Upstream changes</h4><pre style="white-space:pre-wrap;overflow-wrap:anywhere;max-height:260px;overflow:auto">${escapeHtml(this.changelog.error || this.changelog.changelog || this.changelog.content || this.changelog.output || this.changelog.note || 'No changelog content returned')}</pre></div>` : ''}
|
|
1110
|
+
</div>
|
|
1111
|
+
` : ''}
|
|
1112
|
+
|
|
1032
1113
|
${this.screen === 'threads' ? `
|
|
1033
1114
|
<div class="threads-grid">
|
|
1034
1115
|
<div class="threads-list">
|
|
@@ -1055,6 +1136,7 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
1055
1136
|
<p class="plugin-subtitle" style="margin:0 0 10px 0;">Audits branch diff against base using a strictly distinct model family to eliminate confirmation bias.</p>
|
|
1056
1137
|
<div class="input-row">
|
|
1057
1138
|
<input class="task-input" id="review-base-input" type="text" placeholder="Base branch (default: main)" value="${escapeHtml(this.reviewBase)}" />
|
|
1139
|
+
<select class="task-input" id="review-author" aria-label="Code author"><option value="">Author from selected thread</option>${this.agents.map(agent => `<option value="${escapeHtml(agent.id)}" ${this.reviewAuthor === agent.id ? 'selected' : ''}>${escapeHtml(agent.name || agent.id)}</option>`).join('')}</select>
|
|
1058
1140
|
<button class="btn-action" id="btn-run-review" type="button" ${this.reviewing ? 'disabled' : ''}>
|
|
1059
1141
|
${this.reviewing ? 'Auditing...' : 'Run Review'}
|
|
1060
1142
|
</button>
|
|
@@ -1157,6 +1239,13 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
1157
1239
|
root.querySelector('#btn-route')?.addEventListener('click', () => {
|
|
1158
1240
|
if (this.routeInput) this.executeRoute(this.routeInput);
|
|
1159
1241
|
});
|
|
1242
|
+
root.querySelector('#btn-route-run')?.addEventListener('click', () => this.executeRoute(this.routeInput, true));
|
|
1243
|
+
root.querySelector('#coordination-task')?.addEventListener('input', event => { this.coordinationInput = event.target.value; });
|
|
1244
|
+
root.querySelector('#coordination-mode')?.addEventListener('change', event => { this.coordinationMode = event.target.value; });
|
|
1245
|
+
root.querySelector('#btn-coordinate-plan')?.addEventListener('click', () => this.executeCoordination());
|
|
1246
|
+
root.querySelector('#btn-coordinate-run')?.addEventListener('click', () => this.executeCoordination(true));
|
|
1247
|
+
root.querySelector('#docs-agent')?.addEventListener('change', event => { this.selectedAgent = event.target.value; });
|
|
1248
|
+
root.querySelector('#btn-changelog')?.addEventListener('click', () => this.refreshDocsPanel());
|
|
1160
1249
|
|
|
1161
1250
|
root.querySelectorAll('.thread-card-btn').forEach(btn => {
|
|
1162
1251
|
btn.addEventListener('click', () => {
|
|
@@ -1175,6 +1264,7 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
1175
1264
|
root.querySelector('#btn-run-review')?.addEventListener('click', () => {
|
|
1176
1265
|
this.executeReview(this.reviewBase);
|
|
1177
1266
|
});
|
|
1267
|
+
root.querySelector('#review-author')?.addEventListener('change', event => { this.reviewAuthor = event.target.value; });
|
|
1178
1268
|
}
|
|
1179
1269
|
}
|
|
1180
1270
|
|
|
@@ -15,6 +15,7 @@ import { buildKnowledgeGraph } from './src/graph.mjs';
|
|
|
15
15
|
import { getTokenStatus, setTokenValue } from './src/token.mjs';
|
|
16
16
|
import { lintWikiHubs } from './src/lint.mjs';
|
|
17
17
|
import { generateWiki } from './src/generate.mjs';
|
|
18
|
+
import { retrieveContext } from './src/context-provider.mjs';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Regenerate the generated pages of every registered project that carries them.
|
|
@@ -611,6 +612,16 @@ async function autoRefreshCommand(args = []) {
|
|
|
611
612
|
}
|
|
612
613
|
|
|
613
614
|
const IMPLEMENTED = {};
|
|
615
|
+
IMPLEMENTED.context = async args => {
|
|
616
|
+
if (args.includes('--help') || args.includes('-h')) { console.log('Usage: total-recall openwiki context <task> [--json]'); return; }
|
|
617
|
+
try {
|
|
618
|
+
if (args.some(arg => arg.startsWith('--') && arg !== '--json')) throw new Error('context accepts a task and --json');
|
|
619
|
+
const task = args.filter(arg => arg !== '--json').join(' ').trim();
|
|
620
|
+
if (!task) throw new Error('context requires a task');
|
|
621
|
+
const result = await retrieveContext({ task });
|
|
622
|
+
console.log(args.includes('--json') ? JSON.stringify({ ok: true, ...result }) : result.candidates.map(row => `${row.source.path}:${row.source.startLine}-${row.source.endLine}\n${row.text}`).join('\n\n'));
|
|
623
|
+
} catch (error) { console.log(JSON.stringify({ ok: false, error: error.message })); process.exitCode = 1; }
|
|
624
|
+
};
|
|
614
625
|
IMPLEMENTED.probe = probeCommand;
|
|
615
626
|
IMPLEMENTED.hubs = hubsCommand;
|
|
616
627
|
IMPLEMENTED.ingest = ingestCommand;
|
|
@@ -675,9 +686,10 @@ export async function run(argv = process.argv) {
|
|
|
675
686
|
List indexed documentation hubs across workspace, all vaults, or mesh
|
|
676
687
|
search <query> [--all-vaults] [--node <node>] [--limit <n>] [--json]
|
|
677
688
|
Evidence passage search across knowledge hubs and vaults
|
|
689
|
+
context <task> [--json] Task evidence with useful linked vicinity, provenance and freshness
|
|
678
690
|
graph [--all-vaults] [--node <node>] [--format json|mermaid|dot] [--json]
|
|
679
691
|
Extract knowledge graph topology, nodes, and inter-hub links
|
|
680
|
-
generate [--repo <name>] [--
|
|
692
|
+
generate [--repo <name>] [--force] [--dry-run] [--node <node>] [--json]
|
|
681
693
|
Targeted on-demand documentation generation
|
|
682
694
|
token [status|set <provider> <value>] [--stdin] [--json]
|
|
683
695
|
Check or securely configure Total Recall secrets store keys
|