total-recall-brain 3.44.0 → 3.45.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/bin/total-recall.mjs +6 -4
- package/docs/ARCHITECTURE.md +10 -7
- package/docs/reference/cli-reference.md +13 -8
- package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +2 -2
- package/frontend/src/pages/PluginsPage.tsx +1 -1
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
- package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +25 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +124 -24
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +147 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +92 -18
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +42 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +26 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +3 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +25 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +285 -40
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +7 -7
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
- package/src/cli/agent.mjs +1 -1
- package/src/cli/command.mjs +41 -2
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/init.mjs +0 -25
- package/src/cli/lessons.mjs +51 -0
- package/src/cli/memory-plugin.mjs +1 -1
- package/src/cli/plugin/copies.mjs +64 -14
- package/src/cli/plugin/create.mjs +6 -7
- package/src/cli/plugin/index.mjs +11 -7
- package/src/cli/plugin/install.mjs +10 -15
- package/src/cli/plugin/remove.mjs +1 -1
- package/src/cli/plugin/search.mjs +2 -2
- package/src/cli/preflight.mjs +19 -6
- package/src/cli/proposals.mjs +1 -1
- package/src/cli/recall.mjs +66 -23
- package/src/cli/remember.mjs +3 -14
- package/src/cli/server.mjs +18 -0
- package/src/cli/startup.mjs +1 -0
- package/src/cli/task.mjs +9 -2
- package/src/cli/update.mjs +6 -2
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +31 -11
- package/src/core/claude-hooks.mjs +3 -1
- package/src/core/cleanup-task.mjs +15 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-maintenance.mjs +4 -1
- package/src/core/cli-semantic-index.mjs +23 -3
- package/src/core/context-compiler.mjs +20 -0
- package/src/core/daemon-loop.mjs +39 -62
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/dream.mjs +33 -105
- package/src/core/ensure-dependencies.mjs +148 -0
- package/src/core/fast-recall.mjs +2 -2
- package/src/core/field-evaluation.mjs +23 -0
- package/src/core/gate-mode.mjs +1 -1
- package/src/core/harness-hooks.mjs +14 -2
- package/src/core/lesson-hooks.mjs +74 -0
- package/src/core/lesson-memory.mjs +24 -0
- package/src/core/lesson-sessions.mjs +52 -0
- package/src/core/lessons.mjs +96 -0
- package/src/core/meta-harness.mjs +2 -2
- package/src/core/optimizer.mjs +66 -165
- package/src/core/plugin-contracts/card.mjs +3 -0
- package/src/core/plugin-loader.mjs +12 -3
- package/src/core/plugin-runtime.mjs +40 -1
- package/src/core/plugin-store.mjs +35 -23
- package/src/core/post-mortem.mjs +52 -36
- package/src/core/preflight-gate.mjs +23 -2
- package/src/core/proposal-applier.mjs +31 -2
- package/src/core/repo-sync.mjs +4 -6
- package/src/core/required-plugins.mjs +26 -4
- package/src/core/rule-length.mjs +23 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/scheduler.mjs +35 -43
- package/src/core/schema.mjs +1 -2
- package/src/core/search.mjs +4 -3
- package/src/core/server-service.mjs +148 -0
- package/src/core/session-watcher.mjs +19 -1
- package/src/core/skill-gap.mjs +16 -0
- package/src/core/ssss-kernel-bridge.mjs +32 -4
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +65 -54
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +1 -0
- package/src/core/vector-field.mjs +134 -192
- package/src/server/invoke-core-child.mjs +24 -0
- package/src/server/invoke-core-operations.mjs +102 -0
- package/src/server/route-manifest.json +0 -56
- package/src/server/routes/brains.mjs +0 -18
- package/src/server/routes/context.mjs +0 -41
- package/src/server/routes/invoke.mjs +26 -1
- package/src/server/routes/ssss.mjs +0 -169
- package/src/server/routes/system.mjs +0 -15
- package/src/server/routes/tasks.mjs +0 -26
- package/src/server/routes/vault.mjs +2 -23
- package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
- package/src/core/append-log.mjs +0 -131
- package/src/core/plugin-tasks.mjs +0 -173
- package/templates/onboarding-interview.md +0 -82
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
5
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
|
|
@@ -172,9 +172,18 @@ export function findBinary(name, env = process.env) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/** Process groups this process started and has not finished, so termination can stop them. */
|
|
175
|
-
const ACTIVE_GROUPS = new
|
|
175
|
+
const ACTIVE_GROUPS = new Map();
|
|
176
|
+
let cancellation = null;
|
|
177
|
+
export const cancellationResult = () => cancellation && { ...cancellation };
|
|
178
|
+
/** Set the stop state before signalling a child, so its exit cannot start another attempt. */
|
|
179
|
+
export function cancelExecution(signal = 'SIGTERM', graceMs = 500) {
|
|
180
|
+
cancellation ||= { ok: false, cancelled: true, failureKind: 'cancelled', signal,
|
|
181
|
+
exitCode: signal === 'SIGINT' ? 130 : signal === 'SIGHUP' ? 129 : 143, error: `Cancelled by ${signal}.` };
|
|
182
|
+
for (const stop of ACTIVE_GROUPS.values()) stop('cancelled', graceMs);
|
|
183
|
+
return cancellationResult();
|
|
184
|
+
}
|
|
176
185
|
export function stopActiveProcesses(signal = 'SIGTERM') {
|
|
177
|
-
for (const pid of ACTIVE_GROUPS) { try { process.kill(process.platform === 'win32' ? pid : -pid, signal); } catch { /* already gone */ } }
|
|
186
|
+
for (const pid of ACTIVE_GROUPS.keys()) { if (typeof pid !== 'number') continue; try { process.kill(process.platform === 'win32' ? pid : -pid, signal); } catch { /* already gone */ } }
|
|
178
187
|
return ACTIVE_GROUPS.size;
|
|
179
188
|
}
|
|
180
189
|
|
|
@@ -263,25 +272,29 @@ function toolEvidence(toolResults) {
|
|
|
263
272
|
};
|
|
264
273
|
}
|
|
265
274
|
|
|
266
|
-
export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000, env = agentEnvironment(), maxBytes = 2 * 1024 * 1024, codexJsonl = false, claudeJsonl = false } = {}) {
|
|
275
|
+
export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000, env = agentEnvironment(), maxBytes = 2 * 1024 * 1024, codexJsonl = false, claudeJsonl = false, piJsonl = false, onFrame } = {}) {
|
|
276
|
+
if (cancellation) return Promise.resolve({ ...cancellationResult(), stdout: '', stderr: '', reason: 'cancelled' });
|
|
267
277
|
return new Promise(resolve => {
|
|
268
278
|
let stdout = '', stderr = '', bytes = 0, reason = null, child, timer, escalation, settled = false;
|
|
269
279
|
const kill = signal => { try { if (process.platform !== 'win32') process.kill(-child.pid, signal); else child.kill(signal); } catch {} };
|
|
270
280
|
const finish = (exitCode, signal) => {
|
|
271
281
|
if (settled) return;
|
|
272
282
|
settled = true; clearTimeout(timer);
|
|
273
|
-
|
|
283
|
+
const complete = () => {
|
|
284
|
+
if (child?.pid) ACTIVE_GROUPS.delete(child.pid);
|
|
285
|
+
resolve({ exitCode, signal, stdout, stderr, reason, ...(reason === 'cancelled' ? cancellationResult() : {}) });
|
|
286
|
+
};
|
|
274
287
|
// A parent can exit on TERM while a descendant still holds the process group.
|
|
275
288
|
// Finish the owned group cleanup before releasing an aborted operation.
|
|
276
289
|
if (reason && escalation) {
|
|
277
290
|
clearTimeout(escalation);
|
|
278
|
-
escalation = setTimeout(() => { kill('SIGKILL');
|
|
279
|
-
} else
|
|
291
|
+
escalation = setTimeout(() => { kill('SIGKILL'); complete(); }, 500);
|
|
292
|
+
} else complete();
|
|
280
293
|
};
|
|
281
|
-
const stop = cause => { if (reason) return; reason = cause; kill('SIGTERM'); escalation = setTimeout(() => kill('SIGKILL'),
|
|
294
|
+
const stop = (cause, graceMs = 500) => { if (reason) return; reason = cause; kill('SIGTERM'); escalation = setTimeout(() => kill('SIGKILL'), graceMs); };
|
|
282
295
|
try { child = spawn(binary, args, { cwd, env, shell: false, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
283
296
|
catch { finish(null); return; }
|
|
284
|
-
if (child.pid) ACTIVE_GROUPS.
|
|
297
|
+
if (child.pid) ACTIVE_GROUPS.set(child.pid, stop);
|
|
285
298
|
timer = setTimeout(() => stop('timeout'), timeoutMs);
|
|
286
299
|
child.on('error', () => { reason = 'spawn-error'; finish(null); });
|
|
287
300
|
child.stdin.on('error', () => {});
|
|
@@ -290,7 +303,7 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
|
|
|
290
303
|
if (output) stdout += part; else stderr += part;
|
|
291
304
|
if (bytes > maxBytes) stop('output-limit');
|
|
292
305
|
};
|
|
293
|
-
if (codexJsonl || claudeJsonl) {
|
|
306
|
+
if (codexJsonl || claudeJsonl || piJsonl) {
|
|
294
307
|
let transportBytes = 0, toolFrames = 0;
|
|
295
308
|
const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
296
309
|
child.stdout.prependListener('data', chunk => {
|
|
@@ -304,6 +317,8 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
|
|
|
304
317
|
if (reason || !line.trim()) return;
|
|
305
318
|
let frame;
|
|
306
319
|
try { frame = JSON.parse(line); } catch { /* Keep malformed data so validation fails. */ }
|
|
320
|
+
// A-107: every parsed frame reaches the live progress log as it arrives.
|
|
321
|
+
if (frame && onFrame) { try { onFrame(frame); } catch { /* progress never stops the run */ } }
|
|
307
322
|
if (codexJsonl && typeof frame?.type === 'string' && frame.type.startsWith('item.') &&
|
|
308
323
|
frame.item?.type !== 'agent_message' && !frameError(frame)) {
|
|
309
324
|
const toolFrame = completedCodexTool(frame);
|
|
@@ -315,6 +330,25 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
|
|
|
315
330
|
if (content.length) collect(Buffer.from(JSON.stringify({ type: frame.type, message: { content } }) + '\n'), true);
|
|
316
331
|
return;
|
|
317
332
|
}
|
|
333
|
+
if (piJsonl && frame?.type === 'tool_execution_end') {
|
|
334
|
+
if (typeof frame.toolName === 'string' && typeof frame.isError === 'boolean' && toolFrames++ < TOOL_RESULT_LIMIT) {
|
|
335
|
+
const result = { content: toolOutput(frame.result?.content) };
|
|
336
|
+
collect(Buffer.from(JSON.stringify({ type: frame.type, toolName: frame.toolName, isError: frame.isError, result }) + '\n'), true);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (piJsonl && frame?.type === 'agent_end' && Array.isArray(frame.messages)) {
|
|
341
|
+
const messages = frame.messages.filter(message => message?.role === 'assistant').map(message => ({
|
|
342
|
+
role: message.role,
|
|
343
|
+
stopReason: message.stopReason,
|
|
344
|
+
content: Array.isArray(message.content) ? message.content.filter(part => part?.type === 'text') : [],
|
|
345
|
+
}));
|
|
346
|
+
const model = frame.messages.findLast(message => message?.role === 'assistant' && typeof message.model === 'string')?.model;
|
|
347
|
+
collect(Buffer.from(JSON.stringify({ type: frame.type, model, messages }) + '\n'), true);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// Pi's progress events can repeat the full partial assistant message on each delta.
|
|
351
|
+
if (piJsonl && frame && frame.type !== 'agent_end' && frame.type !== 'error' && !frameError(frame)) return;
|
|
318
352
|
collect(Buffer.from(line + '\n'), true);
|
|
319
353
|
});
|
|
320
354
|
} else child.stdout.on('data', chunk => collect(chunk, true));
|
|
@@ -358,8 +392,11 @@ function agentFailure(message, stderr = '') {
|
|
|
358
392
|
}
|
|
359
393
|
const PLAIN_TEXT_ADAPTERS = ['ollama', 'openclaw', 'aider', 'goose', 'openhands', 'cline'];
|
|
360
394
|
export function parseAgentResult(id, result) {
|
|
395
|
+
if (result.cancelled || result.reason === 'cancelled') return { ...result, ok: false, cancelled: true, failureKind: 'cancelled', error: result.error || 'Cancelled.' };
|
|
361
396
|
const stderr = String(result.stderr || '').trim().slice(-2000);
|
|
362
|
-
|
|
397
|
+
// A killed run (timeout, output limit) keeps the tail of what the agent printed, so the failure can be diagnosed (A-103).
|
|
398
|
+
const processFailure = result.reason ? { ok: false, error: result.reason, failureKind: 'process',
|
|
399
|
+
partialOutput: String(result.stdout || '').slice(-8000) || null, stderr: stderr || null } : null;
|
|
363
400
|
if (PLAIN_TEXT_ADAPTERS.includes(id) || loadCustomAdapters().some(c => c.id === id)) {
|
|
364
401
|
if (processFailure) return processFailure;
|
|
365
402
|
if (result.exitCode !== 0) return agentFailure(null, stderr || `Agent process exited ${result.exitCode}`);
|
|
@@ -413,7 +450,7 @@ export function parseAgentResult(id, result) {
|
|
|
413
450
|
return typeof text === 'string' && text.trim() ? { ok: true, response: text, ...metadata } : { ok: false, error: 'Agent did not produce a recognized completed result', failureKind: 'agent', ...metadata };
|
|
414
451
|
}
|
|
415
452
|
|
|
416
|
-
async function readOpenClawResult(execution, { binary, cwd, env, probe, sessionId }) {
|
|
453
|
+
export async function readOpenClawResult(execution, { binary, cwd, env, probe, sessionId }) {
|
|
417
454
|
const fallback = parseAgentResult('openclaw', execution);
|
|
418
455
|
let run;
|
|
419
456
|
try { run = JSON.parse(execution.stdout); } catch { /* No structured run to attribute. */ }
|
|
@@ -492,19 +529,18 @@ export async function totalRecallBootstrap(request, { probe = runProcess, env =
|
|
|
492
529
|
await acknowledge(cwd);
|
|
493
530
|
// A receipt is per checkout, and the agent's own gate reads the one where it works.
|
|
494
531
|
if (request.cwd !== cwd) await acknowledge(request.cwd);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const commands = JSON.parse(await invoke(['composable-cli', 'library', '--json']));
|
|
532
|
+
// A-121: give the agent the commands that match its task from the semantic CLI index, not whole
|
|
533
|
+
// inventories (the plugin and composed libraries were about 550 KB together). It looks up anything else.
|
|
498
534
|
const docs = JSON.parse(await invoke(['meta-harness', 'docs-ensure', '--agent', request.agent, '--json']));
|
|
499
535
|
if (docs.ok !== true) throw notReady('Official agent documentation is unavailable or stale');
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
536
|
+
// Node built-ins only here (this file ships to remote nodes): the result is the pretty-printed JSON after any log lines.
|
|
537
|
+
let found = null;
|
|
538
|
+
try { const out = await invoke(['help', task, '--json']); const at = out.indexOf('{\n'); found = JSON.parse(at >= 0 ? out.slice(at) : out); }
|
|
539
|
+
catch (error) { if (error.code === 'NOT_READY') throw error; found = null; }
|
|
540
|
+
const matches = Array.isArray(found?.matches) ? found.matches.slice(0, 12) : [];
|
|
541
|
+
const references = `${matches.length ? `Commands that match this task (Total Recall semantic command search):\n${matches.map(m => `- ${m.verb} — ${m.description}`).join('\n')}\n` : ''}For anything else, run total-recall help "<what you need>" to find the command, then total-recall <namespace> --help for its arguments. Do not load whole command inventories.`;
|
|
506
542
|
const operatingContext = hasEvidence ? capsule.context.slice(0, evidenceStart).trimEnd() : capsule.context;
|
|
507
|
-
const prompt = `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${operatingContext}\n\
|
|
543
|
+
const prompt = `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${operatingContext}\n\nCommands:\n${references}`;
|
|
508
544
|
const requestedTask = `\n\nRequested task:\n${request.prompt}`;
|
|
509
545
|
if (!hasEvidence) return prompt + requestedTask;
|
|
510
546
|
|
|
@@ -555,7 +591,7 @@ export async function inspectAgents({ probe = runProcess, env = process.env, ids
|
|
|
555
591
|
return { agents, evidence: 'Version and login probes only; actual execution requires a completed run' };
|
|
556
592
|
}
|
|
557
593
|
|
|
558
|
-
export async function dispatchAgent(request, { probe = runProcess, env = process.env, bootstrap = totalRecallBootstrap } = {}) {
|
|
594
|
+
export async function dispatchAgent(request, { probe = runProcess, env = process.env, bootstrap = totalRecallBootstrap, sessionRunner } = {}) {
|
|
559
595
|
const adapter = getActiveAdapters().find(item => item.id === request.agent);
|
|
560
596
|
if (!adapter || !path.isAbsolute(request.cwd || '') || typeof request.prompt !== 'string' || !request.prompt.trim()) throw new Error('Explicit supported agent, absolute workspace and prompt required');
|
|
561
597
|
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`);
|
|
@@ -655,29 +691,67 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
655
691
|
}
|
|
656
692
|
}
|
|
657
693
|
const allowWrites = request.permissions?.write === true;
|
|
658
|
-
|
|
694
|
+
// A writing agent gets full access (Greg, 2026-11-09): workspace-write blocked the vault lock,
|
|
695
|
+
// the auth store and the mesh, so routed operational work could not run (A-106).
|
|
696
|
+
// Every launch gets full access (Greg, 2026-11-09). Codex's read-only sandbox and Claude's plan mode
|
|
697
|
+
// block the Keychain, the network and every command, so a read-only job could not unlock the auth
|
|
698
|
+
// store or run the Total Recall CLI (A-110). Read-only work is contained by its disposable checkout
|
|
699
|
+
// (dispatch.mjs), not by the harness sandbox.
|
|
700
|
+
if (adapter.id === 'codex') args[args.indexOf('--sandbox') + 1] = 'danger-full-access';
|
|
659
701
|
if (billing && adapter.id === 'codex') args.unshift(...CODEX_PLAN_ARGS);
|
|
660
702
|
if (claudePlan) args.unshift(...CLAUDE_PLAN_ARGS);
|
|
661
|
-
|
|
703
|
+
// acceptEdits denied every command a headless run cannot approve, including the preflight and tests (A-104).
|
|
704
|
+
if (adapter.id === 'claude') args[args.indexOf('--permission-mode') + 1] = 'bypassPermissions';
|
|
662
705
|
if (adapter.id === 'agy') args[args.indexOf('--mode') + 1] = allowWrites ? 'accept-edits' : 'plan';
|
|
663
706
|
if (adapter.id === 'muse' && !allowWrites) args.push('--disable-write');
|
|
664
707
|
if (adapter.id === 'pi') args[args.indexOf('--tools') + 1] = allowWrites ? 'read,bash,edit,write' : 'read';
|
|
665
708
|
if (adapter.id === 'ollama') args.push(env.TR_OLLAMA_MODEL || state.chatModels[0]);
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
709
|
+
const progressFile = request.progressFile || path.join(
|
|
710
|
+
env.TR_SECRETS_BRAIN || path.join(env.AGENT_DIR || path.join(env.HOME || os.homedir(), '.agent'), 'skills/total-recall'),
|
|
711
|
+
'config/meta-harness/jobs', `${request.jobId || process.env.TR_METAHARNESS_JOB || `session-${randomUUID()}`}.events.jsonl`);
|
|
712
|
+
const progress = createProgress({ file: progressFile, harness: adapter.id,
|
|
713
|
+
onStall: reason => sendStallAlert(`Metaharness: ${adapter.id} stalled`, `${reason}. Live log: ${progressFile}`) });
|
|
714
|
+
const ownerId = request.jobId || process.env.TR_METAHARNESS_JOB;
|
|
715
|
+
const ownerFile = ownerId ? path.join(path.dirname(progressFile), `${ownerId}.events.jsonl`) : progressFile;
|
|
716
|
+
const ownerProgress = ownerFile !== progressFile ? createProgress({ file: ownerFile, harness: adapter.id }) : null;
|
|
717
|
+
const liveProgress = ownerProgress ? { event: frame => { progress.event(frame); ownerProgress.event(frame); } } : progress;
|
|
718
|
+
const { runSession, sessionOptions } = await import('./session/run.mjs');
|
|
719
|
+
const savedRequest = Object.fromEntries(['agent', 'model', 'tools', 'settingSources', 'allowedTools', 'permissions', 'allowUnknownAuth',
|
|
720
|
+
'goal', 'maxTurns', 'maxTokens', 'maxBudgetUsd', 'resultSchema', 'approvalPolicy', 'questionTimeoutMs', 'effort', 'fallbackModel'].filter(key => request[key] !== undefined).map(key => [key, request[key]]));
|
|
721
|
+
if (savedRequest.resultSchema !== undefined) savedRequest.resultSchema = sessionOptions(request).resultSchema;
|
|
722
|
+
const snapshot = JSON.stringify({ workspace: request.contextCwd || request.cwd, branch: request.branch, request: savedRequest });
|
|
723
|
+
fs.writeFileSync(progressFile.replace('.events.jsonl', '.session.json'), snapshot, { mode: 0o600 });
|
|
724
|
+
if (ownerProgress) fs.writeFileSync(ownerFile.replace('.events.jsonl', '.session.json'), snapshot, { mode: 0o600 });
|
|
725
|
+
let parsed;
|
|
726
|
+
const frames = [];
|
|
727
|
+
const captureFrame = frame => {
|
|
728
|
+
if (frames.length >= 80) return;
|
|
729
|
+
if (adapter.id === 'claude') {
|
|
730
|
+
const content = claudeToolBlocks(frame);
|
|
731
|
+
if (content.length) frames.push({ type: frame.type, message: { content } });
|
|
732
|
+
} else if (adapter.id === 'codex') {
|
|
733
|
+
const item = frame.method === 'item/completed' ? frame.params?.item : null;
|
|
734
|
+
if (item?.type === 'commandExecution' && Number.isInteger(item.exitCode)) frames.push({ type: 'item.completed',
|
|
735
|
+
item: { type: 'command_execution', status: item.status, exit_code: item.exitCode, aggregated_output: toolOutput(item.aggregatedOutput) } });
|
|
736
|
+
} else if (['tool_call', 'tool_result', 'tool_execution_end'].includes(frame.type)) frames.push(frame);
|
|
737
|
+
};
|
|
738
|
+
try {
|
|
739
|
+
parsed = await (sessionRunner || runSession)({ request, adapter, binary, args,
|
|
740
|
+
env: { ...agentEnvironment(env), ...credentials, TR_META_HARNESS_RUN: '1' }, prompt, progress: liveProgress, onFrame: captureFrame,
|
|
741
|
+
register: stop => { const id = Symbol('session'); ACTIVE_GROUPS.set(id, stop); return () => ACTIVE_GROUPS.delete(id); } });
|
|
742
|
+
if (cancellationResult()) parsed = { ...parsed, ...cancellationResult() };
|
|
743
|
+
if (frames.length) Object.assign(parsed, toolEvidence(nativeToolResults(adapter.id, frames)));
|
|
744
|
+
if (openClawSession && parsed.execution) {
|
|
745
|
+
const evidence = await readOpenClawResult(parsed.execution, { binary, cwd: request.cwd, env: agentEnvironment(env), probe, sessionId: openClawSession });
|
|
746
|
+
Object.assign(parsed, toolEvidence(evidence.toolResults || []), { provider: evidence.provider, toolEvidenceError: evidence.toolEvidenceError });
|
|
747
|
+
if (!evidence.ok) Object.assign(parsed, { ok: false, error: evidence.error, failureKind: evidence.failureKind });
|
|
748
|
+
delete parsed.execution;
|
|
749
|
+
}
|
|
750
|
+
} catch (error) { parsed = { ok: false, error: error.message, failureKind: error.failureKind || 'agent' }; }
|
|
751
|
+
finally { progress.close(parsed); ownerProgress?.close(parsed); }
|
|
752
|
+
return { agent: adapter.id, ...parsed, progressFile, statusFile: progress.statusFile,
|
|
753
|
+
sessionId: parsed.sessionId || progress.status().session_id,
|
|
754
|
+
exitCode: parsed.ok ? 0 : parsed.exitCode ?? null };
|
|
681
755
|
} finally { fs.rmSync(temporary, { recursive: true, force: true }); }
|
|
682
756
|
}
|
|
683
757
|
|
|
@@ -709,3 +783,174 @@ export async function runWorker(request) {
|
|
|
709
783
|
}
|
|
710
784
|
throw new Error('Unknown meta-harness worker operation');
|
|
711
785
|
}
|
|
786
|
+
|
|
787
|
+
// ─── Live progress (A-107) ─────────────────────────────────────────────────────
|
|
788
|
+
// Kept inside this file: executor.mjs ships worker.mjs whole to remote nodes, so it may
|
|
789
|
+
// import only Node built-ins.
|
|
790
|
+
/**
|
|
791
|
+
* Live visibility into a dispatched agent (A-107).
|
|
792
|
+
*
|
|
793
|
+
* Claude Code (`--output-format stream-json --verbose`) and Codex (`exec --json`) stream every
|
|
794
|
+
* event while they run. Each event, normalized to one line, goes to the job's event log as it
|
|
795
|
+
* arrives; a status record sits beside it; a watchdog flags a stalled run.
|
|
796
|
+
* <job>.events.jsonl one JSON line per event: { t, kind, text }
|
|
797
|
+
* <job>.status.json { state, harness, session_id, started_at, last_event_at, events, current, in_progress, result }
|
|
798
|
+
*/
|
|
799
|
+
export const STALL_MS = 5 * 60_000;
|
|
800
|
+
export const LONG_STEP_MS = 30 * 60_000;
|
|
801
|
+
const CHECK_MS = 30_000;
|
|
802
|
+
const TEXT_MAX = 300;
|
|
803
|
+
|
|
804
|
+
const clip = value => {
|
|
805
|
+
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
806
|
+
return text.length > TEXT_MAX ? `${text.slice(0, TEXT_MAX - 1)}…` : text;
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
/** One readable event per agent frame, or null for frames that carry nothing a person needs. */
|
|
810
|
+
export function normalizeFrame(harness, frame) {
|
|
811
|
+
if (!frame || typeof frame !== 'object') return null;
|
|
812
|
+
if (typeof frame.kind === 'string') {
|
|
813
|
+
const entry = { ...frame, text: clip(toolOutput(frame.text)) };
|
|
814
|
+
// Native tool frames can be megabytes long and contain command output.
|
|
815
|
+
// The worker keeps bounded tool evidence separately from the live view.
|
|
816
|
+
delete entry.item;
|
|
817
|
+
delete entry.input;
|
|
818
|
+
delete entry.response;
|
|
819
|
+
delete entry.structured;
|
|
820
|
+
delete entry.server;
|
|
821
|
+
if (entry.request) entry.request = toolOutput(JSON.stringify(entry.request));
|
|
822
|
+
return entry;
|
|
823
|
+
}
|
|
824
|
+
if (harness === 'codex') {
|
|
825
|
+
const item = frame.item || {};
|
|
826
|
+
switch (frame.type) {
|
|
827
|
+
case 'thread.started': return { kind: 'session', text: `Codex session ${frame.thread_id || ''}`.trim(), session: frame.thread_id || null };
|
|
828
|
+
case 'turn.started': return { kind: 'turn', text: 'Turn started' };
|
|
829
|
+
case 'turn.completed': return { kind: 'usage', text: `Turn completed (${frame.usage?.input_tokens ?? '?'} in, ${frame.usage?.output_tokens ?? '?'} out tokens)` };
|
|
830
|
+
case 'turn.failed': return { kind: 'error', text: `Turn failed: ${clip(frame.error?.message || frame.error)}` };
|
|
831
|
+
case 'error': return { kind: 'error', text: `Error: ${clip(frame.message || frame.error)}` };
|
|
832
|
+
case 'item.started':
|
|
833
|
+
if (item.type === 'command_execution') return { kind: 'step-start', text: `Running: ${clip(item.command)}` };
|
|
834
|
+
if (item.type === 'mcp_tool_call') return { kind: 'step-start', text: `Calling ${item.server || ''}.${item.tool || ''}` };
|
|
835
|
+
return null;
|
|
836
|
+
case 'item.completed':
|
|
837
|
+
if (item.type === 'command_execution') return { kind: 'step-end', text: `Ran: ${clip(item.command)} (exit ${item.exit_code ?? '?'})` };
|
|
838
|
+
if (item.type === 'mcp_tool_call') return { kind: 'step-end', text: `Called ${item.server || ''}.${item.tool || ''}` };
|
|
839
|
+
if (item.type === 'file_change') return { kind: 'edit', text: `Changed ${clip((item.changes || []).map(change => change.path).join(', '))}` };
|
|
840
|
+
if (item.type === 'agent_message') return { kind: 'message', text: clip(item.text) };
|
|
841
|
+
if (item.type === 'web_search') return { kind: 'search', text: `Searched: ${clip(item.query)}` };
|
|
842
|
+
if (item.type === 'todo_list') return { kind: 'plan', text: `Plan: ${clip((item.items || []).map(entry => `${entry.completed ? '✓' : '·'} ${entry.text}`).join('; '))}` };
|
|
843
|
+
return null;
|
|
844
|
+
default: return null;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (harness === 'claude') {
|
|
848
|
+
if (frame.type === 'system' && frame.subtype === 'init') return { kind: 'session', text: `Claude session ${frame.session_id || ''} (${frame.model || 'model unknown'})`.trim(), session: frame.session_id || null };
|
|
849
|
+
if (frame.type === 'system' && frame.subtype === 'api_retry') return { kind: 'retry', text: `API retry ${frame.attempt}/${frame.max_retries}: ${frame.error || 'unknown'}` };
|
|
850
|
+
if (frame.type === 'system' && frame.subtype === 'permission_denied') return { kind: 'error', text: `Permission denied: ${clip(frame.tool_name || frame.message)}` };
|
|
851
|
+
if (frame.type === 'result') return { kind: frame.is_error ? 'error' : 'usage', text: `Result: ${frame.subtype || ''}${frame.total_cost_usd !== undefined ? `, cost $${frame.total_cost_usd}` : ''}`, session: frame.session_id || null };
|
|
852
|
+
const blocks = Array.isArray(frame.message?.content) ? frame.message.content : [];
|
|
853
|
+
if (frame.type === 'assistant') {
|
|
854
|
+
const tool = blocks.find(block => block.type === 'tool_use');
|
|
855
|
+
if (tool) return { kind: 'step-start', text: `${tool.name}: ${clip(tool.input?.command || tool.input?.file_path || tool.input?.pattern || tool.input?.description || JSON.stringify(tool.input || {}))}` };
|
|
856
|
+
const text = blocks.filter(block => block.type === 'text').map(block => block.text).join(' ');
|
|
857
|
+
return text.trim() ? { kind: 'message', text: clip(text) } : null;
|
|
858
|
+
}
|
|
859
|
+
if (frame.type === 'user' && blocks.some(block => block.type === 'tool_result')) {
|
|
860
|
+
const failed = blocks.some(block => block.type === 'tool_result' && block.is_error);
|
|
861
|
+
return { kind: 'step-end', text: failed ? 'Tool returned an error' : 'Tool finished' };
|
|
862
|
+
}
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Track one dispatched run. `event(frame)` records an agent frame; `close(result)` records the
|
|
870
|
+
* outcome. A watchdog marks the run stalled once, and calls `onStall`, when no event arrives for
|
|
871
|
+
* `stallMs` while no step is running, or one step runs past `longStepMs`.
|
|
872
|
+
*/
|
|
873
|
+
export function createProgress({ file, harness, stallMs = STALL_MS, longStepMs = LONG_STEP_MS, onStall, now = () => Date.now(), checkMs = CHECK_MS } = {}) {
|
|
874
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
875
|
+
const statusFile = file.replace(/\.events\.jsonl$/, '') + '.status.json';
|
|
876
|
+
const status = { state: 'running', harness, pid: process.pid, session_id: null, started_at: new Date(now()).toISOString(), last_event_at: null,
|
|
877
|
+
events: 0, current: null, in_progress: 0, step_started_at: null, events_file: file, result: null };
|
|
878
|
+
const saveStatus = () => {
|
|
879
|
+
const temp = `${statusFile}.${process.pid}.tmp`;
|
|
880
|
+
try { fs.writeFileSync(temp, JSON.stringify(status, null, 2), { mode: 0o600 }); fs.renameSync(temp, statusFile); } catch { /* status is best effort; events stay authoritative */ }
|
|
881
|
+
};
|
|
882
|
+
const write = (kind, text, fields = {}) => {
|
|
883
|
+
const at = new Date(now()).toISOString();
|
|
884
|
+
try { fs.appendFileSync(file, JSON.stringify({ ...fields, t: at, kind, text }) + '\n', { mode: 0o600 }); } catch { /* keep the run going */ }
|
|
885
|
+
status.events += 1; status.last_event_at = at;
|
|
886
|
+
};
|
|
887
|
+
write('start', `${harness} started`);
|
|
888
|
+
saveStatus();
|
|
889
|
+
let stalled = false;
|
|
890
|
+
const check = () => {
|
|
891
|
+
if (status.state !== 'running') return;
|
|
892
|
+
const idle = now() - new Date(status.last_event_at).getTime();
|
|
893
|
+
const stepAge = status.step_started_at ? now() - new Date(status.step_started_at).getTime() : 0;
|
|
894
|
+
const reason = status.in_progress > 0
|
|
895
|
+
? (stepAge > longStepMs ? `One step has run ${Math.round(stepAge / 60000)} min: ${status.current}` : null)
|
|
896
|
+
: (idle > stallMs ? `No agent event for ${Math.round(idle / 60000)} min` : null);
|
|
897
|
+
if (reason && !stalled) {
|
|
898
|
+
stalled = true; status.state = 'stalled'; write('stall', reason); saveStatus();
|
|
899
|
+
try { onStall?.(reason, { ...status }); } catch { /* alerting never stops the run */ }
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
const timer = setInterval(check, checkMs); timer.unref?.();
|
|
903
|
+
return {
|
|
904
|
+
statusFile,
|
|
905
|
+
status: () => ({ ...status }),
|
|
906
|
+
check,
|
|
907
|
+
event(frame) {
|
|
908
|
+
const entry = normalizeFrame(harness, frame);
|
|
909
|
+
if (!entry) return;
|
|
910
|
+
if (entry.session || entry.sessionId) status.session_id = entry.session || entry.sessionId;
|
|
911
|
+
if (entry.capabilities) status.capabilities = entry.capabilities;
|
|
912
|
+
if (entry.kind === 'usage') status.usage = { ...entry };
|
|
913
|
+
if (entry.kind === 'session') status.cwd = entry.cwd || status.cwd;
|
|
914
|
+
if (entry.kind === 'step-start') { status.in_progress += 1; status.step_started_at = new Date(now()).toISOString(); }
|
|
915
|
+
if (entry.kind === 'step-end') { status.in_progress = Math.max(0, status.in_progress - 1); if (!status.in_progress) status.step_started_at = null; }
|
|
916
|
+
if (['step-start', 'message', 'edit', 'plan', 'search'].includes(entry.kind)) status.current = entry.text;
|
|
917
|
+
// A new event after a stall means the agent is working again.
|
|
918
|
+
if (stalled && status.state === 'stalled') { stalled = false; status.state = 'running'; }
|
|
919
|
+
write(entry.kind, entry.text, entry);
|
|
920
|
+
saveStatus();
|
|
921
|
+
},
|
|
922
|
+
close(result = {}) {
|
|
923
|
+
clearInterval(timer);
|
|
924
|
+
// The CLI and its active attempt can share the job status. Keep the last
|
|
925
|
+
// session snapshot when the CLI writes the terminal bookkeeping record.
|
|
926
|
+
try {
|
|
927
|
+
const latest = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
928
|
+
for (const key of ['session_id', 'capabilities', 'usage']) if (latest[key] != null) status[key] = latest[key];
|
|
929
|
+
} catch { /* no prior status */ }
|
|
930
|
+
status.state = result.cancelled ? 'cancelled' : result.ok ? 'finished' : 'failed';
|
|
931
|
+
status.in_progress = 0;
|
|
932
|
+
status.step_started_at = null;
|
|
933
|
+
status.result = { ok: result.ok === true, ...(result.cancelled ? { cancelled: true } : {}), error: result.error || null, failureKind: result.failureKind || null };
|
|
934
|
+
write(result.cancelled ? 'cancel' : result.ok ? 'finish' : 'fail', result.cancelled ? 'Cancelled' : result.ok ? 'Finished' : `Failed: ${clip(result.error || 'unknown')}`);
|
|
935
|
+
saveStatus();
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Status and the last `tail` events of a job, for `meta-harness watch`. */
|
|
941
|
+
export function readProgress(file, tail = 20) {
|
|
942
|
+
const statusFile = file.replace(/\.events\.jsonl$/, '') + '.status.json';
|
|
943
|
+
let status = null;
|
|
944
|
+
try { status = JSON.parse(fs.readFileSync(statusFile, 'utf8')); } catch { /* not started or not a job */ }
|
|
945
|
+
let events = [];
|
|
946
|
+
try { events = fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).slice(-tail).map(line => JSON.parse(line)); } catch { /* none yet */ }
|
|
947
|
+
return { status, events };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
/** Raise an operator alert without importing alerts.mjs, so remote workers can send it too. */
|
|
952
|
+
function sendStallAlert(title, message) {
|
|
953
|
+
try {
|
|
954
|
+
execFileSync(process.env.TOTAL_RECALL_BIN || 'total-recall', ['alerts', 'send', '--title', title, '--message', message, '--severity', 'warning', '--source', 'meta-harness'], { stdio: 'pipe', timeout: 15000 });
|
|
955
|
+
} catch { /* an unavailable alert channel never stops the run */ }
|
|
956
|
+
}
|
|
@@ -20,18 +20,32 @@ export const diffBetween = (cwd, base, head) => must(cwd, ['diff', `${base}...${
|
|
|
20
20
|
const ACTIVE_WORKTREES = new Map();
|
|
21
21
|
|
|
22
22
|
/** `branch` creates a new branch from `ref`. Without it, the checkout is detached at `ref` (read-only use). */
|
|
23
|
-
export function createWorktree(cwd, { branch, ref = 'HEAD', prefix = 'meta-harness-' } = {}) {
|
|
23
|
+
export function createWorktree(cwd, { branch, ref = 'HEAD', prefix = 'meta-harness-', existingBranch = false } = {}) {
|
|
24
24
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
25
|
-
const args = branch ? ['worktree', 'add', '-b', branch, directory, ref] : ['worktree', 'add', '--detach', directory, ref];
|
|
25
|
+
const args = branch ? existingBranch ? ['worktree', 'add', directory, branch] : ['worktree', 'add', '-b', branch, directory, ref] : ['worktree', 'add', '--detach', directory, ref];
|
|
26
26
|
const result = git(cwd, args, 120000);
|
|
27
27
|
if (result.status !== 0) {
|
|
28
28
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
29
29
|
throw new Error(`Creating a worktree failed: ${String(result.stderr).trim().slice(0, 300)}`);
|
|
30
30
|
}
|
|
31
|
-
ACTIVE_WORKTREES.set(directory, { cwd, branch: branch || null });
|
|
31
|
+
ACTIVE_WORKTREES.set(directory, { cwd, branch: branch || null, base: headCommit(directory) });
|
|
32
32
|
seedMandatorySkills(cwd, directory);
|
|
33
|
+
linkPrivatePlans(cwd, directory);
|
|
33
34
|
return directory;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Plans in docs/projects are private and untracked in public repositories, so a checkout lacks them
|
|
38
|
+
* and an agent would start a duplicate plan (A-116). Link the one real plan folder in; never commit it.
|
|
39
|
+
*/
|
|
40
|
+
const PLANS = 'docs/projects';
|
|
41
|
+
function linkPrivatePlans(cwd, directory) {
|
|
42
|
+
const root = git(cwd, ['rev-parse', '--show-toplevel']).stdout.trim();
|
|
43
|
+
const source = path.join(root, PLANS), target = path.join(directory, PLANS);
|
|
44
|
+
if (!root || !fs.existsSync(source) || fs.existsSync(target)) return;
|
|
45
|
+
if (git(cwd, ['ls-files', '--', PLANS]).stdout.trim()) return;
|
|
46
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
47
|
+
fs.symlinkSync(source, target);
|
|
48
|
+
}
|
|
35
49
|
/**
|
|
36
50
|
* An agent's own Total Recall preflight needs these two files at the repository root, and they are
|
|
37
51
|
* usually gitignored, so a fresh checkout lacks them and every agent inside it would report itself blocked.
|
|
@@ -57,17 +71,58 @@ export function linkDependencies(cwd, directory) {
|
|
|
57
71
|
const target = path.join(directory, '.agent/skills/code-quality');
|
|
58
72
|
if (fs.existsSync(quality) && !fs.existsSync(target)) fs.cpSync(quality, target, { recursive: true, filter: source => !source.includes(`${path.sep}reports`) });
|
|
59
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Give an agent's checkout its own dependencies before it starts (A-109). A link would let an
|
|
76
|
+
* agent's `npm install` write into the operator's tree, so the checkout gets a copy-on-write
|
|
77
|
+
* clone of `node_modules` (macOS clonefile, Linux reflink: no extra disk), or, when the operator
|
|
78
|
+
* has none, a frozen install from the lockfile. Returns what it did, for the attempt record.
|
|
79
|
+
*/
|
|
80
|
+
export function provideDependencies(cwd, directory, { run = spawnSync } = {}) {
|
|
81
|
+
if (!fs.existsSync(path.join(directory, 'package.json'))) return { action: 'none', reason: 'no package.json' };
|
|
82
|
+
const target = path.join(directory, 'node_modules');
|
|
83
|
+
if (fs.existsSync(target)) return { action: 'present' };
|
|
84
|
+
const modules = path.join(cwd, 'node_modules');
|
|
85
|
+
if (fs.existsSync(modules)) {
|
|
86
|
+
const args = process.platform === 'darwin' ? ['-c', '-R', modules, target] : ['-a', '--reflink=auto', modules, target];
|
|
87
|
+
const copied = run('cp', args, { stdio: 'ignore' });
|
|
88
|
+
if (copied.status === 0) return { action: 'cloned' };
|
|
89
|
+
}
|
|
90
|
+
const lock = [['package-lock.json', 'npm', ['ci', '--no-audit', '--no-fund']], ['pnpm-lock.yaml', 'pnpm', ['install', '--frozen-lockfile']], ['yarn.lock', 'yarn', ['install', '--frozen-lockfile']]]
|
|
91
|
+
.find(([file]) => fs.existsSync(path.join(directory, file)));
|
|
92
|
+
if (!lock) return { action: 'none', reason: 'no lockfile' };
|
|
93
|
+
const installed = run(lock[1], lock[2], { cwd: directory, stdio: 'ignore', timeout: 600000 });
|
|
94
|
+
return installed.status === 0 ? { action: 'installed', manager: lock[1] } : { action: 'failed', manager: lock[1] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Push a finished job's branch (A-113, Greg 2026-11-09: agents push finished work themselves).
|
|
99
|
+
* Only `meta-harness/...` branches are pushed, so a job can never push a deploy branch.
|
|
100
|
+
* No `origin` remote means nothing to push. Returns what happened, for the job record.
|
|
101
|
+
*/
|
|
102
|
+
export function pushBranch(cwd, branch) {
|
|
103
|
+
if (!branch || !branch.startsWith('meta-harness/')) return { pushed: false, reason: 'not a job branch' };
|
|
104
|
+
if (git(cwd, ['remote', 'get-url', 'origin']).status !== 0) return { pushed: false, reason: 'no origin remote' };
|
|
105
|
+
const result = git(cwd, ['push', '-q', '-u', 'origin', `${branch}:${branch}`], 120000);
|
|
106
|
+
return result.status === 0 ? { pushed: true, remote: 'origin', branch } : { pushed: false, reason: String(result.stderr || result.error || '').trim().slice(0, 300) };
|
|
107
|
+
}
|
|
60
108
|
export const deleteBranch = (cwd, branch) => git(cwd, ['branch', '-D', branch]).status === 0;
|
|
61
109
|
export function removeWorktree(cwd, directory) {
|
|
62
110
|
if (!directory) return;
|
|
63
111
|
ACTIVE_WORKTREES.delete(directory);
|
|
64
|
-
|
|
112
|
+
for (const link of ['node_modules', PLANS]) {
|
|
113
|
+
try { if (fs.lstatSync(path.join(directory, link)).isSymbolicLink()) fs.rmSync(path.join(directory, link)); } catch { /* no link */ }
|
|
114
|
+
}
|
|
65
115
|
git(cwd, ['worktree', 'remove', '--force', directory], 60000);
|
|
66
116
|
fs.rmSync(directory, { recursive: true, force: true });
|
|
67
117
|
}
|
|
68
118
|
/** Commit whatever the agent left. Returns the commit, or null when it changed nothing. */
|
|
69
119
|
export function commitAll(directory, message) {
|
|
70
|
-
|
|
120
|
+
// Stage everything git does not ignore. Naming node_modules in the pathspec fails when it is
|
|
121
|
+
// ignored (A-114), so it is unstaged afterwards instead, for repositories that do not ignore it.
|
|
122
|
+
must(directory, ['add', '-A'], 'Staging agent changes');
|
|
123
|
+
git(directory, ['rm', '-r', '--cached', '-q', '--ignore-unmatch', '--', 'node_modules']);
|
|
124
|
+
// The linked private plan folder (A-116) is the operator's, never part of the agent's commit.
|
|
125
|
+
try { if (fs.lstatSync(path.join(directory, PLANS)).isSymbolicLink()) git(directory, ['rm', '-r', '--cached', '-q', '--ignore-unmatch', '--', PLANS]); } catch { /* no link */ }
|
|
71
126
|
// Seeded skills are scaffolding, not the agent's work. A repository that tracks them keeps normal history.
|
|
72
127
|
for (const file of MANDATORY_SKILLS) if (git(directory, ['cat-file', '-e', `HEAD:${file}`]).status !== 0) git(directory, ['rm', '--cached', '-q', '--ignore-unmatch', '--', file]);
|
|
73
128
|
if (git(directory, ['diff', '--cached', '--quiet']).status === 0) return null;
|
|
@@ -77,10 +132,22 @@ export function commitAll(directory, message) {
|
|
|
77
132
|
must(directory, ['commit', '-m', message], 'Committing agent changes');
|
|
78
133
|
return headCommit(directory);
|
|
79
134
|
}
|
|
80
|
-
/**
|
|
81
|
-
export function removeActiveWorktrees() {
|
|
82
|
-
|
|
135
|
+
/** Remove owned checkouts. Cancellation keeps branches that contain work. */
|
|
136
|
+
export function removeActiveWorktrees({ preserveChanges = false } = {}) {
|
|
137
|
+
const saved = [];
|
|
138
|
+
for (const [directory, { cwd, branch, base }] of [...ACTIVE_WORKTREES]) {
|
|
139
|
+
let wip = null;
|
|
140
|
+
if (preserveChanges && branch) {
|
|
141
|
+
try { wip = commitAll(directory, 'meta-harness: unfinished work from a cancelled job'); }
|
|
142
|
+
catch (error) {
|
|
143
|
+
// Do not destroy edits if the WIP commit fails (for example, a credential file).
|
|
144
|
+
saved.push({ branch, directory, error: error.message });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (wip || headCommit(directory) !== base) saved.push({ branch, wip, commit: headCommit(directory) });
|
|
148
|
+
}
|
|
83
149
|
removeWorktree(cwd, directory);
|
|
84
|
-
if (branch) deleteBranch(cwd, branch);
|
|
150
|
+
if (branch && (!preserveChanges || !saved.some(item => item.branch === branch))) deleteBranch(cwd, branch);
|
|
85
151
|
}
|
|
152
|
+
return saved;
|
|
86
153
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# DESIGN.md —
|
|
1
|
+
# DESIGN.md — Metaharness
|
|
2
2
|
|
|
3
3
|
```yaml
|
|
4
4
|
tokens:
|
|
5
|
-
brand-name: "
|
|
6
|
-
wordmark: "
|
|
5
|
+
brand-name: "Metaharness"
|
|
6
|
+
wordmark: "METAHARNESS"
|
|
7
7
|
color-primary: "#3b82f6"
|
|
8
8
|
color-background: "#0c1427"
|
|
9
9
|
color-surface: "#1e293b"
|
|
@@ -20,8 +20,8 @@ tokens:
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Brand & Typographic Wordmark
|
|
23
|
-
- **Name**:
|
|
24
|
-
- **Wordmark Logo**: `
|
|
23
|
+
- **Name**: Metaharness
|
|
24
|
+
- **Wordmark Logo**: `METAHARNESS` (clean typographic badge, no invented cyber delimiters)
|
|
25
25
|
- **Palette**: Primary `#3b82f6`, Surface `#0c1427`, Secondary Surface `#1e293b`, Accent `#38bdf8`, Line `#1e293b`, Text `#f1f5f9`.
|
|
26
26
|
- **States**: Verified Pass `#10b981`, Warning `#f59e0b`, Danger `#ef4444`.
|
|
27
27
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Metaharness — Multi-Agent Coordination & Inspection Console
|
|
3
3
|
*
|
|
4
4
|
* Bespoke operator interface for terminal coding agent fleet management.
|
|
5
5
|
* Displays real-time agent discovery matrix, mesh node targets, authentication
|
|
@@ -855,9 +855,9 @@ export class CommandFleetCard extends HTMLElement {
|
|
|
855
855
|
|
|
856
856
|
<div class="header">
|
|
857
857
|
<div class="brand-group">
|
|
858
|
-
<div class="wordmark-badge">
|
|
858
|
+
<div class="wordmark-badge">METAHARNESS</div>
|
|
859
859
|
<div class="title-meta">
|
|
860
|
-
<h3 class="plugin-title">
|
|
860
|
+
<h3 class="plugin-title">Metaharness Fleet</h3>
|
|
861
861
|
<p class="plugin-subtitle">Multi-agent runtime coordination, quotas & benchmarks</p>
|
|
862
862
|
</div>
|
|
863
863
|
</div>
|