total-recall-brain 3.41.6 → 3.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +3118 -17
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +84 -40
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/exit-after-flush.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-api-rotation.mjs +61 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +3 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +15 -7
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +51 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +67 -25
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +52 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +21 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/compat-harness.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +48 -22
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +9 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +21 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +9 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +10 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +30 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +19 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +222 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-worker.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +70 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +60 -19
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +32 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +24 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +88 -125
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +110 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +95 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +13 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +78 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/context-provider.mjs +113 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/src/cli/agent.mjs +11 -6
- package/src/cli/compact.mjs +3 -3
- package/src/cli/context.mjs +52 -19
- package/src/cli/harness.mjs +65 -147
- package/src/cli/init.mjs +4 -2
- package/src/cli/preflight.mjs +56 -13
- package/src/core/agent-manager.mjs +23 -329
- package/src/core/context-compactor.mjs +2 -1
- package/src/core/context-compiler.mjs +16 -25
- package/src/core/context-providers.mjs +69 -0
- package/src/core/meta-harness.mjs +27 -749
- package/src/core/plugin-runner.mjs +12 -5
- package/src/core/plugin-runtime.mjs +4 -2
- package/src/core/runtime.mjs +30 -604
- package/src/core/task-context.mjs +122 -0
- package/src/server/routes/context.mjs +4 -1
- package/scaffold/.agent/skills/decision/SKILL.md +0 -20
- package/scaffold/.agent/skills/decision/references/integration.md +0 -25
- package/scaffold/.agent/skills/decision/references/use-cases.md +0 -20
- package/scaffold/.agent/skills/meta-harness/SKILL.md +0 -15
- package/scaffold/.agent/skills/meta-harness/references/optimized/index.md +0 -20
- package/scaffold/.agent/skills/meta-harness/references/optimized/manifest.json +0 -79
- package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +0 -135
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-02.md +0 -17
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-03.md +0 -8
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-04.md +0 -2
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-05.md +0 -6
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-06.md +0 -14
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-07.md +0 -8
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-08.md +0 -4
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +0 -20
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-10.md +0 -6
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-11.md +0 -5
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-12.md +0 -7
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-13.md +0 -4
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-14.md +0 -7
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-15.md +0 -9
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-16.md +0 -5
|
@@ -6,20 +6,26 @@ import { evaluateGateCascade } from '../capability/index.mjs';
|
|
|
6
6
|
import { dispatchWithReceipt } from './dispatch.mjs';
|
|
7
7
|
import { notifyAlert } from './alerts.mjs';
|
|
8
8
|
import { recordRunReceipt } from './learner.mjs';
|
|
9
|
-
import { resolvePool } from './pool.mjs';
|
|
9
|
+
import { resolvePool, localExecutionNode } from './pool.mjs';
|
|
10
10
|
import { routeTask } from './router.mjs';
|
|
11
11
|
import { runQualityGate } from './gate.mjs';
|
|
12
12
|
import { isGitRepo, createWorktree, removeWorktree, commitAll, linkDependencies, deleteBranch } from './worktree.mjs';
|
|
13
|
+
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
13
14
|
|
|
14
15
|
export async function executeRoutedTask(task, options = {}) {
|
|
16
|
+
const remote = await forwardRemoteOperation('route', task, { ...options, execute: true });
|
|
17
|
+
if (remote) return remote;
|
|
18
|
+
if (options.node) localExecutionNode(options);
|
|
15
19
|
const cwd = options.cwd || process.cwd();
|
|
16
20
|
if (!isGitRepo(cwd)) throw new Error('Routed execution writes on its own branch, so it must run inside a git repository');
|
|
17
21
|
const route = options.route || await routeTask(task, options);
|
|
18
22
|
if (!route.selected) return { ok: false, route, attempts: [], error: 'No harness can take this task right now.' };
|
|
23
|
+
const node = localExecutionNode(options, route.placement);
|
|
19
24
|
const pool = options.pool || (await resolvePool(options)).ready.map(h => h.id);
|
|
20
25
|
const maxAttempts = options.maxAttempts || 3;
|
|
21
26
|
const id = `route-${Date.now().toString(36)}`;
|
|
22
27
|
const attempts = [];
|
|
28
|
+
const alerts = [];
|
|
23
29
|
const tried = new Set();
|
|
24
30
|
let harness = route.selected, diagnostic = null, previous = null;
|
|
25
31
|
|
|
@@ -31,23 +37,30 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
31
37
|
attempts.push(attempt);
|
|
32
38
|
try {
|
|
33
39
|
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}` : ''}`;
|
|
34
|
-
const result = await dispatchWithReceipt({ agent: harness, cwd: directory, contextCwd: cwd, contextTask: task, prompt, permissions: { read: true, write: true }, allowUnknownAuth: true }, { dispatchFn: options.dispatchFn, role: 'Routed', complexityScore: route.complexity?.score, escalatedFrom: previous });
|
|
40
|
+
const result = await dispatchWithReceipt({ agent: harness, node, cwd: directory, contextCwd: cwd, contextTask: task, prompt, permissions: { read: true, write: true }, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Routed', complexityScore: route.complexity?.score, escalatedFrom: previous });
|
|
35
41
|
attempt.ok = result.ok === true;
|
|
36
42
|
attempt.durationMs = result.durationMs;
|
|
37
43
|
attempt.output = result.response || null;
|
|
38
44
|
if (result.ok) {
|
|
39
45
|
attempt.commit = commitAll(directory, `meta-harness: ${task.slice(0, 60)}\n\nAgent: ${harness}`);
|
|
40
46
|
if (attempt.commit) attempt.branch = branch;
|
|
41
|
-
else {
|
|
47
|
+
else {
|
|
48
|
+
attempt.ok = false; attempt.error = 'The agent reported success but changed no files.';
|
|
49
|
+
await recordRunReceipt({ harness, role: 'Implementer', task, success: false, failureKind: 'no-change', error: attempt.error });
|
|
50
|
+
}
|
|
42
51
|
} else {
|
|
43
52
|
attempt.error = result.error;
|
|
44
53
|
attempt.failureKind = result.failureKind;
|
|
45
54
|
}
|
|
46
55
|
if (attempt.ok && options.gate) {
|
|
47
56
|
linkDependencies(cwd, directory);
|
|
48
|
-
attempt.gate = (options.gateFn || runQualityGate)(directory, options.gateOptions);
|
|
57
|
+
attempt.gate = await (options.gateFn || runQualityGate)(directory, options.gateOptions);
|
|
49
58
|
// A gate that could not run says nothing about this agent's work, so it does not escalate.
|
|
50
|
-
if (['unavailable', 'unconfigured'].includes(attempt.gate.status))
|
|
59
|
+
if (['unavailable', 'unconfigured'].includes(attempt.gate.status)) {
|
|
60
|
+
attempt.ok = false;
|
|
61
|
+
attempt.error = `The quality gate is ${attempt.gate.status}: ${attempt.gate.summary}`;
|
|
62
|
+
return { ok: false, route, attempts, error: attempt.error };
|
|
63
|
+
}
|
|
51
64
|
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 });
|
|
52
65
|
attempt.ok = attempt.gate.passed;
|
|
53
66
|
}
|
|
@@ -60,14 +73,14 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
60
73
|
if (!attempt.ok) { deleteBranch(cwd, branch); attempt.branch = null; }
|
|
61
74
|
}
|
|
62
75
|
|
|
63
|
-
if (attempt.ok) return { ok: true, route, gateApplied: Boolean(options.gate), branch: attempt.branch, commit: attempt.commit, harness, attempts };
|
|
76
|
+
if (attempt.ok) return { ok: true, route, gateApplied: Boolean(options.gate), branch: attempt.branch, commit: attempt.commit, harness, attempts, alerts };
|
|
64
77
|
|
|
65
78
|
const remaining = pool.filter(item => !tried.has(item));
|
|
66
79
|
const next = remaining.length ? evaluateGateCascade(harness, { testsPassed: false }, remaining) : null;
|
|
67
80
|
diagnostic = attempt.gate?.summary || attempt.error || 'The previous attempt failed.';
|
|
68
81
|
previous = harness;
|
|
69
82
|
harness = next?.recommendedHarness && !tried.has(next.recommendedHarness) ? next.recommendedHarness : null;
|
|
70
|
-
if (harness) await notifyAlert({ title: 'Meta Harness: escalating', message: `${previous} failed on "${task.slice(0, 60)}"; escalating to ${harness}.`, severity: 'warning' });
|
|
83
|
+
if (harness) alerts.push(await notifyAlert({ title: 'Meta Harness: escalating', message: `${previous} failed on "${task.slice(0, 60)}"; escalating to ${harness}.`, severity: 'warning' }));
|
|
71
84
|
}
|
|
72
|
-
return { ok: false, route, gateApplied: Boolean(options.gate), attempts, error: 'No attempt passed.' };
|
|
85
|
+
return { ok: false, route, gateApplied: Boolean(options.gate), attempts, alerts, error: 'No attempt passed.' };
|
|
73
86
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Compatibility interfaces for native hosts. All execution uses the plugin's common boundary. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getActiveAdapters, findBinary, inspectAgents, agentEnvironment } from './worker.mjs';
|
|
4
|
+
import { getInstalledVersion, getLatestVersion } from './tracker.mjs';
|
|
5
|
+
|
|
6
|
+
export const HARNESS_SPECS = Object.fromEntries(getActiveAdapters().map(adapter => [adapter.id, {
|
|
7
|
+
...adapter, defaultFlags: adapter.args, execType: adapter.input === 'stdin' ? 'pipe_stdin' : 'flag_last',
|
|
8
|
+
enforcement: 'advisory', enforcementEvidence: 'Use the measured preflight adapter report for enforcement evidence',
|
|
9
|
+
}]));
|
|
10
|
+
|
|
11
|
+
export function normalizeToolSelection(tools) {
|
|
12
|
+
if (tools === undefined || tools === null) return null;
|
|
13
|
+
const values = (Array.isArray(tools) ? tools : String(tools).split(',')).map(value => String(value).trim()).filter(Boolean);
|
|
14
|
+
if (!values.length || (values.length === 1 && values[0] === 'none')) return '';
|
|
15
|
+
if (values.length === 1 && ['all', 'default'].includes(values[0])) return null;
|
|
16
|
+
return values.join(',');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateHarnessOptions(id, options = {}) {
|
|
20
|
+
const adapter = getActiveAdapters().find(item => item.id === id);
|
|
21
|
+
if (!adapter) throw new Error(`Unknown harness ID "${id}"`);
|
|
22
|
+
if ((normalizeToolSelection(options.tools) !== null || options.settingSources !== undefined) && id !== 'claude') {
|
|
23
|
+
throw new Error(`Harness "${adapter.name}" does not support choosing its tool set or --setting-sources`);
|
|
24
|
+
}
|
|
25
|
+
if (options.settingSources !== undefined && (typeof options.settingSources !== 'string' || !/^(user|project|local)(,(user|project|local))*$/.test(options.settingSources))) throw new Error('Invalid --setting-sources');
|
|
26
|
+
if (options.allowUnknownAuth !== undefined && typeof options.allowUnknownAuth !== 'boolean') throw new Error('allowUnknownAuth must be an explicit boolean');
|
|
27
|
+
return adapter;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Legacy inspection helper only; execution flags are built by worker.mjs. */
|
|
31
|
+
export function buildHarnessArgs(spec, options = {}) {
|
|
32
|
+
const adapter = validateHarnessOptions(spec.id, options);
|
|
33
|
+
const args = [...adapter.args], extra = [];
|
|
34
|
+
const tools = normalizeToolSelection(options.tools);
|
|
35
|
+
if (tools !== null) extra.push('--tools', tools);
|
|
36
|
+
if (options.settingSources !== undefined) extra.push('--setting-sources', options.settingSources);
|
|
37
|
+
const promptFlag = args.at(-1) === '-p' ? args.pop() : null;
|
|
38
|
+
return [...args, ...extra, ...(promptFlag ? [promptFlag] : [])];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const shellQuote = value => "'" + String(value).replaceAll("'", "'\\''") + "'";
|
|
42
|
+
|
|
43
|
+
export function detectHarnesses() {
|
|
44
|
+
return getActiveAdapters().map(adapter => {
|
|
45
|
+
const binaryPath = findBinary(adapter.binary);
|
|
46
|
+
return { id: adapter.id, name: adapter.name, available: Boolean(binaryPath), binaryPath, description: adapter.docs, execType: adapter.input };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function detectHarnessesWithAuth(options = {}) {
|
|
51
|
+
const result = await (options.inspect || inspectAgents)(options);
|
|
52
|
+
return result.agents.map(agent => ({ ...agent, available: agent.installed, authed: agent.authenticated === true && !agent.blockedBy, authRoute: agent.authenticated === 'unknown' ? 'unknown' : agent.authenticated ? 'measured-login' : 'needs-auth' }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Kept for callers preparing environments; only an adapter's declared credentials may be resolved. */
|
|
56
|
+
export async function resolveHarnessEnv(spec, baseEnv = process.env, { readSecret } = {}) {
|
|
57
|
+
const adapter = validateHarnessOptions(spec.id);
|
|
58
|
+
const env = agentEnvironment(baseEnv);
|
|
59
|
+
if (!Object.keys(adapter.credentials || {}).length) return env;
|
|
60
|
+
if (!readSecret) {
|
|
61
|
+
const { runProcess } = await import('./worker.mjs');
|
|
62
|
+
const binary = findBinary(baseEnv.TOTAL_RECALL_BIN || 'total-recall', baseEnv);
|
|
63
|
+
if (!binary) throw new Error('Total Recall auth owner is unavailable');
|
|
64
|
+
readSecret = async key => {
|
|
65
|
+
const result = await runProcess(binary, ['auth', 'secret', 'get', key], { env: agentEnvironment(baseEnv) });
|
|
66
|
+
return result.exitCode === 0 ? result.stdout.trim() : null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
for (const [name, keys] of Object.entries(adapter.credentials)) {
|
|
70
|
+
for (const key of keys) { const value = await readSecret(key); if (value) { env[name] = value; break; } }
|
|
71
|
+
if (!env[name]) throw new Error(`Configure ${name} through auth before dispatch`);
|
|
72
|
+
}
|
|
73
|
+
return env;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function dispatchTask(id, prompt, options = {}, dependencies = {}) {
|
|
77
|
+
validateHarnessOptions(id, options);
|
|
78
|
+
const remote = options.node && options.node !== 'local';
|
|
79
|
+
if (remote && (typeof options.cwd !== 'string' || (!path.posix.isAbsolute(options.cwd) && !path.win32.isAbsolute(options.cwd)))) throw new Error('Choose an explicit absolute workspace on the remote node');
|
|
80
|
+
const { dispatchWithReceipt } = await import('./dispatch.mjs');
|
|
81
|
+
const result = await (dependencies.dispatch || dispatchWithReceipt)({
|
|
82
|
+
agent: id, cwd: remote ? options.cwd : path.resolve(options.cwd || process.cwd()), prompt,
|
|
83
|
+
node: options.node || 'local', permissions: { read: true, write: options.write === true },
|
|
84
|
+
allowUnknownAuth: options.allowUnknownAuth === true, timeoutMs: options.timeoutMs,
|
|
85
|
+
tools: options.tools, settingSources: options.settingSources, model: options.model,
|
|
86
|
+
}, { node: options.node || 'local', role: options.role || 'Dispatch' });
|
|
87
|
+
return { ...result, harnessId: id, harnessName: HARNESS_SPECS[id]?.name || id, success: result.ok === true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function runCouncil(prompt, ids, options = {}) {
|
|
91
|
+
const { coordinateTask } = await import('./coordinator.mjs');
|
|
92
|
+
return coordinateTask(prompt, { ...options, mode: 'council', execute: true, ...(ids ? { availableHarnesses: ids } : {}) });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const getInstalledHarnessVersion = id => getInstalledVersion(id) || 'unknown';
|
|
96
|
+
export const getLatestHarnessVersion = (id, installed) => getLatestVersion(id, installed);
|
|
97
|
+
|
|
98
|
+
export async function queryHarnessUsage(options = {}) {
|
|
99
|
+
const { inspectQuotas } = await import('./router.mjs');
|
|
100
|
+
const report = await (options.inspectQuotas || inspectQuotas)(options);
|
|
101
|
+
if (!report.ok) throw new Error(report.error || 'Harness usage is unavailable');
|
|
102
|
+
const remaining = value => typeof value === 'number' ? `${Math.max(0, 100 - value)}%` : '—';
|
|
103
|
+
return Object.entries(report.quotas || {}).filter(([id]) => !['system', 'metered'].includes(id)).map(([id, quota]) => ({
|
|
104
|
+
id, name: HARNESS_SPECS[id]?.name || id, available: quota.installed === true,
|
|
105
|
+
authed: quota.status === 'ready', authRoute: quota.status, status: quota.status,
|
|
106
|
+
version: quota.version || 'unknown', latestVersion: quota.latestVersion || null, isCurrent: quota.latestVersion ? quota.version === quota.latestVersion : null,
|
|
107
|
+
rolling5hRemainingPct: remaining(quota.rolling5hUsedPercent), weeklyRemainingPct: remaining(quota.weeklyUsedPercent),
|
|
108
|
+
quotaRemaining: quota.quotaNote || 'No usage source', enforcement: 'advisory', enforcementEvidence: null,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
@@ -4,19 +4,33 @@
|
|
|
4
4
|
* branch in its own worktree, and the Verifier inspects that exact commit from a different model
|
|
5
5
|
* family. Nothing merges into the operator's branch; the branch is the deliverable.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { recordRunReceipt } from './learner.mjs';
|
|
8
|
+
import { classifyTaskComplexity, synthesizeOneWayValve } from '../capability/index.mjs';
|
|
8
9
|
import { notifyAlert } from './alerts.mjs';
|
|
9
10
|
import { dispatchWithReceipt } from './dispatch.mjs';
|
|
10
|
-
import { resolvePool } from './pool.mjs';
|
|
11
|
+
import { resolvePool, localExecutionNode } from './pool.mjs';
|
|
11
12
|
import { createThread, appendThreadTurn, updateThread } from './threads.mjs';
|
|
12
13
|
import { executeCodeReview } from './review.mjs';
|
|
13
14
|
import { isGitRepo, isDirty, headCommit, createWorktree, removeWorktree, commitAll, diffBetween, linkDependencies, deleteBranch } from './worktree.mjs';
|
|
14
15
|
import { lastJsonWith } from './json-reply.mjs';
|
|
15
16
|
import { runQualityGate } from './gate.mjs';
|
|
17
|
+
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
18
|
+
|
|
19
|
+
const OPEN_THREADS = new Set();
|
|
20
|
+
const MAX_BLUEPRINT = 20000;
|
|
21
|
+
/** A stopped pipeline must not leave its thread reading as active. */
|
|
22
|
+
export function failOpenThreads(summary = 'The run was stopped before it finished.') {
|
|
23
|
+
for (const id of OPEN_THREADS) { try { updateThread(id, { status: 'failed', summary }); } catch { /* the thread store reports its own failure */ } }
|
|
24
|
+
OPEN_THREADS.clear();
|
|
25
|
+
}
|
|
16
26
|
|
|
17
27
|
export function assignPipelineRoles(pool = [], options = {}) {
|
|
18
28
|
const sorted = [...pool].sort((a, b) => b.qualityScore - a.qualityScore);
|
|
19
|
-
const findById = id =>
|
|
29
|
+
const findById = id => {
|
|
30
|
+
const harness = sorted.find(h => h.id === id);
|
|
31
|
+
if (!harness) throw new Error(`Selected harness ${id} is not ready for this task`);
|
|
32
|
+
return harness;
|
|
33
|
+
};
|
|
20
34
|
// The session's own harness leads only when it can take work; TR_ACTIVE_HARNESS is advisory.
|
|
21
35
|
const active = process.env.TR_ACTIVE_HARNESS;
|
|
22
36
|
const orchestrator = options.orchestrator ? findById(options.orchestrator) : sorted.find(h => h.id === active) || sorted[0] || null;
|
|
@@ -78,13 +92,15 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
78
92
|
|
|
79
93
|
const verifier = roles.verifier || roles.implementer;
|
|
80
94
|
const complexity = classifyTaskComplexity(task);
|
|
81
|
-
|
|
95
|
+
// Validate the repository before creating lifecycle state: an unborn repository has no usable base.
|
|
82
96
|
const baseCommit = headCommit(cwd);
|
|
83
|
-
const branch = options.branch || `meta-harness/${thread.id}`;
|
|
84
97
|
const warnings = isDirty(cwd) ? ['The working tree has uncommitted changes; the branch starts from the last commit and does not include them.'] : [];
|
|
98
|
+
const thread = createThread({ title: task.slice(0, 80), task, platform: roles.orchestrator.id, model: roles.orchestrator.name, status: 'active' });
|
|
99
|
+
OPEN_THREADS.add(thread.id);
|
|
100
|
+
const branch = options.branch || `meta-harness/${thread.id}`;
|
|
85
101
|
const run = async (stage, harness, prompt, { workdir = cwd, permissions, role = stage.role } = {}) => {
|
|
86
102
|
stage.status = 'running';
|
|
87
|
-
const result = await dispatchWithReceipt({ agent: harness.id, cwd: workdir, contextCwd: cwd, contextTask: task, prompt, permissions: permissions || stage.permissions, allowUnknownAuth: true }, { dispatchFn: options.dispatchFn, role, complexityScore: complexity.score });
|
|
103
|
+
const result = await dispatchWithReceipt({ agent: harness.id, node: 'local', cwd: workdir, contextCwd: cwd, contextTask: task, prompt, permissions: permissions || stage.permissions, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role, complexityScore: complexity.score });
|
|
88
104
|
stage.status = result.ok ? 'completed' : 'failed';
|
|
89
105
|
stage.output = result.response || result.error || null;
|
|
90
106
|
stage.failureKind = result.ok ? undefined : result.failureKind;
|
|
@@ -104,13 +120,15 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
104
120
|
}
|
|
105
121
|
const architect = await run(stage('Architect'), roles.architect, `[ROLE: ARCHITECT]\nDo not change any file. Task:\n${task}\n\nExplorer digest:\n${digest || 'None.'}\n\nProduce a concise blueprint, the files to change, and how to verify the result.`);
|
|
106
122
|
if (!architect.ok) { ok = false; return finish(); }
|
|
123
|
+
const blueprint = architect.response.length > MAX_BLUEPRINT ? `${architect.response.slice(0, MAX_BLUEPRINT)}\n[Blueprint shortened to ${MAX_BLUEPRINT} characters.]` : architect.response;
|
|
107
124
|
|
|
108
125
|
implementerDir = createWorktree(cwd, { branch });
|
|
109
|
-
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${
|
|
126
|
+
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 });
|
|
110
127
|
if (!implementer.ok) { ok = false; return finish(); }
|
|
111
128
|
commit = commitAll(implementerDir, `meta-harness: ${task.slice(0, 60)}\n\nImplementer: ${roles.implementer.id}`);
|
|
112
129
|
if (!commit) {
|
|
113
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.' });
|
|
114
132
|
ok = false; return finish();
|
|
115
133
|
}
|
|
116
134
|
stage('Implementer').commit = commit;
|
|
@@ -118,21 +136,21 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
118
136
|
|
|
119
137
|
if (options.gate) {
|
|
120
138
|
linkDependencies(cwd, implementerDir);
|
|
121
|
-
gate = runQualityGate(implementerDir, options.gateOptions);
|
|
139
|
+
gate = await (options.gateFn || runQualityGate)(implementerDir, options.gateOptions);
|
|
122
140
|
if (!gate.passed) { ok = false; stage('Verifier').status = 'skipped'; stage('Verifier').output = `Quality gate did not pass: ${gate.summary}`; return finish(); }
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
const diff = diffBetween(cwd, baseCommit, commit);
|
|
126
144
|
const verifyDir = createWorktree(cwd, { ref: commit, prefix: 'meta-harness-verify-' });
|
|
127
145
|
try {
|
|
128
|
-
const verification = await run(stage('Verifier'), verifier, `[ROLE: INDEPENDENT VERIFIER]\nYou did not write this code. Do not change any file. Run the checks you can and audit commit ${commit} for this task:\n${task}\n\nBlueprint:\n${
|
|
146
|
+
const verification = await run(stage('Verifier'), verifier, `[ROLE: INDEPENDENT VERIFIER]\nYou did not write this code. Do not change any file. Run the checks you can and audit commit ${commit} for this task:\n${task}\n\nBlueprint:\n${blueprint}\n\nDiff${diff.length > 30000 ? ' (first 30000 characters; read the rest from this checkout)' : ''}:\n${diff.slice(0, 30000)}\n\nEnd your reply with exactly one JSON object: {"verdict":"PASS"|"FAIL","findings":["..."]}`, { workdir: verifyDir });
|
|
129
147
|
const parsed = verification.ok ? parseVerification(verification.response) : { parsed: false, verdict: 'NOT_RUN', findings: [] };
|
|
130
148
|
Object.assign(stage('Verifier'), { commit, verdict: parsed.verdict, findings: parsed.findings, passed: parsed.verdict === 'PASS' });
|
|
131
|
-
if (parsed.verdict !== 'PASS') ok = false;
|
|
149
|
+
if (parsed.verdict !== 'PASS') { ok = false; return finish(); }
|
|
132
150
|
} finally { removeWorktree(cwd, verifyDir); }
|
|
133
151
|
|
|
134
152
|
if (options.autoReview) {
|
|
135
|
-
const review = await executeCodeReview({ cwd, author: roles.implementer.id, threadId: thread.id, task, target: `${baseCommit}...${commit}`, dispatchFn: options.dispatchFn, reviewer: options.reviewer });
|
|
153
|
+
const review = await executeCodeReview({ cwd, author: roles.implementer.id, threadId: thread.id, task, target: `${baseCommit}...${commit}`, dispatchFn: options.dispatchFn, reviewer: options.reviewer, allowUnknownAuth: options.allowUnknownAuth === true, availableHarnesses: options.availableHarnesses });
|
|
136
154
|
stages.push({ role: 'Reviewer', harness: review.review?.reviewer || null, status: review.ok ? 'completed' : 'failed', verdict: review.review?.verdict || null, riskScore: review.review?.riskScore ?? null, output: review.review?.summary || review.error || null });
|
|
137
155
|
if (!review.ok || review.review?.verdict !== 'APPROVE') ok = false;
|
|
138
156
|
}
|
|
@@ -145,14 +163,16 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
145
163
|
return finish();
|
|
146
164
|
} finally {
|
|
147
165
|
removeWorktree(cwd, implementerDir);
|
|
148
|
-
if (implementerDir && !commit) deleteBranch(cwd, branch);
|
|
166
|
+
if (implementerDir && (!commit || !ok)) deleteBranch(cwd, branch);
|
|
149
167
|
}
|
|
150
168
|
|
|
151
169
|
function finish() {
|
|
152
170
|
return (async () => {
|
|
153
|
-
|
|
171
|
+
for (const item of stages) if (item.status === 'planned') item.status = 'skipped';
|
|
172
|
+
OPEN_THREADS.delete(thread.id);
|
|
173
|
+
updateThread(thread.id, { status: ok ? 'completed' : 'failed', branch: ok && commit ? branch : null, commit, baseCommit });
|
|
154
174
|
const alert = await notifyAlert({ title: ok ? 'Meta Harness: pipeline passed' : 'Meta Harness: pipeline failed', message: `${task.slice(0, 80)}${commit ? ` (branch ${branch})` : ''}`, severity: ok ? 'info' : 'warning' });
|
|
155
|
-
return { ...base, ok, threadId: thread.id, branch: commit ? branch : null, commit, baseCommit, gate, warnings, durationMs: Date.now() - started, alert };
|
|
175
|
+
return { ...base, ok, threadId: thread.id, branch: ok && commit ? branch : null, commit, baseCommit, gate, warnings, durationMs: Date.now() - started, alert };
|
|
156
176
|
})();
|
|
157
177
|
}
|
|
158
178
|
}
|
|
@@ -165,7 +185,7 @@ async function runCouncil(task, poolInfo, options) {
|
|
|
165
185
|
if (!seats.length) throw new Error('No harness can take work right now');
|
|
166
186
|
const cwd = options.cwd || process.cwd();
|
|
167
187
|
const votes = await Promise.all(seats.map(async harness => {
|
|
168
|
-
const result = await dispatchWithReceipt({ agent: harness, 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: true }, { dispatchFn: options.dispatchFn, role: 'Council' });
|
|
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' });
|
|
169
189
|
return { harness, ok: result.ok === true, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }), feedback: result.response || null };
|
|
170
190
|
}));
|
|
171
191
|
const count = vote => votes.filter(item => item.vote === vote).length;
|
|
@@ -178,20 +198,23 @@ async function runRace(task, poolInfo, options) {
|
|
|
178
198
|
const seats = options.candidates || [...poolInfo.ready].sort((a, b) => b.qualityScore - a.qualityScore).slice(0, 2).map(h => h.id);
|
|
179
199
|
if (options.execute !== true) return { ok: seats.length > 1, mode: 'race', task, executedLive: false, excluded: poolInfo.excluded, candidates: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
180
200
|
if (seats.length < 2) throw new Error('A race needs at least two harnesses that can take work');
|
|
201
|
+
if (!options.gate) return { ok: false, mode: 'race', task, executedLive: false, verification: 'unverified', gateApplied: false, error: 'A race requires a deterministic quality gate before any candidate can win.' };
|
|
181
202
|
const cwd = options.cwd || process.cwd();
|
|
182
203
|
if (!isGitRepo(cwd)) throw new Error('A race writes on separate branches, so it must run inside a git repository');
|
|
183
204
|
const id = `race-${Date.now().toString(36)}`;
|
|
184
205
|
const entries = await Promise.all(seats.map(async harness => {
|
|
185
206
|
const branch = `meta-harness/${id}-${harness}`;
|
|
186
|
-
|
|
207
|
+
let directory;
|
|
187
208
|
let keep = false;
|
|
188
209
|
try {
|
|
189
|
-
const
|
|
210
|
+
const started = Date.now();
|
|
211
|
+
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' });
|
|
190
213
|
const commit = result.ok ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
|
|
191
214
|
if (commit && options.gate) linkDependencies(cwd, directory);
|
|
192
|
-
const gate = commit
|
|
193
|
-
keep = Boolean(commit) &&
|
|
194
|
-
return { harness, branch: keep ? branch : null, commit: keep ? commit : null, ok: keep, gate, durationMs:
|
|
215
|
+
const gate = commit ? await (options.gateFn || runQualityGate)(directory, options.gateOptions) : null;
|
|
216
|
+
keep = Boolean(commit) && gate?.passed === true;
|
|
217
|
+
return { harness, branch: keep ? branch : null, commit: keep ? commit : null, ok: keep, gate, completedAt: Date.now(), durationMs: Date.now() - started, error: result.ok ? (commit ? (keep ? undefined : 'The quality gate failed') : 'No files changed') : result.error };
|
|
195
218
|
} catch (error) {
|
|
196
219
|
return { harness, branch: null, commit: null, ok: false, gate: null, durationMs: null, error: String(error.message) };
|
|
197
220
|
} finally {
|
|
@@ -199,15 +222,18 @@ async function runRace(task, poolInfo, options) {
|
|
|
199
222
|
if (!keep) deleteBranch(cwd, branch);
|
|
200
223
|
}
|
|
201
224
|
}));
|
|
202
|
-
const finishers = entries.filter(entry => entry.ok).sort((a, b) => a.
|
|
203
|
-
return { ok: finishers.length > 0, mode: 'race', task, executedLive: true, gateApplied:
|
|
225
|
+
const finishers = entries.filter(entry => entry.ok).sort((a, b) => a.completedAt - b.completedAt);
|
|
226
|
+
return { ok: finishers.length > 0, mode: 'race', task, executedLive: true, gateApplied: true, verification: finishers.length ? 'passed' : 'failed', winner: finishers[0]?.harness || null, excluded: poolInfo.excluded, candidates: entries };
|
|
204
227
|
}
|
|
205
228
|
|
|
206
229
|
export async function coordinateTask(taskDescription, options = {}) {
|
|
207
230
|
const task = String(taskDescription || '').trim();
|
|
208
231
|
if (!task) throw new Error('Task description is required for coordination');
|
|
232
|
+
const remote = await forwardRemoteOperation('coordinate', task, options);
|
|
233
|
+
if (remote) return remote;
|
|
209
234
|
const mode = options.mode || 'pipeline';
|
|
210
235
|
if (!['pipeline', 'orchestrate', 'council', 'race'].includes(mode)) throw new Error(`Unsupported coordination mode: ${mode}`);
|
|
236
|
+
localExecutionNode(options);
|
|
211
237
|
const poolInfo = await resolvePool(options);
|
|
212
238
|
if (mode === 'council') return runCouncil(task, poolInfo, options);
|
|
213
239
|
if (mode === 'race') return runRace(task, poolInfo, options);
|
|
@@ -4,7 +4,7 @@ import { classifyFailure, READ_ONLY_ADAPTERS } from './worker.mjs';
|
|
|
4
4
|
import { recordRunReceipt } from './learner.mjs';
|
|
5
5
|
import { isGitRepo, createWorktree, removeWorktree } from './worktree.mjs';
|
|
6
6
|
|
|
7
|
-
export async function dispatchWithReceipt(request, { dispatchFn, node = request.node || 'local', role, complexityScore, escalatedFrom } = {}) {
|
|
7
|
+
export async function dispatchWithReceipt(request, { dispatchFn, node = request.node || 'local', role, complexityScore, escalatedFrom, record = recordRunReceipt } = {}) {
|
|
8
8
|
const started = Date.now();
|
|
9
9
|
// A harness that cannot be told not to write does read-only work in a checkout that is thrown away.
|
|
10
10
|
const contain = node === 'local' && request.permissions?.write !== true && !READ_ONLY_ADAPTERS.includes(request.agent) && request.isolated !== true && isGitRepo(request.cwd);
|
|
@@ -24,10 +24,15 @@ export async function dispatchWithReceipt(request, { dispatchFn, node = request.
|
|
|
24
24
|
if (scratch) removeWorktree(request.cwd, scratch);
|
|
25
25
|
}
|
|
26
26
|
const durationMs = Date.now() - started;
|
|
27
|
-
|
|
27
|
+
// A finished run keeps its result: the worktree and any cost are real even when bookkeeping fails.
|
|
28
|
+
let receiptError;
|
|
29
|
+
try {
|
|
30
|
+
await record({
|
|
28
31
|
harness: request.agent, role, node, task: request.contextTask || request.prompt, complexityScore,
|
|
32
|
+
model: result.model ?? null, exitCode: result.exitCode ?? null, finishedReason: result.finishedReason ?? null,
|
|
29
33
|
success: result.ok === true, durationMs, failureKind: result.ok ? null : result.failureKind || 'agent',
|
|
30
34
|
error: result.ok ? null : result.error, resetsAt: result.resetsAt, escalatedFrom,
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
});
|
|
36
|
+
} catch (error) { receiptError = String(error?.message || error); }
|
|
37
|
+
return { ...result, durationMs, ...(receiptError ? { receiptError } : {}) };
|
|
33
38
|
}
|
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
|
+
import { loadCustomAdapters } from './worker.mjs';
|
|
6
7
|
|
|
7
8
|
export const DOC_SOURCES = [
|
|
8
9
|
{ id: 'claude', index: 'https://code.claude.com/docs/llms.txt', prefix: 'https://code.claude.com/docs/', catalog:true },
|
|
@@ -19,12 +20,22 @@ export const DOC_SOURCES = [
|
|
|
19
20
|
{ id: 'cline', index: 'https://docs.cline.bot/llms.txt', prefix: 'https://docs.cline.bot/', catalog:true },
|
|
20
21
|
{ id: 'openclaw', index: 'https://docs.openclaw.ai/llms.txt', prefix: 'https://docs.openclaw.ai/', catalog:true },
|
|
21
22
|
{ id: 'aider', index: 'https://aider.chat/docs/', prefix: 'https://aider.chat/docs/' },
|
|
22
|
-
{ id: 'goose', index: 'https://
|
|
23
|
+
{ id: 'goose', index: 'https://goose-docs.ai/llms.txt', prefix: 'https://goose-docs.ai/', catalog:true },
|
|
23
24
|
{ id: 'openhands', index: 'https://docs.openhands.dev/llms.txt', prefix: 'https://docs.openhands.dev/', catalog:true },
|
|
24
25
|
{ id: 'replit', index: 'https://docs.replit.com/llms.txt', prefix: 'https://docs.replit.com/', catalog:true },
|
|
25
26
|
{ id: 'lovable', index: 'https://docs.lovable.dev/llms.txt', prefix: 'https://docs.lovable.dev/', catalog:true },
|
|
26
27
|
{ id: 'ollama', index: 'https://docs.ollama.com/llms.txt', prefix: 'https://docs.ollama.com/', catalog:true },
|
|
27
28
|
];
|
|
29
|
+
function sourcesFor(agent) {
|
|
30
|
+
const sources = new Map(DOC_SOURCES.map(source => [source.id, source]));
|
|
31
|
+
for (const adapter of loadCustomAdapters()) if (adapter.docs) {
|
|
32
|
+
const url = new URL(adapter.docs);
|
|
33
|
+
sources.set(adapter.id, { id: adapter.id, index: url.href, prefix: new URL('.', url).href, origin: url.hostname });
|
|
34
|
+
}
|
|
35
|
+
const selected = agent ? [sources.get(agent)].filter(Boolean) : [...sources.values()];
|
|
36
|
+
if (!selected.length) throw new Error('Unknown documentation adapter or missing declared documentation URL');
|
|
37
|
+
return selected;
|
|
38
|
+
}
|
|
28
39
|
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
29
40
|
const TTL = 24 * 60 * 60 * 1000;
|
|
30
41
|
function pageFile(directory,id,file){if(!/^[a-f0-9]{64}-[a-f0-9]{64}\.txt$/.test(file || ''))throw new Error('Invalid cached documentation filename');return path.join(directory,id,file);}
|
|
@@ -39,11 +50,11 @@ export function documentationDirectory() {
|
|
|
39
50
|
}
|
|
40
51
|
function metadata(directory, id) { try { return JSON.parse(fs.readFileSync(path.join(directory,id,'index.json'),'utf8')); } catch { return null; } }
|
|
41
52
|
export function documentationStatus({ directory = documentationDirectory(), agent, now = Date.now() } = {}) {
|
|
42
|
-
const sources =
|
|
53
|
+
const sources = sourcesFor(agent);
|
|
43
54
|
if (!sources.length) throw new Error('Unknown documentation adapter');
|
|
44
55
|
const agents = sources.map(source => {
|
|
45
56
|
const saved = metadata(directory,source.id);
|
|
46
|
-
const complete = saved?.complete === true && saved.pages?.
|
|
57
|
+
const complete = saved?.complete === true && saved.pages?.some(page => page.url === source.index) && saved.pages.every(page => {
|
|
47
58
|
try { return hash(fs.readFileSync(pageFile(directory,source.id,page.file))) === page.sha256; } catch { return false; }
|
|
48
59
|
});
|
|
49
60
|
const age=now-Date.parse(saved?.checkedAt);
|
|
@@ -52,9 +63,10 @@ export function documentationStatus({ directory = documentationDirectory(), agen
|
|
|
52
63
|
});
|
|
53
64
|
return { ok:agents.every(row=>row.fresh), directory, agents };
|
|
54
65
|
}
|
|
55
|
-
async function download(url, fetchImpl) {
|
|
56
|
-
const allowed = new Set(['code.claude.com','claude.com','platform.claude.com','docs.anthropic.com','anthropic.com','developers.openai.com','openai.com','help.openai.com','learn.chatgpt.com','chatgpt.com','antigravity.google','cursor.com','docs.x.ai','dev.meta.ai','docs.devin.ai','docs.windsurf.com','windsurf.com','docs.cline.bot','docs.openclaw.ai','aider.chat','docs.replit.com','docs.lovable.dev','docs.ollama.com','block.github.io','docs.openhands.dev','raw.githubusercontent.com','api.github.com']);
|
|
66
|
+
async function download(url, fetchImpl, declaredOrigin) {
|
|
67
|
+
const allowed = new Set(['code.claude.com','claude.com','platform.claude.com','docs.anthropic.com','anthropic.com','developers.openai.com','openai.com','help.openai.com','learn.chatgpt.com','chatgpt.com','antigravity.google','cursor.com','docs.x.ai','dev.meta.ai','docs.devin.ai','docs.windsurf.com','windsurf.com','docs.cline.bot','docs.openclaw.ai','aider.chat','docs.replit.com','docs.lovable.dev','docs.ollama.com','goose-docs.ai','block.github.io','docs.openhands.dev','raw.githubusercontent.com','api.github.com']);
|
|
57
68
|
let target = new URL(url);
|
|
69
|
+
if (declaredOrigin) allowed.add(declaredOrigin);
|
|
58
70
|
for(let redirects=0;redirects<5;redirects++) {
|
|
59
71
|
if(target.protocol!=='https:' || target.username || target.password || !allowed.has(target.hostname)) throw new Error(`Unapproved official documentation origin: ${target.hostname}`);
|
|
60
72
|
const response=await fetchImpl(target,{redirect:'manual',signal:AbortSignal.timeout(20000),headers:{'User-Agent':'Total-Recall-Meta-Harness-Docs','Accept':'*/*'}});
|
|
@@ -80,7 +92,7 @@ function links(text,base,prefix) {
|
|
|
80
92
|
.filter(url=>url.startsWith(prefix) && !/%3[CE]|[<>"'`\s]/i.test(url) && !/\.(png|jpg|jpeg|svg|gif|css|js|woff2?|ico|pdf|json|ya?ml|xml|zip|gz|tgz|lock)$/i.test(url));
|
|
81
93
|
}
|
|
82
94
|
export async function refreshDocumentation({ directory = documentationDirectory(), agent, fetchImpl = fetch, now = () => new Date(), maxPages = 1500 } = {}) {
|
|
83
|
-
const sources=
|
|
95
|
+
const sources=sourcesFor(agent);
|
|
84
96
|
if(!sources.length)throw new Error('Unknown documentation adapter');
|
|
85
97
|
const reports=[];
|
|
86
98
|
for(const source of sources) {
|
|
@@ -98,7 +110,7 @@ export async function refreshDocumentation({ directory = documentationDirectory(
|
|
|
98
110
|
const batch=[];
|
|
99
111
|
while(queue.length && batch.length<4){const url=queue.shift();if(!seen.has(url)){seen.add(url);batch.push(url);}}
|
|
100
112
|
if(seen.size>maxPages)throw new Error('Official documentation exceeds configured page budget');
|
|
101
|
-
const downloads=await Promise.allSettled(batch.map(async url=>({url,text:await download(url,fetchImpl)})));
|
|
113
|
+
const downloads=await Promise.allSettled(batch.map(async url=>({url,text:await download(url,fetchImpl,source.origin)})));
|
|
102
114
|
for(let index=0;index<downloads.length;index++) {
|
|
103
115
|
const result=downloads[index];
|
|
104
116
|
if(result.status==='rejected'){
|
|
@@ -152,7 +164,7 @@ export async function maintainDocumentation(options={}) {
|
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
export function readDocumentation({agent,url,directory=documentationDirectory()}) {
|
|
155
|
-
|
|
167
|
+
sourcesFor(agent);
|
|
156
168
|
const index=metadata(directory,agent);
|
|
157
169
|
if(!url)return {agent,pages:index?.pages?.map(page=>({url:page.url,sha256:page.sha256,bytes:page.bytes})) || []};
|
|
158
170
|
const page=index?.pages?.find(page=>page.url===url);
|
|
@@ -164,7 +176,7 @@ export function readDocumentation({agent,url,directory=documentationDirectory()}
|
|
|
164
176
|
|
|
165
177
|
export function searchDocumentation({agent,query,directory=documentationDirectory()}) {
|
|
166
178
|
if(typeof query!=='string' || !query.trim())throw new Error('Documentation query required');
|
|
167
|
-
const sources=
|
|
179
|
+
const sources=sourcesFor(agent);
|
|
168
180
|
const matches=[],phrase=query.trim().toLowerCase(),terms=phrase.split(/\s+/).filter(Boolean);
|
|
169
181
|
for(const source of sources)for(const page of metadata(directory,source.id)?.pages || []) {
|
|
170
182
|
const doc=readDocumentation({agent:source.id,url:page.url,directory});const text=doc.content.toLowerCase();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
-
import { runWorker, runProcess, getActiveAdapters, notReady } from './worker.mjs';
|
|
4
|
+
import { runWorker, runProcess, getActiveAdapters, notReady, agentEnvironment } from './worker.mjs';
|
|
5
|
+
import { dispatchWithReceipt } from './dispatch.mjs';
|
|
5
6
|
|
|
6
7
|
async function hostModule(name) {
|
|
7
8
|
const selected = process.env.TR_PACKAGE_ROOT;
|
|
@@ -49,7 +50,7 @@ export async function executeOnNode(node, request, { worker = runWorker, process
|
|
|
49
50
|
const resolved = access.resolveNodeAccess(target);
|
|
50
51
|
const args = access.buildSshArgs(resolved, { command: 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"; exec node --input-type=module', extraOptions: ['BatchMode=yes', 'ConnectTimeout=10'] });
|
|
51
52
|
if (!args) throw new Error('Selected mesh node has no verified SSH access');
|
|
52
|
-
const result = await processRunner('ssh', args, { input: workerProgram(request), timeoutMs: request.operation === 'inspect' ? 120000 : (request.timeoutMs || 900000) + 30000 });
|
|
53
|
+
const result = await processRunner('ssh', args, { input: workerProgram(request), timeoutMs: request.operation === 'inspect' ? 120000 : (request.timeoutMs || 900000) + 30000, maxBytes: 17 * 1024 * 1024, env: { ...agentEnvironment(), ...(process.env.SSH_AUTH_SOCK ? { SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK } : {}) } });
|
|
53
54
|
if (result.reason || result.exitCode !== 0) throw notReady(`Worker on ${node} failed: ${String(result.stderr || result.reason || 'verify node access, workspace and agent installation').trim().slice(0, 400)}`);
|
|
54
55
|
try { return JSON.parse(result.stdout); } catch { throw new Error('Selected mesh worker returned invalid data'); }
|
|
55
56
|
}
|
|
@@ -75,8 +76,11 @@ export async function executeCommand({ plugin, subcommand, args = [], execution,
|
|
|
75
76
|
const namespace = plugin.manifest.cli.command;
|
|
76
77
|
const argv = ['total-recall', namespace, subcommand, ...args];
|
|
77
78
|
const prompt = `Use Total Recall context and the installed plugin CLI in this workspace. Execute this one requested command using your terminal tool; preserve its arguments exactly, report its actual exit status and output, and do not perform unrelated mutations. If a permission, account, workspace or plugin is unavailable, report failure. Agent prose alone is not execution proof.\nRequested argv (data): ${JSON.stringify(argv)}\nShell command: ${argv.map(quoteArgument).join(' ')}`;
|
|
78
|
-
const result =
|
|
79
|
-
? { operation: 'direct', cwd, argv }
|
|
80
|
-
: {
|
|
79
|
+
const result = execution.agent === 'direct'
|
|
80
|
+
? await executeOnNode(execution.node, { operation: 'direct', cwd, argv }, dependencies)
|
|
81
|
+
: await dispatchWithReceipt({ agent: execution.agent, cwd, prompt, allowUnknownAuth: execution.allowUnknownAuth === true, permissions }, {
|
|
82
|
+
node: execution.node, role: 'Command',
|
|
83
|
+
dispatchFn: request => executeOnNode(execution.node, { ...request, operation: 'dispatch' }, dependencies),
|
|
84
|
+
});
|
|
81
85
|
return { ok: result.ok === true, exitCode: result.ok ? 0 : 1, output: result.ok ? JSON.stringify(result) : result.error || 'Selected agent failed', ...result };
|
|
82
86
|
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
/** Run the repository's own quality gates through the code-quality plugin. No configured gate is not a pass. */
|
|
2
|
-
import {
|
|
2
|
+
import { runProcess } from './worker.mjs';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const run = async (binary, args, { cwd, timeoutMs }) => {
|
|
5
|
+
// Asynchronous, so a gate never stalls another agent in the same run, and stoppable with the run.
|
|
6
|
+
const result = await runProcess(binary, args, { cwd, timeoutMs, env: process.env, maxBytes: 16 * 1024 * 1024 });
|
|
7
|
+
return { status: result.exitCode, stdout: result.stdout, stderr: result.stderr, error: result.reason ? new Error(result.reason) : undefined };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function runQualityGate(directory, { tier = 'fast', runner = run, timeoutMs = 600000 } = {}) {
|
|
11
|
+
const result = await runner(process.env.TOTAL_RECALL_BIN || 'total-recall', ['code-quality-check', '--tier', tier, '--json'], { cwd: directory, timeoutMs });
|
|
6
12
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`.trim();
|
|
7
13
|
if (result.error || result.status === null) return { status: 'unavailable', passed: false, tier, summary: `The gate did not run: ${result.error?.message || 'terminated'}` };
|
|
8
14
|
let report = null;
|
|
9
15
|
try { report = JSON.parse(result.stdout); } catch { /* a text report */ }
|
|
10
|
-
if (/No code-quality state/.test(
|
|
16
|
+
if (/No code-quality state|No checks at tier/.test(output)) return { status: 'unconfigured', passed: false, tier, summary: String(report?.error || output).slice(0, 500) };
|
|
11
17
|
return { status: result.status === 0 ? 'passed' : 'failed', passed: result.status === 0, tier, summary: output.slice(-2000), report };
|
|
12
18
|
}
|