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.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +3118 -17
  3. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -2
  4. package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +84 -40
  5. package/scaffold/.agent/skills/total-recall/plugins/auth/src/exit-after-flush.mjs +22 -0
  6. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-api-rotation.mjs +61 -6
  7. package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -0
  8. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +8 -0
  9. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +20 -1
  10. package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +3 -0
  11. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +15 -7
  12. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  13. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  14. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  15. package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
  16. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
  17. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +51 -7
  18. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +67 -25
  19. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +4 -4
  20. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +52 -6
  21. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +1 -1
  22. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +21 -8
  23. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/compat-harness.mjs +110 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +48 -22
  25. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +9 -4
  26. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +21 -9
  27. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +9 -5
  28. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +10 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +30 -3
  30. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +6 -1
  31. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +19 -3
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +222 -0
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-worker.mjs +18 -0
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +94 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +70 -0
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +60 -19
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +32 -7
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +24 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +88 -125
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +110 -29
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +35 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +95 -5
  43. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +13 -1
  44. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +5 -5
  45. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +78 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/context-provider.mjs +113 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  48. package/src/cli/agent.mjs +11 -6
  49. package/src/cli/compact.mjs +3 -3
  50. package/src/cli/context.mjs +52 -19
  51. package/src/cli/harness.mjs +65 -147
  52. package/src/cli/init.mjs +4 -2
  53. package/src/cli/preflight.mjs +56 -13
  54. package/src/core/agent-manager.mjs +23 -329
  55. package/src/core/context-compactor.mjs +2 -1
  56. package/src/core/context-compiler.mjs +16 -25
  57. package/src/core/context-providers.mjs +69 -0
  58. package/src/core/meta-harness.mjs +27 -749
  59. package/src/core/plugin-runner.mjs +12 -5
  60. package/src/core/plugin-runtime.mjs +4 -2
  61. package/src/core/runtime.mjs +30 -604
  62. package/src/core/task-context.mjs +122 -0
  63. package/src/server/routes/context.mjs +4 -1
  64. package/scaffold/.agent/skills/decision/SKILL.md +0 -20
  65. package/scaffold/.agent/skills/decision/references/integration.md +0 -25
  66. package/scaffold/.agent/skills/decision/references/use-cases.md +0 -20
  67. package/scaffold/.agent/skills/meta-harness/SKILL.md +0 -15
  68. package/scaffold/.agent/skills/meta-harness/references/optimized/index.md +0 -20
  69. package/scaffold/.agent/skills/meta-harness/references/optimized/manifest.json +0 -79
  70. package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +0 -135
  71. package/scaffold/.agent/skills/meta-harness/references/optimized/section-01.md +0 -5
  72. package/scaffold/.agent/skills/meta-harness/references/optimized/section-02.md +0 -17
  73. package/scaffold/.agent/skills/meta-harness/references/optimized/section-03.md +0 -8
  74. package/scaffold/.agent/skills/meta-harness/references/optimized/section-04.md +0 -2
  75. package/scaffold/.agent/skills/meta-harness/references/optimized/section-05.md +0 -6
  76. package/scaffold/.agent/skills/meta-harness/references/optimized/section-06.md +0 -14
  77. package/scaffold/.agent/skills/meta-harness/references/optimized/section-07.md +0 -8
  78. package/scaffold/.agent/skills/meta-harness/references/optimized/section-08.md +0 -4
  79. package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +0 -20
  80. package/scaffold/.agent/skills/meta-harness/references/optimized/section-10.md +0 -6
  81. package/scaffold/.agent/skills/meta-harness/references/optimized/section-11.md +0 -5
  82. package/scaffold/.agent/skills/meta-harness/references/optimized/section-12.md +0 -7
  83. package/scaffold/.agent/skills/meta-harness/references/optimized/section-13.md +0 -4
  84. package/scaffold/.agent/skills/meta-harness/references/optimized/section-14.md +0 -7
  85. package/scaffold/.agent/skills/meta-harness/references/optimized/section-15.md +0 -9
  86. 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://block.github.io/goose/' },
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
- const spec = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
56
- if (spec && typeof spec.id === 'string' && typeof spec.binary === 'string') {
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
- id: spec.id,
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 clock = String(text).match(/try again at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
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 ${cwd}, so dispatch was refused${detail ? `: ${detail.slice(0, 300)}` : ''}`);
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, '--budget', '24000', '--format', 'json']));
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
- return `Requester: the operator of this Total Recall workspace.\nUse Total Recall and its owning plugin CLI for this task. The following context and command inventories were obtained automatically on this node before dispatch. Official documentation is cached at ${docs.directory}. Read the applicable cached references before changing an integration.\n\nTask context:\n${capsule.context}\n\nComplete CLI help:\n${help}\n\nInstalled plugin command library:\n${JSON.stringify(plugins)}\n\nComposed command library:\n${JSON.stringify(commands)}\n\nRequested task:\n${request.prompt}`;
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() || Buffer.byteLength(request.prompt) > 65536) throw new Error('Explicit supported agent, absolute workspace and bounded prompt required');
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 prompt = await bootstrap(request, { probe, env });
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
- let input = adapter.input === 'stdin' ? prompt : '', temporary;
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, full CLI/plugin/composed-command inventories, official documentation location and the requested task. Carry out that task using the supplied 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
- try { return { agent: adapter.id, ...parseAgentResult(adapter.id, await probe(binary, args, { cwd: request.cwd, input, timeoutMs: request.timeoutMs || 900000, env: { ...agentEnvironment(env), ...credentials } })) }; }
301
- finally { if (temporary) fs.rmSync(temporary, { recursive: true, force: true }); }
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 result = await runProcess(binary, request.argv.slice(1), { cwd: request.cwd });
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 res = await this.host.run('review', [`${base}...HEAD`, '--json']);
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>] [--path <dir>] [--output <dir>] [--force] [--dry-run] [--node <node>] [--json]
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