total-recall-brain 3.43.3 → 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 +16 -9
- 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 +3 -3
- package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
- package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
- package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
- package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
- 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 +17 -16
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
- 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 +160 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
- 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 +101 -16
- 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 +54 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
- 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 +64 -15
- 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 +249 -0
- 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 +6 -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/pool.mjs +16 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
- 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 +28 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
- 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 +110 -0
- 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 +597 -70
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
- 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 +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/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/connect.mjs +2 -29
- package/src/cli/context.mjs +1 -0
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/ingest-okf.mjs +4 -4
- 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/release.mjs +4 -1
- 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 +10 -3
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/automatic-brain-updates.mjs +11 -6
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +42 -12
- 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-compactor.mjs +7 -2
- package/src/core/context-compiler.mjs +26 -1
- package/src/core/context-policy.mjs +7 -3
- package/src/core/daemon-control.mjs +22 -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/global-brain-updates.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/mesh-brain-updates.mjs +2 -1
- 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-restart.mjs +128 -7
- 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 +33 -6
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +68 -57
- package/src/core/task-context.mjs +9 -8
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +5 -0
- package/src/core/vault-files.mjs +8 -2
- package/src/core/vault.mjs +1 -1
- package/src/core/vector-field.mjs +134 -192
- package/src/server/index.mjs +3 -1
- 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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Local job identity and cancellation. A terminal log result is the completion record. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { configDir } from './paths.mjs';
|
|
6
|
+
import { createProgress } from './worker.mjs';
|
|
7
|
+
|
|
8
|
+
const jobsDir = () => path.join(configDir(), 'jobs');
|
|
9
|
+
export const newJobId = () => `${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
|
|
10
|
+
|
|
11
|
+
export function startJob(operation, { id = newJobId() } = {}) {
|
|
12
|
+
if (!/^[a-z0-9-]+$/.test(id)) throw new Error('Invalid job ID.');
|
|
13
|
+
const directory = jobsDir();
|
|
14
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
15
|
+
const log = path.join(directory, `${id}.log`);
|
|
16
|
+
const pidFile = path.join(directory, `${id}.pid`);
|
|
17
|
+
const record = { job: id, pid: process.pid, operation, startedAt: new Date().toISOString() };
|
|
18
|
+
fs.appendFileSync(log, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
19
|
+
fs.writeFileSync(pidFile, `${process.pid}\n`, { mode: 0o600, flag: 'wx' });
|
|
20
|
+
const progress = createProgress({ file: path.join(directory, `${id}.events.jsonl`), harness: operation });
|
|
21
|
+
return { ...record, log, finish(result) {
|
|
22
|
+
progress.close(result);
|
|
23
|
+
fs.appendFileSync(log, `${JSON.stringify(result)}\n`, { mode: 0o600 });
|
|
24
|
+
fs.rmSync(pidFile, { force: true });
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJob(log) {
|
|
29
|
+
const lines = fs.readFileSync(log, 'utf8').trim().split('\n');
|
|
30
|
+
let record;
|
|
31
|
+
try { record = JSON.parse(lines[0]); } catch { /* old log without PID */ }
|
|
32
|
+
let result = null;
|
|
33
|
+
for (const line of lines.slice(1).reverse()) {
|
|
34
|
+
try {
|
|
35
|
+
const value = JSON.parse(line);
|
|
36
|
+
if (typeof value.ok === 'boolean') { result = value; break; }
|
|
37
|
+
} catch { /* progress text */ }
|
|
38
|
+
}
|
|
39
|
+
return { record, result };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveJob(target) {
|
|
43
|
+
if (!target) throw new Error('cancel requires a job ID or log path.');
|
|
44
|
+
const directory = jobsDir();
|
|
45
|
+
const prefix = path.basename(target).replace(/\.(?:log|pid|events\.jsonl|status\.json)$/, '');
|
|
46
|
+
const files = fs.existsSync(directory) ? fs.readdirSync(directory) : [];
|
|
47
|
+
let names = files.filter(name => name.endsWith('.log') && name.startsWith(prefix));
|
|
48
|
+
if (!names.length) {
|
|
49
|
+
// An attempt ID from watch resolves to its owning job, never directly to a PID.
|
|
50
|
+
const events = files.filter(name => name.endsWith('.events.jsonl') && name.startsWith(prefix))
|
|
51
|
+
.sort((a, b) => fs.statSync(path.join(directory, b)).mtimeMs - fs.statSync(path.join(directory, a)).mtimeMs);
|
|
52
|
+
let pid;
|
|
53
|
+
try { pid = JSON.parse(fs.readFileSync(path.join(directory, events[0].replace('.events.jsonl', '.status.json')), 'utf8')).pid; } catch { /* no matching status */ }
|
|
54
|
+
if (pid) names = files.filter(name => name.endsWith('.log') && readJob(path.join(directory, name)).record?.pid === pid);
|
|
55
|
+
}
|
|
56
|
+
const exact = names.find(name => name === `${prefix}.log`);
|
|
57
|
+
if (!names.length) throw new Error(`No job log matches ${target}.`);
|
|
58
|
+
if (!exact && names.length > 1) throw new Error(`More than one job matches ${target}. Use a longer job ID.`);
|
|
59
|
+
const log = path.join(directory, exact || names[0]);
|
|
60
|
+
return { id: path.basename(log, '.log'), log, ...readJob(log) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function cancelJob(target, { timeoutMs = 60_000, pollMs = 100 } = {}) {
|
|
64
|
+
const job = resolveJob(target);
|
|
65
|
+
if (job.result) throw new Error(`Job ${job.id} has finished${job.result.cancelled ? ' (cancelled)' : ''}.`);
|
|
66
|
+
const pidFile = path.join(jobsDir(), `${job.id}.pid`);
|
|
67
|
+
let pid;
|
|
68
|
+
try { pid = Number(fs.readFileSync(pidFile, 'utf8').trim()); } catch { /* stale or old job */ }
|
|
69
|
+
if (!Number.isSafeInteger(pid) || pid <= 1 || job.record?.pid !== pid || job.record?.job !== job.id) {
|
|
70
|
+
throw new Error(`Job ${job.id} has no active PID record.`);
|
|
71
|
+
}
|
|
72
|
+
try { process.kill(pid, 'SIGTERM'); }
|
|
73
|
+
catch (error) { throw new Error(`Cannot cancel job ${job.id}: ${error.code === 'ESRCH' ? 'the process has stopped' : error.message}.`); }
|
|
74
|
+
const deadline = Date.now() + Math.min(timeoutMs, 60_000);
|
|
75
|
+
do {
|
|
76
|
+
const { result } = readJob(job.log);
|
|
77
|
+
if (result) {
|
|
78
|
+
if (!result.cancelled) throw new Error(`Job ${job.id} finished before it could be cancelled.`);
|
|
79
|
+
return { ...result, job: job.id, log: job.log };
|
|
80
|
+
}
|
|
81
|
+
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
82
|
+
} while (Date.now() < deadline);
|
|
83
|
+
throw new Error(`Job ${job.id} did not report cancellation within ${Math.min(timeoutMs, 60_000) / 1000} seconds. Log: ${job.log}`);
|
|
84
|
+
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Execution receipts and the learning loop.
|
|
3
3
|
* Receipts are an append-only SSSS ledger. Scorecards, quota walls and proposals fold from it.
|
|
4
4
|
*/
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
5
|
import { computeEmpiricalScorecard } from '../capability/index.mjs';
|
|
7
6
|
import { receiptsFile } from './paths.mjs';
|
|
8
7
|
import { classifyFailure } from './worker.mjs';
|
|
@@ -49,6 +48,9 @@ export async function recordRunReceipt(receipt = {}) {
|
|
|
49
48
|
success: receipt.success === true,
|
|
50
49
|
durationMs: receipt.durationMs || 0,
|
|
51
50
|
failureKind: receipt.failureKind || null,
|
|
51
|
+
...(receipt.billingMode ? { billingMode: receipt.billingMode } : {}),
|
|
52
|
+
...(Array.isArray(receipt.tools) && receipt.tools.length ? { tools: receipt.tools.map(({ tool, status, exitCode }) =>
|
|
53
|
+
({ tool, status, ...(Number.isInteger(exitCode) ? { exitCode } : {}) })) } : {}),
|
|
52
54
|
error: receipt.error ? String(receipt.error).slice(0, 500) : null,
|
|
53
55
|
resetsAt: receipt.resetsAt || null,
|
|
54
56
|
gate: receipt.gate || null,
|
|
@@ -102,18 +104,10 @@ export function activeCredentialBlocks(entries = [], now = Date.now(), holdMs =
|
|
|
102
104
|
return blocks;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
/**
|
|
106
|
-
export async function learnFromHistory(
|
|
107
|
+
/** Read the same receipts the router uses. Proposals stay in this report. */
|
|
108
|
+
export async function learnFromHistory() {
|
|
107
109
|
const entries = await readReceipts();
|
|
108
110
|
const scorecard = computeEmpiricalScorecard(entries);
|
|
109
111
|
const proposals = proposalsFrom(scorecard, entries);
|
|
110
|
-
|
|
111
|
-
if (emit) for (const proposal of proposals) {
|
|
112
|
-
const result = runner(process.env.TOTAL_RECALL_BIN || 'total-recall', ['remember', 'fact', `${proposal.reason} ${proposal.recommendation}`, '--project', '--slug', proposal.id, '--tags', `meta-harness,${proposal.id}`], { encoding: 'utf8', timeout: 30000 });
|
|
113
|
-
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
114
|
-
// The slug is the proposal's identity, so an open proposal is reported once, however often learn runs.
|
|
115
|
-
const existing = result.status !== 0 && /already exists/i.test(output);
|
|
116
|
-
emitted.push({ id: proposal.id, ok: result.status === 0 || existing, ...(existing ? { existing: true } : {}), error: result.status === 0 || existing ? undefined : String(result.stderr || result.error || 'remember failed').slice(0, 300) });
|
|
117
|
-
}
|
|
118
|
-
return { ok: emitted.every(row => row.ok), totalHistoryRuns: entries.length, scorecard, walls: activeQuotaWalls(entries), credentialBlocks: activeCredentialBlocks(entries), proposals, ...(emit ? { emitted } : {}) };
|
|
112
|
+
return { ok: true, totalHistoryRuns: entries.length, scorecard, walls: activeQuotaWalls(entries), credentialBlocks: activeCredentialBlocks(entries), proposals };
|
|
119
113
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Run each maintenance owner even when another owner fails. The daemon records the result. */
|
|
2
|
+
import { setupIntegrations } from './setup.mjs';
|
|
3
|
+
import { maintainDocumentation } from './documentation.mjs';
|
|
4
|
+
|
|
5
|
+
export async function maintainIntegrations(options = {}, owners = {}) {
|
|
6
|
+
const steps = {
|
|
7
|
+
setup: owners.setup || (() => setupIntegrations()),
|
|
8
|
+
gates: owners.gates || (async () => (await import('./gate-adapters/index.mjs')).installAll()),
|
|
9
|
+
docs: owners.docs || (() => maintainDocumentation(options)),
|
|
10
|
+
};
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const [name, execute] of Object.entries(steps)) {
|
|
13
|
+
try { result[name] = await execute(); }
|
|
14
|
+
catch (error) { result[name] = { ok: false, error: error.message }; }
|
|
15
|
+
}
|
|
16
|
+
return { ...result, ok: Object.values(result).every(step => step?.ok === true) };
|
|
17
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP stdio (JSONL), with the initialize lifecycle used by Claude and Codex.
|
|
3
|
+
* Protocol: https://modelcontextprotocol.io/specification/2025-11-25
|
|
4
|
+
* Approval result: https://code.claude.com/docs/en/agent-sdk/user-input
|
|
5
|
+
*/
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
import { brainDir } from './paths.mjs';
|
|
12
|
+
import { appendEvent, jobPaths, tailInbox } from './control.mjs';
|
|
13
|
+
|
|
14
|
+
const execute = promisify(execFile);
|
|
15
|
+
export const QUESTION_TIMEOUT_MS = 10 * 60_000;
|
|
16
|
+
export const NO_ANSWER = 'No answer; proceed on your best judgment';
|
|
17
|
+
const versions = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
|
|
18
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
const textResult = text => ({ content: [{ type: 'text', text }] });
|
|
20
|
+
const fault = (code, message) => Object.assign(new Error(message), { code });
|
|
21
|
+
|
|
22
|
+
export const MCP_TOOLS = [
|
|
23
|
+
{ name: 'ask', description: 'Ask the operator questions and wait for a reply.', inputSchema: {
|
|
24
|
+
type: 'object', properties: { questions: { type: 'array', minItems: 1, items: { oneOf: [{ type: 'string', minLength: 1 }, { type: 'object' }] } } }, required: ['questions'], additionalProperties: false,
|
|
25
|
+
} },
|
|
26
|
+
{ name: 'approve', description: 'Answer a Claude Code permission prompt under the job approval policy.', inputSchema: {
|
|
27
|
+
type: 'object', properties: { tool_name: { type: 'string', minLength: 1 }, input: { type: 'object' }, tool_use_id: { type: 'string' } }, required: ['tool_name', 'input'],
|
|
28
|
+
} },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** Return Claude's --mcp-config object and Codex's -c arguments. No shell parses these values. */
|
|
32
|
+
export function mcpConfigFor(job, options = {}) {
|
|
33
|
+
const id = jobPaths(job).id;
|
|
34
|
+
const command = process.execPath;
|
|
35
|
+
const args = [fileURLToPath(import.meta.url), '--approvals', options.approvalPolicy || 'auto',
|
|
36
|
+
'--question-timeout-ms', String(options.questionTimeoutMs ?? options.timeoutMs ?? QUESTION_TIMEOUT_MS)];
|
|
37
|
+
const env = { TR_METAHARNESS_JOB: id, TR_SECRETS_BRAIN: brainDir() };
|
|
38
|
+
if (process.env.TOTAL_RECALL_BIN) env.TOTAL_RECALL_BIN = process.env.TOTAL_RECALL_BIN;
|
|
39
|
+
const claude = { mcpServers: { metaharness: { command, args, env } } };
|
|
40
|
+
const tomlEnv = `{ ${Object.entries(env).map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join(', ')} }`;
|
|
41
|
+
const codex = ['-c', `mcp_servers.metaharness.command=${JSON.stringify(command)}`,
|
|
42
|
+
'-c', `mcp_servers.metaharness.args=${JSON.stringify(args)}`, '-c', `mcp_servers.metaharness.env=${tomlEnv}`,
|
|
43
|
+
'-c', `mcp_servers.metaharness.tool_timeout_sec=${Math.ceil(Number(args.at(-1)) / 1000) + 60}`];
|
|
44
|
+
return { claude, codex, claudeArgs: ['--mcp-config', JSON.stringify(claude), '--permission-prompt-tool', 'mcp__metaharness__approve'], codexArgs: codex };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The decision owner returns a typed choice. Missing or failed decisions deny the call. */
|
|
48
|
+
async function decideApproval(request, { signal } = {}) {
|
|
49
|
+
const questions = { approval: { type: 'choice', instructions: 'Apply Total Recall rules to this tool request. Allow only if the request is authorized. Deny if authorization is unclear.',
|
|
50
|
+
criteria: { allow: 'Authorized by the operator and the applicable rules.', deny: 'Not authorized or prohibited by a rule.' } } };
|
|
51
|
+
const { stdout } = await execute(process.env.TOTAL_RECALL_BIN || 'total-recall',
|
|
52
|
+
['decision', 'run', '--state', JSON.stringify({ request }), '--questions', JSON.stringify(questions), '--json'],
|
|
53
|
+
{ encoding: 'utf8', timeout: 60_000, maxBuffer: 1024 * 1024, signal });
|
|
54
|
+
const record = JSON.parse(stdout);
|
|
55
|
+
if (record.ok === false) throw new Error(record.error || 'The decision owner failed.');
|
|
56
|
+
const answer = record.answers?.approval ?? record.decision?.answers?.approval ?? record.result?.approval;
|
|
57
|
+
const value = typeof answer === 'string' ? answer : answer?.choice;
|
|
58
|
+
if (!['allow', 'deny'].includes(value)) throw new Error('The decision owner returned no allow or deny answer.');
|
|
59
|
+
return { value, record };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolve all pending calls on close; no poll timer or child command outlives the server. */
|
|
63
|
+
export function createMcpTools({ job = process.env.TR_METAHARNESS_JOB, approvalPolicy = 'auto',
|
|
64
|
+
questionTimeoutMs = QUESTION_TIMEOUT_MS, timeoutMs, decisionFn = decideApproval, alertFn,
|
|
65
|
+
deferQuestions = process.env.TR_METAHARNESS_DEFER_QUESTIONS !== '0' } = {}) {
|
|
66
|
+
jobPaths(job);
|
|
67
|
+
if (!['auto', 'operator', 'rules'].includes(approvalPolicy)) throw new Error('Approval policy must be auto, operator, or rules.');
|
|
68
|
+
questionTimeoutMs = timeoutMs ?? questionTimeoutMs;
|
|
69
|
+
if (!Number.isSafeInteger(questionTimeoutMs) || questionTimeoutMs < 1) throw new Error('Question timeout must be a positive number of milliseconds.');
|
|
70
|
+
const pending = new Map();
|
|
71
|
+
const lifetime = new AbortController();
|
|
72
|
+
const children = new Set();
|
|
73
|
+
const notify = event => {
|
|
74
|
+
const task = alertFn ? Promise.resolve().then(() => alertFn(event)) : execute(process.env.TOTAL_RECALL_BIN || 'total-recall',
|
|
75
|
+
['alerts', 'send', '--title', `Metaharness: ${event.kind}`, '--message', `Job ${job}: ${event.text}`, '--severity', 'info', '--source', 'meta-harness'],
|
|
76
|
+
{ timeout: 15_000, maxBuffer: 65536, signal: lifetime.signal });
|
|
77
|
+
children.add(task);
|
|
78
|
+
task.catch(() => {}).finally(() => children.delete(task));
|
|
79
|
+
};
|
|
80
|
+
const watcher = tailInbox(job, command => {
|
|
81
|
+
const wait = pending.get(command.requestId);
|
|
82
|
+
if (!wait || command.type !== (wait.kind === 'question' ? 'reply' : 'approve')) return false;
|
|
83
|
+
wait.finish({ value: wait.kind === 'question' ? command.text : command.value, commandId: command.id });
|
|
84
|
+
return { via: 'metaharness' };
|
|
85
|
+
}, { recipient: 'mcp', onError: error => {
|
|
86
|
+
for (const wait of pending.values()) wait.fail(error);
|
|
87
|
+
} });
|
|
88
|
+
|
|
89
|
+
const waitFor = (event, signal) => new Promise((resolve, reject) => {
|
|
90
|
+
let timer;
|
|
91
|
+
const cleanup = () => { clearTimeout(timer); pending.delete(event.requestId); signal?.removeEventListener('abort', abort); };
|
|
92
|
+
const finish = value => { cleanup(); resolve(value); };
|
|
93
|
+
const fail = error => { cleanup(); reject(error); };
|
|
94
|
+
const abort = () => fail(new Error('The request was cancelled.'));
|
|
95
|
+
pending.set(event.requestId, { kind: event.kind, finish, fail });
|
|
96
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
97
|
+
if (signal?.aborted || lifetime.signal.aborted) { abort(); return; }
|
|
98
|
+
timer = setTimeout(() => finish({ value: event.kind === 'question' ? NO_ANSWER : 'deny', timeout: true }), questionTimeoutMs);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
async call(name, args, { signal, defer = deferQuestions } = {}) {
|
|
103
|
+
if (lifetime.signal.aborted) throw new Error('The MCP server has closed.');
|
|
104
|
+
if (name === 'ask') {
|
|
105
|
+
if (!object(args) || !Array.isArray(args.questions) || !args.questions.length ||
|
|
106
|
+
args.questions.some(q => typeof q === 'string' ? !q.trim() : !object(q))) throw fault(-32602, 'ask requires a non-empty questions array.');
|
|
107
|
+
} else if (name === 'approve') {
|
|
108
|
+
if (!object(args) || typeof args.tool_name !== 'string' || !args.tool_name.trim() || !object(args.input) ||
|
|
109
|
+
(args.tool_use_id !== undefined && typeof args.tool_use_id !== 'string')) throw fault(-32602, 'approve requires tool_name and input.');
|
|
110
|
+
} else throw fault(-32602, `Unknown tool: ${name}`);
|
|
111
|
+
const requestId = randomUUID();
|
|
112
|
+
if (name === 'ask' && defer) {
|
|
113
|
+
// A question costs nothing while it waits (A-125): record it, tell the agent to end its turn,
|
|
114
|
+
// and let the session runner start the next turn with the operator's answer. Holding the tool
|
|
115
|
+
// call open made Codex call the model every few seconds while it waited.
|
|
116
|
+
const event = { kind: 'question', source: 'deferred', requestId, text: JSON.stringify(args.questions), questions: args.questions };
|
|
117
|
+
appendEvent(job, event);
|
|
118
|
+
notify(event);
|
|
119
|
+
return textResult(`Your question went to the operator (request ${requestId}). End your turn now and do no other work. The operator's answer arrives as the next message in this session.`);
|
|
120
|
+
}
|
|
121
|
+
const event = { kind: name === 'ask' ? 'question' : 'approval', source: 'mcp', requestId,
|
|
122
|
+
text: name === 'ask' ? JSON.stringify(args.questions) : `${args.tool_name}: ${JSON.stringify(args.input)}`,
|
|
123
|
+
...(name === 'ask' ? { questions: args.questions } : { request: args, policy: approvalPolicy }) };
|
|
124
|
+
appendEvent(job, event);
|
|
125
|
+
let answer;
|
|
126
|
+
try {
|
|
127
|
+
if (name === 'ask' || approvalPolicy === 'operator') {
|
|
128
|
+
// Register the waiter before notifying the operator.
|
|
129
|
+
const waiting = waitFor(event, signal);
|
|
130
|
+
notify(event);
|
|
131
|
+
answer = await waiting;
|
|
132
|
+
} else if (approvalPolicy === 'auto') answer = { value: 'allow', policy: 'auto' };
|
|
133
|
+
else {
|
|
134
|
+
try { answer = { ...await decisionFn(args, { signal: signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal }), policy: 'rules' }; }
|
|
135
|
+
catch (error) { answer = { value: 'deny', policy: 'rules', error: error.message }; }
|
|
136
|
+
if (!['allow', 'deny'].includes(answer.value)) answer = { value: 'deny', policy: 'rules', error: 'Invalid decision answer.' };
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
appendEvent(job, { kind: 'ack', command: name, requestId, ok: true, cancelled: true, text: error.message });
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
// tailInbox records delivery; this event records the actual answer and policy decision.
|
|
143
|
+
appendEvent(job, { kind: 'ack', command: name, requestId, ok: true, ...answer, text: String(answer.value) });
|
|
144
|
+
if (name === 'ask') return textResult(String(answer.value));
|
|
145
|
+
const result = answer.value === 'allow' ? { behavior: 'allow', updatedInput: args.input }
|
|
146
|
+
: { behavior: 'deny', message: answer.timeout ? 'Approval timed out.' : answer.error || 'The request was denied.' };
|
|
147
|
+
return textResult(JSON.stringify(result));
|
|
148
|
+
},
|
|
149
|
+
async close() {
|
|
150
|
+
lifetime.abort();
|
|
151
|
+
for (const wait of pending.values()) wait.fail(new Error('The MCP server has closed.'));
|
|
152
|
+
await watcher.close();
|
|
153
|
+
await Promise.allSettled(children);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Never log on stdout here: every line must be a JSON-RPC message. */
|
|
159
|
+
export async function runMcpServer({ input = process.stdin, output = process.stdout, ...options } = {}) {
|
|
160
|
+
const tools = createMcpTools(options);
|
|
161
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
162
|
+
const calls = new Map();
|
|
163
|
+
let initialized = false, ready = false, closed = false;
|
|
164
|
+
const write = message => { if (!closed) output.write(JSON.stringify(message) + '\n'); };
|
|
165
|
+
const respond = (id, result) => write({ jsonrpc: '2.0', id, result });
|
|
166
|
+
const error = (id, code, message) => write({ jsonrpc: '2.0', id, error: { code, message } });
|
|
167
|
+
const handle = async message => {
|
|
168
|
+
if (!object(message) || message.jsonrpc !== '2.0' || typeof message.method !== 'string' ||
|
|
169
|
+
(Object.hasOwn(message, 'id') && typeof message.id !== 'string' && typeof message.id !== 'number') ||
|
|
170
|
+
(message.params !== undefined && !object(message.params))) {
|
|
171
|
+
error(null, -32600, 'Invalid Request'); return;
|
|
172
|
+
}
|
|
173
|
+
const { id, method, params = {} } = message;
|
|
174
|
+
if (id === undefined) {
|
|
175
|
+
if (method === 'notifications/initialized' && initialized) ready = true;
|
|
176
|
+
if (method === 'notifications/cancelled') calls.get(params.requestId)?.controller.abort();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (calls.has(id)) { error(id, -32600, 'Duplicate request ID'); return; }
|
|
180
|
+
if (method === 'initialize') {
|
|
181
|
+
if (initialized) { error(id, -32600, 'Already initialized'); return; }
|
|
182
|
+
if (typeof params.protocolVersion !== 'string' || !object(params.capabilities) || !object(params.clientInfo)) {
|
|
183
|
+
error(id, -32602, 'initialize requires protocolVersion, capabilities, and clientInfo'); return;
|
|
184
|
+
}
|
|
185
|
+
initialized = true;
|
|
186
|
+
respond(id, { protocolVersion: versions.includes(params.protocolVersion) ? params.protocolVersion : versions[0],
|
|
187
|
+
capabilities: { tools: {} }, serverInfo: { name: 'metaharness', version: '1.0.0' } });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (method === 'ping') { respond(id, {}); return; }
|
|
191
|
+
if (!ready) { error(id, -32002, 'Initialize the MCP session first'); return; }
|
|
192
|
+
if (method === 'tools/list') { respond(id, { tools: MCP_TOOLS }); return; }
|
|
193
|
+
if (method !== 'tools/call') { error(id, -32601, 'Method not found'); return; }
|
|
194
|
+
if (typeof params.name !== 'string') { error(id, -32602, 'tools/call requires a tool name'); return; }
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
const call = { controller };
|
|
197
|
+
calls.set(id, call);
|
|
198
|
+
call.done = (async () => {
|
|
199
|
+
try { respond(id, await tools.call(params.name, params.arguments, { signal: controller.signal })); }
|
|
200
|
+
catch (failure) {
|
|
201
|
+
if (typeof failure.code === 'number') error(id, failure.code, failure.message);
|
|
202
|
+
else respond(id, { ...textResult(failure.message), isError: true });
|
|
203
|
+
} finally { calls.delete(id); }
|
|
204
|
+
})();
|
|
205
|
+
};
|
|
206
|
+
lines.on('line', line => {
|
|
207
|
+
let message;
|
|
208
|
+
try { message = JSON.parse(line); } catch { error(null, -32700, 'Parse error'); return; }
|
|
209
|
+
void handle(message).catch(failure => error(message?.id ?? null, -32603, failure.message));
|
|
210
|
+
});
|
|
211
|
+
try { await new Promise((resolve, reject) => { lines.once('close', resolve); input.once('error', reject); output.once('error', reject); }); }
|
|
212
|
+
finally {
|
|
213
|
+
closed = true;
|
|
214
|
+
lines.close();
|
|
215
|
+
for (const call of calls.values()) call.controller.abort();
|
|
216
|
+
await tools.close();
|
|
217
|
+
await Promise.allSettled([...calls.values()].map(call => call.done));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export const startMcpServer = runMcpServer;
|
|
222
|
+
|
|
223
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
224
|
+
const value = flag => { const i = process.argv.indexOf(flag); return i < 0 ? undefined : process.argv[i + 1]; };
|
|
225
|
+
await runMcpServer({ approvalPolicy: value('--approvals') || 'auto',
|
|
226
|
+
questionTimeoutMs: value('--question-timeout-ms') === undefined ? QUESTION_TIMEOUT_MS : Number(value('--question-timeout-ms')) })
|
|
227
|
+
.catch(error => { console.error(`Metaharness MCP: ${error.message}`); process.exitCode = 1; });
|
|
228
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ADAPTER_CATALOG } from '../capability/index.mjs';
|
|
|
3
3
|
import { inspectAgents, getActiveAdapters } from './worker.mjs';
|
|
4
4
|
import { readReceipts, activeQuotaWalls, activeCredentialBlocks } from './learner.mjs';
|
|
5
5
|
import { measuredWalls } from './telemetry.mjs';
|
|
6
|
+
import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy } from './included-usage.mjs';
|
|
6
7
|
import os from 'node:os';
|
|
7
8
|
|
|
8
9
|
const describe = (id, extra = {}) => {
|
|
@@ -23,16 +24,23 @@ export function exclusionReason(agent, wall, credentialBlock, { allowUnknownAuth
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/** An explicit list is the caller's decision and is not filtered. */
|
|
26
|
-
export async function resolvePool({ availableHarnesses, inspect = inspectAgents, receipts = readReceipts, allowUnknownAuth = false } = {}) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
27
|
+
export async function resolvePool({ availableHarnesses, inspect = inspectAgents, receipts = readReceipts, allowUnknownAuth = false, policy } = {}) {
|
|
28
|
+
policy ??= await readIncludedPolicy();
|
|
29
|
+
if (!policy.enabled && Array.isArray(availableHarnesses) && availableHarnesses.length) return { ready: availableHarnesses.map(id => describe(id)), excluded: [], walls: {} };
|
|
30
|
+
const claudePlan = policy.enabled ? policy.claudePlan?.organization : undefined;
|
|
31
|
+
const [{ agents = [] }, entries] = await Promise.all([inspect({ env: process.env, claudePlan }), receipts()]);
|
|
32
|
+
const billingEntries = policy.enabled ? entries.filter(run => run.billingMode === 'included') : entries;
|
|
33
|
+
const walls = { ...activeQuotaWalls(billingEntries), ...(!policy.enabled ? measuredWalls() : {}) };
|
|
34
|
+
const blocks = activeCredentialBlocks(billingEntries);
|
|
31
35
|
const ready = [], excluded = [];
|
|
32
|
-
for (const
|
|
33
|
-
const
|
|
36
|
+
for (const installed of agents) {
|
|
37
|
+
const agent = policy.enabled && installed.id === 'claude' && !claudePlan ? { ...installed, authenticated: claudeProofCurrent(policy) } : installed;
|
|
38
|
+
const policyBlocked = policy.enabled && (!INCLUDED_AGENTS.includes(agent.id) || (Array.isArray(availableHarnesses) && !availableHarnesses.includes(agent.id)));
|
|
39
|
+
const reason = policyBlocked ? 'outside the included usage route' : exclusionReason(agent, walls[agent.id], blocks[agent.id], { allowUnknownAuth: !policy.enabled && allowUnknownAuth });
|
|
34
40
|
if (!reason) ready.push(describe(agent.id, { authVerified: agent.authenticated === true }));
|
|
35
|
-
else if (agent.installed) excluded.push({ harness: agent.id, reason
|
|
41
|
+
else if (agent.installed) excluded.push({ harness: agent.id, reason,
|
|
42
|
+
...(policy.enabled && !policyBlocked && agent.authenticated === true && agent.versionVerified && !agent.blockedBy && walls[agent.id]
|
|
43
|
+
? { failureKind: 'quota', resetsAt: walls[agent.id].resetReported ? walls[agent.id].until : null } : {}) });
|
|
36
44
|
}
|
|
37
45
|
return { ready, excluded, walls };
|
|
38
46
|
}
|
|
@@ -161,7 +161,7 @@ export async function executeProcessTask(job, dependencies = {}) {
|
|
|
161
161
|
tools: job.options.tools, settingSources: job.options.settingSources, model: job.options.model,
|
|
162
162
|
allowUnknownAuth: job.options.allowUnknownAuth === true, timeoutMs: job.options.timeoutMs,
|
|
163
163
|
}, { role: 'Process', node: 'local' });
|
|
164
|
-
if (result.ok && worktree) commit = commitAll(worktree, '
|
|
164
|
+
if (result.ok && worktree) commit = commitAll(worktree, 'Metaharness managed agent work');
|
|
165
165
|
} catch (error) {
|
|
166
166
|
result = { ok: false, error: error.message };
|
|
167
167
|
}
|
|
@@ -169,8 +169,18 @@ export async function executeProcessTask(job, dependencies = {}) {
|
|
|
169
169
|
if (worktree) { (dependencies.removeWorktree || removeWorktree)(job.cwd, worktree); worktree = null; }
|
|
170
170
|
if (branch && !commit) { (dependencies.deleteBranch || deleteBranch)(job.cwd, branch); branch = null; }
|
|
171
171
|
} catch (error) { result = { ...result, ok: false, error: `${result.error ? result.error + '; ' : ''}Isolation cleanup failed: ${error.message}` }; }
|
|
172
|
-
|
|
172
|
+
const record = updateAgentRecord(job.id, { status: result.ok ? 'completed' : 'failed', exitCode: result.exitCode ?? (result.ok ? 0 : 1), error: result.ok ? null : result.error,
|
|
173
|
+
...(result.queueId ? { queueId: result.queueId } : {}),
|
|
173
174
|
result: result.response || null, branch, commit, worktreePath: worktree, endedAt: new Date().toISOString() });
|
|
175
|
+
return {
|
|
176
|
+
...record,
|
|
177
|
+
...(Array.isArray(result.toolResults) ? {
|
|
178
|
+
toolResults: result.toolResults,
|
|
179
|
+
toolExecutionVerified: result.toolExecutionVerified,
|
|
180
|
+
commandExecutionVerified: result.commandExecutionVerified,
|
|
181
|
+
} : {}),
|
|
182
|
+
...(result.toolEvidenceError ? { toolEvidenceError: result.toolEvidenceError } : {}),
|
|
183
|
+
};
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
export async function killAgent(idOrPid, dependencies = {}) {
|
|
@@ -210,6 +220,20 @@ export async function getAgentLogs(idOrPid, tailLines = 50, dependencies = {}) {
|
|
|
210
220
|
if (record.remote) {
|
|
211
221
|
const result = await remoteCall(record.node, record.cwd, ['logs', record.id, '--tail', String(tailLines)], dependencies);
|
|
212
222
|
if (typeof result.logs !== 'string') throw new Error('Remote owner returned no logs');
|
|
223
|
+
if (['starting', 'running'].includes(record.status)) {
|
|
224
|
+
for (const line of result.logs.trimEnd().split('\n').reverse()) {
|
|
225
|
+
let terminal;
|
|
226
|
+
try { terminal = JSON.parse(line); } catch { continue; }
|
|
227
|
+
if (terminal?.id !== record.id || !['completed', 'failed', 'killed'].includes(terminal.status) ||
|
|
228
|
+
typeof terminal.endedAt !== 'string' || !Number.isFinite(Date.parse(terminal.endedAt))) continue;
|
|
229
|
+
updateAgentRecord(record.id, {
|
|
230
|
+
status: terminal.status,
|
|
231
|
+
exitCode: Number.isInteger(terminal.exitCode) ? terminal.exitCode : null,
|
|
232
|
+
endedAt: terminal.endedAt,
|
|
233
|
+
});
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
213
237
|
return result.logs;
|
|
214
238
|
}
|
|
215
239
|
if (path.dirname(record.logFile || '') !== logDirectory()) throw new Error('Invalid managed log path');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Headless reasoning belongs to
|
|
1
|
+
/** Headless reasoning belongs to Metaharness, including config, readiness and receipts. */
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { brainDir } from './paths.mjs';
|
|
@@ -81,6 +81,11 @@ export async function callLocalRuntime(prompt, system, config, dependencies = {}
|
|
|
81
81
|
model: agent.model || undefined, write: false, role: 'Reasoning', allowUnknownAuth: config.allowUnknownAuth === true,
|
|
82
82
|
});
|
|
83
83
|
if (result.ok === true || result.success === true) return result.response || '';
|
|
84
|
+
if (result.queued || result.queueError || result.failureKind === 'billing-policy') {
|
|
85
|
+
const error = new Error(result.queued ? `Included usage exhausted. Queued as ${result.queueId}.` : result.queueError || result.error);
|
|
86
|
+
error.queueId = result.queueId;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
84
89
|
failures.push(`${agent.name}: ${result.error || 'dispatch failed'}`);
|
|
85
90
|
}
|
|
86
91
|
throw new Error(`All CLI agents failed (${failures.join('; ')})`);
|
|
@@ -9,6 +9,7 @@ const BOOLEAN_FLAGS = {
|
|
|
9
9
|
};
|
|
10
10
|
const VALUE_FLAGS = {
|
|
11
11
|
mode: '--mode', orchestrator: '--orchestrator', explorer: '--explorer', architect: '--architect', implementer: '--implementer',
|
|
12
|
+
goal: '--goal', maxTurns: '--max-turns', maxBudgetUsd: '--max-budget-usd', maxTokens: '--max-tokens', approvalPolicy: '--approvals', questionTimeoutMs: '--question-timeout-ms', resultSchema: '--result-schema',
|
|
12
13
|
verifier: '--verifier', reviewer: '--reviewer', author: '--author', threadId: '--thread', branch: '--branch',
|
|
13
14
|
};
|
|
14
15
|
const OPERATION_KEYS = {
|
|
@@ -33,9 +34,12 @@ export async function forwardRemoteOperation(operation, task, options = {}) {
|
|
|
33
34
|
if (typeof task !== 'string' || !task.trim()) throw new Error('Remote operation requires a task description');
|
|
34
35
|
argv.push(task);
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
const controls = ['route', 'coordinate'].includes(operation) ? ['goal', 'maxTurns', 'maxBudgetUsd', 'maxTokens', 'approvalPolicy', 'questionTimeoutMs', 'resultSchema'] : [];
|
|
38
|
+
for (const key of [...OPERATION_KEYS[operation], ...controls]) {
|
|
37
39
|
if (BOOLEAN_FLAGS[key]) { if (options[key] === true) argv.push(BOOLEAN_FLAGS[key]); }
|
|
38
40
|
else if (options[key] !== undefined && options[key] !== null) {
|
|
41
|
+
if (['maxTurns', 'maxBudgetUsd', 'maxTokens', 'questionTimeoutMs'].includes(key)) { argv.push(VALUE_FLAGS[key], String(options[key])); continue; }
|
|
42
|
+
if (key === 'resultSchema' && typeof options[key] !== 'string') { argv.push('--result-schema-json', JSON.stringify(options[key])); continue; }
|
|
39
43
|
if (typeof options[key] !== 'string' || !options[key] || /\0/.test(options[key])) throw new Error(`Invalid remote ${key}`);
|
|
40
44
|
argv.push(VALUE_FLAGS[key], options[key]);
|
|
41
45
|
}
|
|
@@ -60,7 +64,7 @@ export async function forwardRemoteOperation(operation, task, options = {}) {
|
|
|
60
64
|
error: transport.error || 'Remote operation returned no valid structured result' };
|
|
61
65
|
}
|
|
62
66
|
const result = { ...payload, ok: transport.ok === true && payload.ok === true, execution, transportExitCode: transport.exitCode ?? null,
|
|
63
|
-
...(!transport.ok ? { failureKind: 'transport' } : {}) };
|
|
67
|
+
...(!transport.ok && !payload.failureKind ? { failureKind: 'transport' } : {}) };
|
|
64
68
|
if (!result.ok && !result.error) result.error = transport.error || `Remote ${operation} failed`;
|
|
65
69
|
if (result.ok && payload.branch) result.deliverable = { node: options.node, workspace: options.cwd, branch: payload.branch, commit: payload.commit ?? null };
|
|
66
70
|
const threadId = payload.threadId || options.threadId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Metaharness Universal Cross-Model Adversarial Code Review Engine
|
|
3
3
|
*
|
|
4
4
|
* Runs automated code reviews across integrated agent platforms (claude, codex, agy,
|
|
5
5
|
* grok, dsh, hermes, etc.).
|
|
@@ -19,6 +19,7 @@ import { resolvePool, localExecutionNode } from './pool.mjs';
|
|
|
19
19
|
import { lastJsonWith } from './json-reply.mjs';
|
|
20
20
|
import { createWorktree, removeWorktree, commitAll, headCommit } from './worktree.mjs';
|
|
21
21
|
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
22
|
+
import { recordReview } from './reviews.mjs';
|
|
22
23
|
|
|
23
24
|
const MAX_DIFF = 30000;
|
|
24
25
|
// Generated surfaces say nothing a reviewer can act on, and they crowd out the source.
|
|
@@ -129,11 +130,13 @@ export function buildReviewPrompt(diff, context = {}) {
|
|
|
129
130
|
You are an independent, highly critical senior security and architecture engineer performing an adversarial code review.
|
|
130
131
|
You did NOT write this code. Do not rubber-stamp. Look for vulnerabilities, silent failures, regressions, anti-patterns, and test coverage gaps.
|
|
131
132
|
|
|
132
|
-
${task}${branch}${context.stat ? `Files changed:\n${context.stat}\n\n` : ''}${context.untracked?.length ? `New untracked files, not in the diff; read them too:\n${context.untracked.slice(0, 100).join('\n')}\n\n` : ''}${diff.length > MAX_DIFF ? `This diff is cut at ${MAX_DIFF} of ${diff.length} characters. Read the remainder from the repository and say so if you could not.\n` : ''}GIT DIFF TO REVIEW:
|
|
133
|
+
${task}${branch}${context.evidence ? `Job evidence (recorded by Metaharness, not by the author):\n${JSON.stringify(context.evidence, null, 2).slice(0, 8000)}\n\n` : ''}${context.stat ? `Files changed:\n${context.stat}\n\n` : ''}${context.untracked?.length ? `New untracked files, not in the diff; read them too:\n${context.untracked.slice(0, 100).join('\n')}\n\n` : ''}${diff.length > MAX_DIFF ? `This diff is cut at ${MAX_DIFF} of ${diff.length} characters. Read the remainder from the repository and say so if you could not.\n` : ''}GIT DIFF TO REVIEW:
|
|
133
134
|
\`\`\`diff
|
|
134
135
|
${diff.slice(0, MAX_DIFF)}
|
|
135
136
|
\`\`\`
|
|
136
137
|
|
|
138
|
+
Judge the diff. Use the job evidence for claims about how the job ran (questions asked, replies, the goal check, the gate). A claim you cannot verify from the diff or the evidence is a "suggestion" marked unverified in its issue text; it is never "critical" or blocking. Mark a finding "critical" only for a defect in the diff itself.
|
|
139
|
+
|
|
137
140
|
Evaluate the diff against these strict criteria:
|
|
138
141
|
1. Security & Blast Radius (privilege escalation, token leakage, unhandled exceptions)
|
|
139
142
|
2. Invariant & Rule Adherence (does it break existing contracts or introduce silent side effects?)
|
|
@@ -166,14 +169,39 @@ export function parseReviewVerdict(response) {
|
|
|
166
169
|
return { verdict: 'NEEDS_DISCUSSION', parsed: false, riskScore: null, summary: text.slice(0, 300) || 'The reviewer returned nothing readable.', findings: [] };
|
|
167
170
|
}
|
|
168
171
|
|
|
172
|
+
export function reviewBlocksPush(result) {
|
|
173
|
+
if (result?.ok !== true) return true;
|
|
174
|
+
if (result.verdict === 'NO_DIFF') return false;
|
|
175
|
+
const review = result.review;
|
|
176
|
+
// R45.8: only blocking findings stop the push; other findings go to the job result.
|
|
177
|
+
// An unreadable review cannot clear a push, so it blocks.
|
|
178
|
+
return !review || review.parsed === false ||
|
|
179
|
+
review.findings?.some(f => f.blocking === true || ['blocking', 'critical'].includes(f.severity)) === true;
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
export async function executeCodeReview(options = {}) {
|
|
170
183
|
const remote = await forwardRemoteOperation('review', null, options);
|
|
171
184
|
if (remote) return remote;
|
|
172
185
|
localExecutionNode(options);
|
|
186
|
+
const selection = { workspace: options.cwd || process.cwd(), target: options.target || (options.staged ? '--staged' : 'working tree'), reviewer: options.reviewer || null };
|
|
187
|
+
let result;
|
|
188
|
+
try { result = await executeLocalReview(options, selection); }
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Preserve the existing CLI error contract, but retain failed attempts too.
|
|
191
|
+
try { recordReview({ ok: false, error: error.message }, selection); }
|
|
192
|
+
catch (storeError) { throw new Error(`${error.message}; review store failed: ${storeError.message}`); }
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
try { return { ...result, reviewId: recordReview(result, selection).id }; }
|
|
196
|
+
catch (error) { return { ...result, ok: false, reviewStoreError: error.message }; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function executeLocalReview(options, selection) {
|
|
173
200
|
const cwd = options.cwd || process.cwd();
|
|
174
201
|
const author = options.author || null;
|
|
175
202
|
const diffResult = extractGitDiff({ cwd, target: options.target, staged: options.staged });
|
|
176
203
|
if (!diffResult.ok) throw new Error(`Failed to read the diff: ${diffResult.error}`);
|
|
204
|
+
Object.assign(selection, { diff: diffResult.diff, untracked: diffResult.untracked, target: diffResult.target, commit: headCommit(cwd) });
|
|
177
205
|
if (!diffResult.diff && !diffResult.untracked?.length) return { ok: true, verdict: 'NO_DIFF', target: diffResult.target, summary: `Nothing to review for ${diffResult.target}.` };
|
|
178
206
|
|
|
179
207
|
const { ready, excluded } = await resolvePool(options);
|
|
@@ -184,13 +212,16 @@ export async function executeCodeReview(options = {}) {
|
|
|
184
212
|
const reviewerFamily = ADAPTER_CATALOG[reviewer.id]?.family || reviewer.family || null;
|
|
185
213
|
if (author && (reviewer.id === author || (authorFamily && authorFamily === reviewerFamily))) throw new Error(`${reviewer.id} shares a model family with the author ${author}, so it cannot review this independently`);
|
|
186
214
|
const independence = author ? 'verified' : 'unverified: the author was not named';
|
|
215
|
+
selection.reviewer = reviewer.id;
|
|
187
216
|
const truncated = diffResult.diff.length > MAX_DIFF;
|
|
188
217
|
const snapshot = reviewSnapshot(cwd, diffResult);
|
|
218
|
+
selection.commit = snapshot.commit;
|
|
189
219
|
let result;
|
|
190
220
|
try { result = await dispatchWithReceipt({
|
|
191
221
|
agent: reviewer.id, node: 'local', cwd: snapshot.directory, contextCwd: cwd, contextTask: options.task || `Review the code changes in ${diffResult.target}`,
|
|
192
|
-
prompt: buildReviewPrompt(diffResult.diff, { task: options.task, branch: options.branch || diffResult.target, stat: diffResult.stat, untracked: diffResult.untracked }),
|
|
222
|
+
prompt: buildReviewPrompt(diffResult.diff, { task: options.task, evidence: options.evidence, branch: options.branch || diffResult.target, stat: diffResult.stat, untracked: diffResult.untracked }),
|
|
193
223
|
permissions: { read: true, write: false }, allowUnknownAuth: options.allowUnknownAuth === true,
|
|
224
|
+
...(reviewer.id === 'codex' ? { reviewTarget: { type: 'custom', instructions: buildReviewPrompt(diffResult.diff, { task: options.task, evidence: options.evidence, branch: diffResult.target }) } } : {}),
|
|
194
225
|
}, { dispatchFn: options.dispatchFn, role: 'Reviewer' }); }
|
|
195
226
|
finally { removeWorktree(cwd, snapshot.directory); }
|
|
196
227
|
if (!result.ok) return { ok: false, target: diffResult.target, reviewer: reviewer.id, error: result.error, failureKind: result.failureKind, excluded };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Review results are append-only SSSS records, independent of thread lifetime. */
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { threadsFile } from './paths.mjs';
|
|
5
|
+
import { PluginStateStore } from './ssss-store.mjs';
|
|
6
|
+
|
|
7
|
+
const store = () => new PluginStateStore(path.join(path.dirname(threadsFile()), 'meta-harness-reviews.jsonl'), 'meta-harness');
|
|
8
|
+
const dispositions = { APPROVE: 'approved', REQUEST_CHANGES: 'changes_requested', NEEDS_DISCUSSION: 'needs_discussion', NO_DIFF: 'no_diff', FAILED: 'failed' };
|
|
9
|
+
|
|
10
|
+
export function readReviews() { return store().read([]); }
|
|
11
|
+
export function recordReview(result, selection = {}) {
|
|
12
|
+
const review = result.review;
|
|
13
|
+
const verdict = review?.verdict || result.verdict || 'FAILED';
|
|
14
|
+
if (!dispositions[verdict]) throw new Error('Invalid review verdict');
|
|
15
|
+
const entry = {
|
|
16
|
+
id: `review-${crypto.randomUUID()}`, timestamp: new Date().toISOString(),
|
|
17
|
+
workspace: selection.workspace, target: review?.target || result.target || selection.target || null,
|
|
18
|
+
commit: review?.snapshotCommit || selection.commit || null,
|
|
19
|
+
diff: selection.diff || '', untracked: selection.untracked || [],
|
|
20
|
+
reviewer: review?.reviewer || result.reviewer || selection.reviewer || null,
|
|
21
|
+
verdict, disposition: dispositions[verdict], result,
|
|
22
|
+
};
|
|
23
|
+
store().append(entry);
|
|
24
|
+
return entry;
|
|
25
|
+
}
|