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
|
@@ -9,16 +9,52 @@ import { recordRunReceipt } from './learner.mjs';
|
|
|
9
9
|
import { resolvePool, localExecutionNode } from './pool.mjs';
|
|
10
10
|
import { routeTask } from './router.mjs';
|
|
11
11
|
import { runQualityGate } from './gate.mjs';
|
|
12
|
-
import { isGitRepo, createWorktree, removeWorktree, commitAll, linkDependencies, deleteBranch } from './worktree.mjs';
|
|
12
|
+
import { isGitRepo, isDirty, pushBranch, createWorktree, removeWorktree, commitAll, linkDependencies, deleteBranch, provideDependencies } from './worktree.mjs';
|
|
13
13
|
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
14
|
+
import { queueIncludedJob } from './usage-queue.mjs';
|
|
15
|
+
import { configDir } from './paths.mjs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { cancellationResult } from './worker.mjs';
|
|
18
|
+
import { executeCodeReview, reviewBlocksPush } from './review.mjs';
|
|
19
|
+
import { sessionOptions } from './session/run.mjs';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import { jobPaths } from './control.mjs';
|
|
22
|
+
|
|
23
|
+
// A routed attempt gets time in proportion to the work the route measured (A-101); --timeout-ms overrides.
|
|
24
|
+
export const ROUTED_TIMEOUT_MS = { local: 15 * 60_000, budget: 30 * 60_000, frontier: 90 * 60_000 };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What Metaharness itself recorded about this job, for the reviewer (A-122): questions the agent
|
|
28
|
+
* asked, the operator's replies, the goal check and the gate. The reviewer sees only the diff
|
|
29
|
+
* otherwise and cannot verify claims about how the job ran.
|
|
30
|
+
*/
|
|
31
|
+
export function reviewEvidence(jobId, attempt = {}) {
|
|
32
|
+
const evidence = { goalCheck: attempt.goalCheck ?? null, gate: attempt.gate ? { status: attempt.gate.status, passed: attempt.gate.passed } : null, questions: [], replies: [] };
|
|
33
|
+
if (!jobId) return evidence;
|
|
34
|
+
let paths;
|
|
35
|
+
try { paths = jobPaths(jobId); } catch { return evidence; }
|
|
36
|
+
const lines = file => { try { return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); } catch { return []; } };
|
|
37
|
+
for (const e of lines(paths.events)) if (e.kind === 'question') evidence.questions.push({ requestId: e.requestId, questions: e.questions || e.text, source: e.source, t: e.t });
|
|
38
|
+
for (const c of lines(paths.inbox)) if (c.type === 'reply' || c.command === 'reply') evidence.replies.push({ requestId: c.requestId || null, text: c.text, t: c.t });
|
|
39
|
+
return evidence;
|
|
40
|
+
}
|
|
14
41
|
|
|
15
42
|
export async function executeRoutedTask(task, options = {}) {
|
|
43
|
+
if (cancellationResult()) return cancellationResult();
|
|
16
44
|
const remote = await forwardRemoteOperation('route', task, { ...options, execute: true });
|
|
17
45
|
if (remote) return remote;
|
|
18
46
|
if (options.node) localExecutionNode(options);
|
|
19
47
|
const cwd = options.cwd || process.cwd();
|
|
48
|
+
const controls = sessionOptions({ ...options, cwd });
|
|
20
49
|
if (!isGitRepo(cwd)) throw new Error('Routed execution writes on its own branch, so it must run inside a git repository');
|
|
21
50
|
const route = options.route || await routeTask(task, options);
|
|
51
|
+
const replay = { kind: 'route', gate: options.gate === true, gateOptions: options.gateOptions, controls };
|
|
52
|
+
if (!route.selected && route.deferred) {
|
|
53
|
+
const blocked = { ok: false, failureKind: 'quota', error: 'The included usage allowance is exhausted.', resetsAt: route.deferred.resetsAt };
|
|
54
|
+
const job = await queueIncludedJob({ agent: route.deferred.harness, cwd, prompt: task }, blocked,
|
|
55
|
+
{ role: 'Routed', replay, launchPrevented: true });
|
|
56
|
+
return { ...blocked, route, attempts: [], queued: true, queueId: job.id };
|
|
57
|
+
}
|
|
22
58
|
if (!route.selected) return { ok: false, route, attempts: [], error: 'No harness can take this task right now.' };
|
|
23
59
|
const node = localExecutionNode(options, route.placement);
|
|
24
60
|
const pool = options.pool || (await resolvePool(options)).ready.map(h => h.id);
|
|
@@ -27,32 +63,50 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
27
63
|
const attempts = [];
|
|
28
64
|
const alerts = [];
|
|
29
65
|
const tried = new Set();
|
|
30
|
-
let harness = route.selected, diagnostic = null, previous = null;
|
|
66
|
+
let harness = route.selected, diagnostic = null, previous = null, carried = null;
|
|
67
|
+
const timeoutMs = options.timeoutMs || ROUTED_TIMEOUT_MS[route.tier] || ROUTED_TIMEOUT_MS.budget;
|
|
31
68
|
|
|
32
69
|
for (let index = 1; index <= maxAttempts && harness; index += 1) {
|
|
70
|
+
if (cancellationResult()) return { route, attempts, alerts, ...cancellationResult() };
|
|
33
71
|
tried.add(harness);
|
|
34
72
|
const branch = `meta-harness/${id}-${index}-${harness}`;
|
|
35
|
-
|
|
36
|
-
const
|
|
73
|
+
// An escalation continues from the previous attempt's unfinished work instead of starting over (A-102).
|
|
74
|
+
const directory = createWorktree(cwd, { branch, ...(carried ? { ref: carried } : {}) });
|
|
75
|
+
// A-107: each attempt streams its agent's events here live; the path is announced first.
|
|
76
|
+
const progressFile = options.progressDir === false ? undefined
|
|
77
|
+
: path.join(options.progressDir || path.join(configDir(), 'jobs'), `${id}-${index}-${harness}.events.jsonl`);
|
|
78
|
+
const attempt = { harness, branch: null, commit: null, gate: null, escalatedFrom: previous, ...(progressFile ? { progressFile } : {}) };
|
|
37
79
|
attempts.push(attempt);
|
|
80
|
+
if (progressFile) console.error(JSON.stringify({ progress: progressFile, attempt: index, harness }));
|
|
81
|
+
// A-109: the agent gets the repository's dependencies in its own checkout, so its tests run.
|
|
82
|
+
let stopped;
|
|
38
83
|
try {
|
|
84
|
+
attempt.dependencies = (options.provideDependencies || provideDependencies)(cwd, directory);
|
|
39
85
|
const prompt = `Work only in this checkout. Do not commit; the harness commits your changes.\nTask:\n${task}${diagnostic ? `\n\nA previous attempt by ${previous} failed. Diagnostic:\n${diagnostic}` : ''}`;
|
|
40
|
-
const result = await dispatchWithReceipt({ agent: harness, node, cwd: directory, contextCwd: cwd, contextTask: task, prompt, permissions: { read: true, write: true },
|
|
86
|
+
const result = await dispatchWithReceipt({ agent: harness, node, branch, cwd: directory, contextCwd: cwd, contextTask: task, prompt, permissions: { read: true, write: true }, timeoutMs,
|
|
87
|
+
...controls, goal: controls.goal || task,
|
|
88
|
+
...(progressFile ? { progressFile } : {}), allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Routed', complexityScore: route.complexity?.score, escalatedFrom: previous, replay, retryId: options.retryId });
|
|
41
89
|
attempt.ok = result.ok === true;
|
|
90
|
+
if (result.cancelled) stopped = result;
|
|
42
91
|
attempt.durationMs = result.durationMs;
|
|
43
|
-
attempt.
|
|
92
|
+
attempt.timeoutMs = timeoutMs;
|
|
93
|
+
attempt.output = result.response || result.partialOutput || null;
|
|
94
|
+
if (result.sessionId) { attempt.sessionId = result.sessionId; attempt.branch = branch; }
|
|
95
|
+
attempt.goalCheck = result.goalCheck;
|
|
96
|
+
attempt.structured = result.structured;
|
|
97
|
+
attempt.usage = result.usage;
|
|
98
|
+
if (!attempt.ok && result.stderr) attempt.stderr = result.stderr;
|
|
44
99
|
if (result.ok) {
|
|
45
100
|
attempt.commit = commitAll(directory, `meta-harness: ${task.slice(0, 60)}\n\nAgent: ${harness}`);
|
|
46
101
|
if (attempt.commit) attempt.branch = branch;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await recordRunReceipt({ harness, role: 'Implementer', task, success: false, failureKind: 'no-change', error: attempt.error });
|
|
50
|
-
}
|
|
102
|
+
// Keep the session branch even when the goal required no repository edit.
|
|
103
|
+
else if (result.sessionId) attempt.branch = branch;
|
|
51
104
|
} else {
|
|
52
105
|
attempt.error = result.error;
|
|
53
106
|
attempt.failureKind = result.failureKind;
|
|
107
|
+
if (result.queued || result.queueError || (result.failureKind === 'quota' && options.retryId) || ['billing-policy', 'limit', 'interrupted'].includes(result.failureKind)) stopped = result;
|
|
54
108
|
}
|
|
55
|
-
if (attempt.ok && options.gate) {
|
|
109
|
+
if (attempt.ok && options.gate && !cancellationResult()) {
|
|
56
110
|
linkDependencies(cwd, directory);
|
|
57
111
|
attempt.gate = await (options.gateFn || runQualityGate)(directory, options.gateOptions);
|
|
58
112
|
// A gate that could not run says nothing about this agent's work, so it does not escalate.
|
|
@@ -64,23 +118,54 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
64
118
|
await recordRunReceipt({ harness, role: 'Gate', task, success: attempt.gate.passed, failureKind: attempt.gate.passed ? null : 'gate', error: attempt.gate.passed ? null : attempt.gate.summary, gate: attempt.gate.status });
|
|
65
119
|
attempt.ok = attempt.gate.passed;
|
|
66
120
|
}
|
|
121
|
+
if (attempt.ok && attempt.commit && !cancellationResult()) {
|
|
122
|
+
attempt.review = await (options.reviewFn || executeCodeReview)({ cwd, author: harness, task,
|
|
123
|
+
evidence: reviewEvidence(options.jobId || process.env.TR_METAHARNESS_JOB, attempt),
|
|
124
|
+
target: `HEAD...${attempt.commit}`, dispatchFn: options.dispatchFn, reviewer: options.reviewer,
|
|
125
|
+
allowUnknownAuth: options.allowUnknownAuth === true, availableHarnesses: options.availableHarnesses });
|
|
126
|
+
if (reviewBlocksPush(attempt.review)) {
|
|
127
|
+
attempt.ok = false; attempt.failureKind = 'review';
|
|
128
|
+
attempt.error = attempt.review.error || JSON.stringify(attempt.review.review?.findings || []) || 'Review blocked the push.';
|
|
129
|
+
}
|
|
130
|
+
}
|
|
67
131
|
} catch (error) {
|
|
68
132
|
attempt.ok = false;
|
|
69
133
|
attempt.error = String(error.message);
|
|
70
134
|
} finally {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
135
|
+
if (cancellationResult()) {
|
|
136
|
+
Object.assign(attempt, cancellationResult());
|
|
137
|
+
stopped = cancellationResult();
|
|
138
|
+
}
|
|
139
|
+
// A failed attempt's edits are kept as an unfinished commit on its branch for review and for the
|
|
140
|
+
// next attempt to continue from; only an attempt that changed nothing loses its branch (A-102).
|
|
141
|
+
if (!attempt.ok && !attempt.commit) {
|
|
142
|
+
let wip = null;
|
|
143
|
+
try { wip = commitAll(directory, `meta-harness: unfinished attempt by ${harness}\n\n${String(attempt.error || '').slice(0, 300)}`); }
|
|
144
|
+
catch (error) { attempt.wipError = String(error.message); }
|
|
145
|
+
if (wip) { attempt.wip = wip; attempt.branch = branch; carried = wip; }
|
|
146
|
+
} else if (!attempt.ok && attempt.commit) carried = attempt.commit;
|
|
147
|
+
// Edits that could not be committed are never deleted (A-114): the checkout and branch stay for recovery.
|
|
148
|
+
if (attempt.wipError && isDirty(directory)) { attempt.keptWorktree = directory; attempt.branch = branch; }
|
|
149
|
+
else {
|
|
150
|
+
removeWorktree(cwd, directory);
|
|
151
|
+
if (!attempt.ok && !attempt.wip && !attempt.commit && !attempt.sessionId) { deleteBranch(cwd, branch); attempt.branch = null; }
|
|
152
|
+
}
|
|
74
153
|
}
|
|
75
154
|
|
|
76
|
-
if (
|
|
155
|
+
if (stopped?.cancelled) return { ...stopped, route, attempts, alerts, branch: attempt.branch, commit: attempt.commit, wip: attempt.wip, harness };
|
|
156
|
+
if (attempt.ok) {
|
|
157
|
+
const push = options.push === false ? { pushed: false, reason: 'push disabled for this run' } : pushBranch(cwd, attempt.branch);
|
|
158
|
+
return { ok: true, route, goalCheck: attempt.goalCheck, structured: attempt.structured, review: attempt.review,
|
|
159
|
+
gateApplied: Boolean(options.gate), branch: attempt.branch, commit: attempt.commit, push, harness, attempts, alerts };
|
|
160
|
+
}
|
|
161
|
+
if (stopped) return { ...stopped, route, attempts, alerts };
|
|
77
162
|
|
|
78
163
|
const remaining = pool.filter(item => !tried.has(item));
|
|
79
164
|
const next = remaining.length ? evaluateGateCascade(harness, { testsPassed: false }, remaining) : null;
|
|
80
165
|
diagnostic = attempt.gate?.summary || attempt.error || 'The previous attempt failed.';
|
|
81
166
|
previous = harness;
|
|
82
167
|
harness = next?.recommendedHarness && !tried.has(next.recommendedHarness) ? next.recommendedHarness : null;
|
|
83
|
-
if (harness) alerts.push(await notifyAlert({ title: '
|
|
168
|
+
if (harness) alerts.push(await notifyAlert({ title: 'Metaharness: escalating', message: `${previous} failed on "${task.slice(0, 60)}"; escalating to ${harness}.`, severity: 'warning' }));
|
|
84
169
|
}
|
|
85
170
|
return { ok: false, route, gateApplied: Boolean(options.gate), attempts, alerts, error: 'No attempt passed.' };
|
|
86
171
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Job commands and events. The inbox is append-only; ack events are its cursor. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { configDir } from './paths.mjs';
|
|
8
|
+
import { resolveJob } from './jobs.mjs';
|
|
9
|
+
|
|
10
|
+
const execute = promisify(execFile);
|
|
11
|
+
export function jobPaths(job) {
|
|
12
|
+
const id = typeof job === 'string' ? job : job?.id || job?.job;
|
|
13
|
+
if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id)) throw new Error('Invalid job ID.');
|
|
14
|
+
const base = path.join(configDir(), 'jobs', id);
|
|
15
|
+
return { id, inbox: `${base}.inbox.jsonl`, events: `${base}.events.jsonl` };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readLines(file) {
|
|
19
|
+
let data;
|
|
20
|
+
try { data = fs.readFileSync(file, 'utf8'); }
|
|
21
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
22
|
+
// An append from another process can still be incomplete.
|
|
23
|
+
return data.split('\n').slice(0, -1).filter(Boolean).map(line => JSON.parse(line));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function appendEvent(job, event) {
|
|
27
|
+
const { events } = jobPaths(job);
|
|
28
|
+
fs.mkdirSync(path.dirname(events), { recursive: true, mode: 0o700 });
|
|
29
|
+
const entry = { ...event, t: new Date().toISOString() };
|
|
30
|
+
fs.appendFileSync(events, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
31
|
+
return entry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read all requests, including requests older than watch's --tail. */
|
|
35
|
+
export function waitingRequests(job) {
|
|
36
|
+
const file = typeof job === 'string' && job.endsWith('.events.jsonl') ? job : jobPaths(job).events;
|
|
37
|
+
const waiting = new Map();
|
|
38
|
+
for (const event of readLines(file)) {
|
|
39
|
+
if (['question', 'approval'].includes(event.kind) && event.requestId) waiting.set(event.requestId, event);
|
|
40
|
+
if (event.kind === 'ack' && event.requestId && event.ok !== false) waiting.delete(event.requestId);
|
|
41
|
+
}
|
|
42
|
+
return [...waiting.values()];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeCommand(command) {
|
|
46
|
+
if (!command || typeof command !== 'object' || Array.isArray(command)) throw new Error('A job command is required.');
|
|
47
|
+
const type = command.type || command.command || command.kind;
|
|
48
|
+
if (!['reply', 'approve', 'interrupt', 'goal', 'fork'].includes(type)) throw new Error('Unsupported job command.');
|
|
49
|
+
if (['reply', 'goal'].includes(type) && (typeof command.text !== 'string' || !command.text.trim())) throw new Error(`${type} requires text.`);
|
|
50
|
+
if (type === 'approve' && (typeof command.requestId !== 'string' || !command.requestId || !['allow', 'deny'].includes(command.value))) {
|
|
51
|
+
throw new Error('approve requires a request ID and allow or deny.');
|
|
52
|
+
}
|
|
53
|
+
if (command.requestId !== undefined && (typeof command.requestId !== 'string' || !command.requestId)) throw new Error('Invalid request ID.');
|
|
54
|
+
return { ...command, type, command: type, id: command.id || randomUUID(), t: new Date().toISOString() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const quote = value => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
58
|
+
|
|
59
|
+
/** Resolve prefixes through the same owner as cancel. Remote delivery never writes a local inbox. */
|
|
60
|
+
export async function appendCommand(target, command, { local = false, execFileFn = execute } = {}) {
|
|
61
|
+
const job = resolveJob(typeof target === 'string' ? target : target?.id || target?.job);
|
|
62
|
+
if (job.result) throw new Error(`Job ${job.id} has finished.`);
|
|
63
|
+
const entry = normalizeCommand(command);
|
|
64
|
+
const node = job.record?.node;
|
|
65
|
+
if (!local && node && node !== 'local') {
|
|
66
|
+
if (typeof node !== 'string' || node.startsWith('-') || /[\0\r\n]/.test(node)) throw new Error('Invalid job node.');
|
|
67
|
+
const args = [entry.type, job.record.remoteJob || job.id];
|
|
68
|
+
if (entry.type === 'approve') args.push(entry.requestId, entry.value);
|
|
69
|
+
else if (!['interrupt', 'fork'].includes(entry.type)) args.push(entry.text);
|
|
70
|
+
if (entry.type === 'fork' && entry.atTurn) args.push('--at', entry.atTurn);
|
|
71
|
+
args.push('--local-delivery', '--json');
|
|
72
|
+
const remote = `total-recall meta-harness ${args.map(quote).join(' ')}`;
|
|
73
|
+
const result = await execFileFn(process.env.TOTAL_RECALL_BIN || 'total-recall', ['mesh', 'exec', node, remote],
|
|
74
|
+
{ encoding: 'utf8', timeout: 60_000, maxBuffer: 1024 * 1024 });
|
|
75
|
+
if (result.exitCode && result.exitCode !== 0) throw new Error(result.stderr || 'Remote command failed.');
|
|
76
|
+
// mesh exec may wrap the remote result. Transport failures must not look like delivery.
|
|
77
|
+
let report;
|
|
78
|
+
try { report = JSON.parse(result.stdout); } catch { /* mesh can return plain command output */ }
|
|
79
|
+
if (report?.ok === false) throw new Error(report.error || 'Remote command failed.');
|
|
80
|
+
return { ok: true, job: job.id, node, remote: true, command: entry, ...(report ? { delivery: report } : {}) };
|
|
81
|
+
}
|
|
82
|
+
const waiting = waitingRequests(job.id);
|
|
83
|
+
const reserved = new Set(readLines(jobPaths(job.id).inbox).map(row => row.requestId).filter(Boolean));
|
|
84
|
+
const request = entry.requestId ? waiting.find(row => row.requestId === entry.requestId)
|
|
85
|
+
: entry.type === 'reply' ? waiting.find(row => row.kind === 'question' && !reserved.has(row.requestId)) : null;
|
|
86
|
+
if (entry.requestId && !request) throw new Error(`No waiting request matches ${entry.requestId}.`);
|
|
87
|
+
if (request && entry.type === 'approve' && request.kind !== 'approval') throw new Error('The request is not an approval.');
|
|
88
|
+
if (request && entry.type === 'reply' && request.kind !== 'question') throw new Error('The request is not a question.');
|
|
89
|
+
if (request) {
|
|
90
|
+
if (reserved.has(request.requestId)) throw new Error('The request already has an answer in the inbox.');
|
|
91
|
+
entry.requestId = request.requestId;
|
|
92
|
+
if (request.source === 'mcp') entry.recipient = 'mcp';
|
|
93
|
+
// A deferred question is answered by the session runner, which starts the next turn with it.
|
|
94
|
+
else if (request.source === 'deferred') entry.recipient = 'deferred';
|
|
95
|
+
}
|
|
96
|
+
const { inbox } = jobPaths(job.id);
|
|
97
|
+
fs.mkdirSync(path.dirname(inbox), { recursive: true, mode: 0o700 });
|
|
98
|
+
fs.appendFileSync(inbox, JSON.stringify(entry) + '\n', { mode: 0o600 });
|
|
99
|
+
return { ok: true, job: job.id, command: entry };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* One consumer handles commands in file order. A false return leaves a command for
|
|
104
|
+
* another consumer. MCP answers have their own recipient so a worker cannot also
|
|
105
|
+
* send them to the driver. Await close() before releasing the driver.
|
|
106
|
+
*/
|
|
107
|
+
export function tailInbox(job, onCommand, { pollMs = 50, recipient = 'driver', onError = () => {} } = {}) {
|
|
108
|
+
const { inbox, events } = jobPaths(job);
|
|
109
|
+
fs.mkdirSync(path.dirname(inbox), { recursive: true, mode: 0o700 });
|
|
110
|
+
fs.closeSync(fs.openSync(inbox, 'a', 0o600));
|
|
111
|
+
let closed = false, running = null;
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const poll = () => {
|
|
114
|
+
if (closed || running) return running;
|
|
115
|
+
running = (async () => {
|
|
116
|
+
const acknowledged = new Set(readLines(events).filter(row => row.kind === 'ack' && row.commandId).map(row => row.commandId));
|
|
117
|
+
for (const command of readLines(inbox)) {
|
|
118
|
+
if (closed) break;
|
|
119
|
+
if (!command.id || seen.has(command.id) || acknowledged.has(command.id) || (command.recipient || 'driver') !== recipient) continue;
|
|
120
|
+
let result;
|
|
121
|
+
try { result = await onCommand(command); }
|
|
122
|
+
catch (error) {
|
|
123
|
+
// A failed command is reported once; do not repeat an agent side effect.
|
|
124
|
+
seen.add(command.id);
|
|
125
|
+
appendEvent(job, { kind: 'ack', command: command.type || command.command, commandId: command.id,
|
|
126
|
+
requestId: command.requestId, ok: false, text: error.message });
|
|
127
|
+
onError(error);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (result === false || result?.handled === false) continue;
|
|
131
|
+
seen.add(command.id);
|
|
132
|
+
appendEvent(job, { kind: 'ack', command: command.type || command.command, commandId: command.id,
|
|
133
|
+
requestId: command.requestId, ok: true, text: `Handled ${command.type || command.command}`, ...(result?.via ? { via: result.via } : {}) });
|
|
134
|
+
}
|
|
135
|
+
})().catch(onError).finally(() => { running = null; });
|
|
136
|
+
return running;
|
|
137
|
+
};
|
|
138
|
+
const timer = setInterval(poll, pollMs);
|
|
139
|
+
poll();
|
|
140
|
+
const close = async () => { closed = true; clearInterval(timer); await running; };
|
|
141
|
+
// A function is convenient for existing watcher callers; properties support explicit lifecycle use.
|
|
142
|
+
close.close = close;
|
|
143
|
+
close.poll = poll;
|
|
144
|
+
return close;
|
|
145
|
+
}
|
|
@@ -8,6 +8,7 @@ import { recordRunReceipt } from './learner.mjs';
|
|
|
8
8
|
import { classifyTaskComplexity, synthesizeOneWayValve } from '../capability/index.mjs';
|
|
9
9
|
import { notifyAlert } from './alerts.mjs';
|
|
10
10
|
import { dispatchWithReceipt } from './dispatch.mjs';
|
|
11
|
+
import { queueIncludedJob } from './usage-queue.mjs';
|
|
11
12
|
import { resolvePool, localExecutionNode } from './pool.mjs';
|
|
12
13
|
import { createThread, appendThreadTurn, updateThread } from './threads.mjs';
|
|
13
14
|
import { executeCodeReview } from './review.mjs';
|
|
@@ -15,6 +16,8 @@ import { isGitRepo, isDirty, headCommit, createWorktree, removeWorktree, commitA
|
|
|
15
16
|
import { lastJsonWith } from './json-reply.mjs';
|
|
16
17
|
import { runQualityGate } from './gate.mjs';
|
|
17
18
|
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
19
|
+
import { cancellationResult } from './worker.mjs';
|
|
20
|
+
import { sessionOptions } from './session/run.mjs';
|
|
18
21
|
|
|
19
22
|
const OPEN_THREADS = new Set();
|
|
20
23
|
const MAX_BLUEPRINT = 20000;
|
|
@@ -100,10 +103,14 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
100
103
|
const branch = options.branch || `meta-harness/${thread.id}`;
|
|
101
104
|
const run = async (stage, harness, prompt, { workdir = cwd, permissions, role = stage.role } = {}) => {
|
|
102
105
|
stage.status = 'running';
|
|
103
|
-
const result = await dispatchWithReceipt({ agent: harness.id, node: 'local', cwd: workdir, contextCwd: cwd, contextTask: task, prompt,
|
|
104
|
-
|
|
106
|
+
const result = await dispatchWithReceipt({ agent: harness.id, node: 'local', cwd: workdir, contextCwd: cwd, contextTask: task, prompt,
|
|
107
|
+
...sessionOptions({ ...options, cwd }), goal: stage.role === 'Implementer' ? options.goal || task : prompt.slice(0, 4000),
|
|
108
|
+
permissions: permissions || stage.permissions, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role, complexityScore: complexity.score });
|
|
109
|
+
stage.status = result.cancelled ? 'cancelled' : result.ok ? 'completed' : result.queued ? 'queued' : 'failed';
|
|
105
110
|
stage.output = result.response || result.error || null;
|
|
111
|
+
for (const key of ['goalCheck', 'structured', 'sessionId', 'progressFile', 'usage']) if (result[key] !== undefined) stage[key] = result[key];
|
|
106
112
|
stage.failureKind = result.ok ? undefined : result.failureKind;
|
|
113
|
+
if (result.queueId) stage.queueId = result.queueId;
|
|
107
114
|
appendThreadTurn(thread.id, { role: role.toLowerCase(), agent: harness.id, content: stage.output || '' });
|
|
108
115
|
return result;
|
|
109
116
|
};
|
|
@@ -126,17 +133,14 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
126
133
|
const implementer = await run(stage('Implementer'), roles.implementer, `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit; the harness commits your changes.\nTask:\n${task}\n\nBlueprint:\n${blueprint}`, { workdir: implementerDir });
|
|
127
134
|
if (!implementer.ok) { ok = false; return finish(); }
|
|
128
135
|
commit = commitAll(implementerDir, `meta-harness: ${task.slice(0, 60)}\n\nImplementer: ${roles.implementer.id}`);
|
|
129
|
-
if (!commit)
|
|
130
|
-
Object.assign(stage('Implementer'), { status: 'failed', failureKind: 'agent', output: 'The Implementer reported success but changed no files.' });
|
|
131
|
-
await recordRunReceipt({ harness: roles.implementer.id, role: 'Implementer', task, success: false, failureKind: 'no-change', error: 'The Implementer reported success but changed no files.' });
|
|
132
|
-
ok = false; return finish();
|
|
133
|
-
}
|
|
136
|
+
if (!commit) commit = baseCommit;
|
|
134
137
|
stage('Implementer').commit = commit;
|
|
135
138
|
updateThread(thread.id, { branch, worktreePath: null });
|
|
136
139
|
|
|
137
140
|
if (options.gate) {
|
|
138
141
|
linkDependencies(cwd, implementerDir);
|
|
139
142
|
gate = await (options.gateFn || runQualityGate)(implementerDir, options.gateOptions);
|
|
143
|
+
if (cancellationResult()) { ok = false; return finish(); }
|
|
140
144
|
if (!gate.passed) { ok = false; stage('Verifier').status = 'skipped'; stage('Verifier').output = `Quality gate did not pass: ${gate.summary}`; return finish(); }
|
|
141
145
|
}
|
|
142
146
|
|
|
@@ -163,16 +167,24 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
163
167
|
return finish();
|
|
164
168
|
} finally {
|
|
165
169
|
removeWorktree(cwd, implementerDir);
|
|
166
|
-
if (implementerDir && (!commit || !ok)) deleteBranch(cwd, branch);
|
|
170
|
+
if (implementerDir && (!commit || (!ok && !cancellationResult()))) deleteBranch(cwd, branch);
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
function finish() {
|
|
174
|
+
const cancelled = cancellationResult();
|
|
175
|
+
if (cancelled) {
|
|
176
|
+
ok = false;
|
|
177
|
+
if (implementerDir) commit = commitAll(implementerDir, 'meta-harness: unfinished pipeline from a cancelled job') || commit;
|
|
178
|
+
}
|
|
170
179
|
return (async () => {
|
|
171
180
|
for (const item of stages) if (item.status === 'planned') item.status = 'skipped';
|
|
172
181
|
OPEN_THREADS.delete(thread.id);
|
|
173
|
-
updateThread(thread.id, { status: ok ? 'completed' : 'failed', branch: ok && commit ? branch : null, commit, baseCommit });
|
|
174
|
-
const alert = await notifyAlert({ title: ok ? '
|
|
175
|
-
|
|
182
|
+
updateThread(thread.id, { status: ok ? 'completed' : 'failed', branch: (ok || cancelled) && commit ? branch : null, commit, baseCommit });
|
|
183
|
+
const alert = cancelled ? null : await notifyAlert({ title: ok ? 'Metaharness: pipeline passed' : 'Metaharness: pipeline failed', message: `${task.slice(0, 80)}${commit ? ` (branch ${branch})` : ''}`, severity: ok ? 'info' : 'warning' });
|
|
184
|
+
const limited = stages.find(item => item.failureKind === 'limit');
|
|
185
|
+
return { ...base, ok, ...cancelled, ...(limited ? { failureKind: 'limit', error: limited.output } : {}), goalCheck: stage('Implementer')?.goalCheck, structured: stage('Implementer')?.structured,
|
|
186
|
+
sessionId: stage('Implementer')?.sessionId, progressFile: stage('Implementer')?.progressFile,
|
|
187
|
+
threadId: thread.id, branch: (ok || cancelled) && commit ? branch : null, commit, baseCommit, gate, warnings, durationMs: Date.now() - started, alert };
|
|
176
188
|
})();
|
|
177
189
|
}
|
|
178
190
|
}
|
|
@@ -185,13 +197,15 @@ async function runCouncil(task, poolInfo, options) {
|
|
|
185
197
|
if (!seats.length) throw new Error('No harness can take work right now');
|
|
186
198
|
const cwd = options.cwd || process.cwd();
|
|
187
199
|
const votes = await Promise.all(seats.map(async harness => {
|
|
188
|
-
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd, contextTask: task, prompt: `[ROLE: COUNCIL MEMBER]\nDo not change any file. Assess this proposal, with its trade-offs and risks:\n${task}\n\nEnd your reply with exactly one JSON object: {"vote":"APPROVE"|"REJECT","reason":"..."}`, permissions: READ, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Council' });
|
|
189
|
-
return { harness, ok: result.ok === true, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }),
|
|
200
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), prompt: `[ROLE: COUNCIL MEMBER]\nDo not change any file. Assess this proposal, with its trade-offs and risks:\n${task}\n\nEnd your reply with exactly one JSON object: {"vote":"APPROVE"|"REJECT","reason":"..."}`, permissions: READ, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Council' });
|
|
201
|
+
return { harness, ok: result.ok === true, failureKind: result.failureKind, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }),
|
|
202
|
+
...(result.queueId ? { queueId: result.queueId } : {}), feedback: result.response || null };
|
|
190
203
|
}));
|
|
191
204
|
const count = vote => votes.filter(item => item.vote === vote).length;
|
|
192
205
|
const majority = votes.length / 2;
|
|
193
206
|
const consensus = count('APPROVE') > majority ? 'approved' : count('REJECT') > majority ? 'rejected' : 'no-consensus';
|
|
194
|
-
|
|
207
|
+
const limited = votes.some(item => item.failureKind === 'limit');
|
|
208
|
+
return { ok: !limited && votes.some(item => item.ok), ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'council', task, executedLive: true, consensus, excluded: poolInfo.excluded, votes };
|
|
195
209
|
}
|
|
196
210
|
|
|
197
211
|
async function runRace(task, poolInfo, options) {
|
|
@@ -209,12 +223,21 @@ async function runRace(task, poolInfo, options) {
|
|
|
209
223
|
try {
|
|
210
224
|
const started = Date.now();
|
|
211
225
|
directory = createWorktree(cwd, { branch });
|
|
212
|
-
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd: directory, contextCwd: cwd, contextTask: task, prompt: `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit.\nTask:\n${task}`, permissions: WRITE, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Race' });
|
|
213
|
-
const commit = result.ok ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
226
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd: directory, contextCwd: cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), goal: options.goal || task, prompt: `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit.\nTask:\n${task}`, permissions: WRITE, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Race' });
|
|
227
|
+
const commit = result.ok || result.cancelled ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
|
|
228
|
+
if (result.cancelled) {
|
|
229
|
+
keep = Boolean(commit);
|
|
230
|
+
return { ...result, harness, branch: keep ? branch : null, wip: commit };
|
|
231
|
+
}
|
|
232
|
+
if (result.ok && options.gate) linkDependencies(cwd, directory);
|
|
233
|
+
const gate = result.ok ? await (options.gateFn || runQualityGate)(directory, options.gateOptions) : null;
|
|
234
|
+
if (cancellationResult()) {
|
|
235
|
+
keep = Boolean(commit);
|
|
236
|
+
return { ...cancellationResult(), harness, branch: keep ? branch : null, commit };
|
|
237
|
+
}
|
|
238
|
+
keep = result.ok === true && gate?.passed === true;
|
|
239
|
+
return { harness, branch: keep ? branch : null, commit: keep ? commit : null, ok: keep, gate, goalCheck: result.goalCheck, structured: result.structured, failureKind: result.failureKind, completedAt: Date.now(), durationMs: Date.now() - started, error: result.ok ? keep ? undefined : 'The quality gate failed' : result.error,
|
|
240
|
+
...(result.queueId ? { queueId: result.queueId } : {}) };
|
|
218
241
|
} catch (error) {
|
|
219
242
|
return { harness, branch: null, commit: null, ok: false, gate: null, durationMs: null, error: String(error.message) };
|
|
220
243
|
} finally {
|
|
@@ -223,10 +246,12 @@ async function runRace(task, poolInfo, options) {
|
|
|
223
246
|
}
|
|
224
247
|
}));
|
|
225
248
|
const finishers = entries.filter(entry => entry.ok).sort((a, b) => a.completedAt - b.completedAt);
|
|
226
|
-
|
|
249
|
+
const limited = entries.some(item => item.failureKind === 'limit');
|
|
250
|
+
return { ok: !limited && finishers.length > 0, ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'race', task, executedLive: true, gateApplied: true, verification: !limited && finishers.length ? 'passed' : 'failed', winner: limited ? null : finishers[0]?.harness || null, excluded: poolInfo.excluded, candidates: entries };
|
|
227
251
|
}
|
|
228
252
|
|
|
229
253
|
export async function coordinateTask(taskDescription, options = {}) {
|
|
254
|
+
if (cancellationResult()) return cancellationResult();
|
|
230
255
|
const task = String(taskDescription || '').trim();
|
|
231
256
|
if (!task) throw new Error('Task description is required for coordination');
|
|
232
257
|
const remote = await forwardRemoteOperation('coordinate', task, options);
|
|
@@ -235,6 +260,14 @@ export async function coordinateTask(taskDescription, options = {}) {
|
|
|
235
260
|
if (!['pipeline', 'orchestrate', 'council', 'race'].includes(mode)) throw new Error(`Unsupported coordination mode: ${mode}`);
|
|
236
261
|
localExecutionNode(options);
|
|
237
262
|
const poolInfo = await resolvePool(options);
|
|
263
|
+
const wall = poolInfo.excluded.find(item => item.failureKind === 'quota');
|
|
264
|
+
const minimum = mode === 'race' || ((mode === 'pipeline' || mode === 'orchestrate') && options.allowSameFamily !== true) ? 2 : 1;
|
|
265
|
+
if (options.execute === true && wall && poolInfo.ready.length < minimum) {
|
|
266
|
+
const blocked = { ok: false, failureKind: 'quota', error: 'Coordination is waiting for included usage.', resetsAt: wall.resetsAt };
|
|
267
|
+
const job = await queueIncludedJob({ agent: wall.harness, cwd: options.cwd || process.cwd(), prompt: task }, blocked,
|
|
268
|
+
{ role: 'Coordinate', launchPrevented: true });
|
|
269
|
+
return { ...blocked, mode, executedLive: false, queued: true, queueId: job.id, needsReview: job.needsReview };
|
|
270
|
+
}
|
|
238
271
|
if (mode === 'council') return runCouncil(task, poolInfo, options);
|
|
239
272
|
if (mode === 'race') return runRace(task, poolInfo, options);
|
|
240
273
|
return runPipeline(task, poolInfo, options);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { sessionOptions } from './session/run.mjs';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PERMISSIONS = Object.freeze({ read: true, write: false, send: false, destructive: false });
|
|
5
|
+
export { DEFAULT_PERMISSIONS };
|
|
6
|
+
|
|
7
|
+
export function commandPermissions(value = DEFAULT_PERMISSIONS) {
|
|
8
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Command permissions must be boolean values');
|
|
9
|
+
for (const key of Object.keys(value)) {
|
|
10
|
+
if (!Object.hasOwn(DEFAULT_PERMISSIONS, key) || typeof value[key] !== 'boolean') throw new Error(`Invalid permission ${key}`);
|
|
11
|
+
}
|
|
12
|
+
return { ...DEFAULT_PERMISSIONS, ...value };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PUBLIC_FIELDS = [
|
|
16
|
+
'agent', 'cwd', 'prompt', 'permissions', 'node', 'allowUnknownAuth', 'timeoutMs',
|
|
17
|
+
'model', 'tools', 'settingSources', 'allowedTools',
|
|
18
|
+
'goal', 'maxTurns', 'maxTokens', 'maxBudgetUsd', 'resultSchema', 'approvalPolicy', 'questionTimeoutMs', 'effort', 'fallbackModel', 'name',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function validateDispatchRequest(request) {
|
|
22
|
+
if (!request || typeof request !== 'object' || Array.isArray(request)) throw new Error('Dispatch requires a JSON object');
|
|
23
|
+
if (Object.keys(request).some(key => !PUBLIC_FIELDS.includes(key))) throw new Error('Unsupported dispatch field; workspace context and containment are managed by Metaharness');
|
|
24
|
+
if (typeof request.agent !== 'string' || !/^[a-z][a-z0-9-]{0,63}$/.test(request.agent) ||
|
|
25
|
+
typeof request.cwd !== 'string' || !path.isAbsolute(request.cwd) || request.cwd.includes('\0') ||
|
|
26
|
+
typeof request.prompt !== 'string' || !request.prompt.trim() || Buffer.byteLength(request.prompt) > 65536) {
|
|
27
|
+
throw new Error('Dispatch requires an explicit agent, absolute workspace, and bounded nonempty prompt');
|
|
28
|
+
}
|
|
29
|
+
if (request.node !== undefined && (typeof request.node !== 'string' || !request.node.trim() || /[\0\r\n]/.test(request.node))) throw new Error('Invalid dispatch node');
|
|
30
|
+
if (request.allowUnknownAuth !== undefined && typeof request.allowUnknownAuth !== 'boolean') throw new Error('allowUnknownAuth must be a boolean');
|
|
31
|
+
const timeoutMs = request.timeoutMs === undefined ? 300000 : request.timeoutMs;
|
|
32
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 540000) throw new Error('Foreground dispatch timeout must be 1..540000 ms');
|
|
33
|
+
return { ...request, ...sessionOptions(request), permissions: commandPermissions(request.permissions), timeoutMs };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The installed destination starts from its own project context and decides containment itself. */
|
|
37
|
+
export function destinationDispatchRequest(request) {
|
|
38
|
+
const fields = PUBLIC_FIELDS.filter(key => key !== 'node' && request[key] !== undefined);
|
|
39
|
+
return validateDispatchRequest(Object.fromEntries(fields.map(key => [key, request[key]])));
|
|
40
|
+
}
|