total-recall-brain 3.46.2 → 3.47.1
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 +5 -0
- package/frontend/dist/assets/index-CpsWeV4X.css +1 -0
- package/frontend/dist/assets/{index-D1goIPjC.js → index-poecbHSl.js} +100 -100
- package/frontend/dist/brand/total-recall-lockup-transparent.png +0 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +1 -1
- package/frontend/src/App.tsx +15 -81
- package/frontend/src/components/DashboardOverview.tsx +164 -0
- package/frontend/src/components/brand/BrandMark.tsx +8 -6
- package/frontend/src/components/plugins/PluginWorkspace.tsx +112 -57
- package/frontend/src/components/workspace/PluginHome.tsx +3 -3
- package/frontend/src/index.css +43 -11
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +30 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/skills/auth/SKILL.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +494 -79
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +33 -8
- package/scaffold/.agent/skills/total-recall/plugins/automations/skills/automations/SKILL.md +29 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/dream-consolidation.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +31 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/skills/code-quality/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +24 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/skills/embeddings/SKILL.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +300 -58
- package/scaffold/.agent/skills/total-recall/plugins/git/skills/git/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +5 -33
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-fleet-ops.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-publish.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +47 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +28 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +7 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-selection.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +50 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +175 -17
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +242 -31
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/skills/openwiki/SKILL.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +41 -15
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +29 -22
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +20 -33
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +42 -49
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/markdown-links.mjs +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +219 -65
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +21 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +51 -2
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +22 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +169 -37
- package/scaffold/.agent/skills/total-recall/plugins/ssss/skills/ssss/SKILL.md +30 -0
- package/src/cli/context.mjs +6 -2
- package/src/cli/plugin/create.mjs +2 -1
- package/src/cli/plugin/monetization.mjs +66 -32
- package/src/cli/plugin/search.mjs +25 -3
- package/src/cli/preflight.mjs +40 -6
- package/src/core/context-compactor.mjs +2 -0
- package/src/core/gate-mode.mjs +13 -24
- package/src/core/plugin-distribution.mjs +35 -1
- package/src/core/preflight-gate.mjs +28 -8
- package/src/core/preflight-receipt.mjs +11 -0
- package/src/server/routes/vault.mjs +1 -0
- package/frontend/dist/assets/index-BRYE1jmv.css +0 -1
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
"$schema": "https://github.com/gregiteen/total-recall/blob/main/metadata.plugin.schema.json",
|
|
3
3
|
"id": "mesh",
|
|
4
4
|
"name": "Headscale",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.1",
|
|
6
6
|
"description": "Control-server mesh administration, Tailscale SSH, and peer node discovery.",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
"reuse": {
|
|
9
|
+
"commands": [
|
|
10
|
+
"total-recall auth secret",
|
|
11
|
+
"total-recall deploy status"
|
|
12
|
+
],
|
|
13
|
+
"owned": "Control-server mesh administration, Headscale server provisioning/updates, Tailscale SSH, and peer node discovery."
|
|
14
|
+
},
|
|
14
15
|
"branding": {
|
|
15
16
|
"name": "Headscale",
|
|
16
17
|
"color": "#818cf8",
|
|
@@ -59,11 +60,25 @@
|
|
|
59
60
|
}
|
|
60
61
|
],
|
|
61
62
|
"gate": {
|
|
62
|
-
"tasks": [
|
|
63
|
+
"tasks": [
|
|
64
|
+
"Run a command on another machine",
|
|
65
|
+
"Copy a file to another machine",
|
|
66
|
+
"Check or enroll machines on the mesh",
|
|
67
|
+
"Mesh access, policy and the control server"
|
|
68
|
+
],
|
|
63
69
|
"owned_commands": [
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
{
|
|
71
|
+
"pattern": "ssh\\s",
|
|
72
|
+
"use": "total-recall mesh exec <node> \"<command>\""
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"pattern": "scp\\s",
|
|
76
|
+
"use": "total-recall mesh file put <node> <source> <destination>"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"pattern": "tailscale\\s+(status|ping)\\b",
|
|
80
|
+
"use": "total-recall mesh status"
|
|
81
|
+
}
|
|
67
82
|
]
|
|
68
83
|
},
|
|
69
84
|
"cli": {
|
|
@@ -181,8 +196,19 @@
|
|
|
181
196
|
"effect": "write",
|
|
182
197
|
"output": "json",
|
|
183
198
|
"args": [
|
|
184
|
-
{
|
|
185
|
-
|
|
199
|
+
{
|
|
200
|
+
"name": "ip",
|
|
201
|
+
"type": "string",
|
|
202
|
+
"label": "Mesh IPv4 address",
|
|
203
|
+
"positional": true,
|
|
204
|
+
"required": true
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "port",
|
|
208
|
+
"flag": "--port",
|
|
209
|
+
"type": "string",
|
|
210
|
+
"label": "Leader port"
|
|
211
|
+
}
|
|
186
212
|
]
|
|
187
213
|
},
|
|
188
214
|
{
|
|
@@ -544,5 +570,12 @@
|
|
|
544
570
|
"parseChecksum",
|
|
545
571
|
"consoleConfig"
|
|
546
572
|
]
|
|
547
|
-
}
|
|
573
|
+
},
|
|
574
|
+
"skills": [
|
|
575
|
+
{
|
|
576
|
+
"id": "mesh",
|
|
577
|
+
"path": "./skills/mesh/SKILL.md",
|
|
578
|
+
"description": "Control-server mesh administration, Tailscale SSH, and peer node discovery."
|
|
579
|
+
}
|
|
580
|
+
]
|
|
548
581
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mesh
|
|
3
|
+
description: "Control-server mesh administration, Tailscale SSH, and peer node discovery."
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
repo_scoped: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Headscale
|
|
9
|
+
|
|
10
|
+
Control-server mesh administration, Tailscale SSH, and peer node discovery.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
Use the `mesh` Total Recall plugin (`total-recall mesh`) instead of ad-hoc scripts or raw commands whenever working on headscale tasks.
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
total-recall mesh --help
|
|
20
|
+
total-recall mesh file # Transfer a file through authenticated mesh access and verify its destination hash
|
|
21
|
+
total-recall mesh status # Inspect live mesh state
|
|
22
|
+
total-recall mesh view # Read independent owner observations for mesh screens
|
|
23
|
+
total-recall mesh nodes # Mesh nodes operation
|
|
24
|
+
total-recall mesh doctor # Mesh doctor operation
|
|
25
|
+
total-recall mesh ssh # Mesh ssh operation
|
|
26
|
+
total-recall mesh exec # Mesh exec operation
|
|
27
|
+
total-recall mesh access # Mesh access operation
|
|
28
|
+
total-recall mesh ping # Mesh ping operation
|
|
29
|
+
total-recall mesh leader # Read the elected leader and its server port
|
|
30
|
+
total-recall mesh leader-pin # Change the pinned leader address and optional server port
|
|
31
|
+
total-recall mesh enroll # Mesh enroll operation
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Owned Raw Commands
|
|
35
|
+
|
|
36
|
+
This plugin owns: `ssh\s`, `scp\s`, `tailscale\s+(status|ping)\b`.
|
|
@@ -134,6 +134,7 @@ async function runOperation(argv) {
|
|
|
134
134
|
...Object.fromEntries([['goal', '--goal'], ['maxTurns', '--max-turns'], ['maxBudgetUsd', '--max-budget-usd'], ['maxTokens', '--max-tokens'],
|
|
135
135
|
['resultSchema', '--result-schema'], ['approvalPolicy', '--approvals'], ['questionTimeoutMs', '--question-timeout-ms']]
|
|
136
136
|
.filter(([, flag]) => value(flag) !== undefined).map(([key, flag]) => [key, ['maxTurns', 'maxBudgetUsd', 'maxTokens', 'questionTimeoutMs'].includes(key) ? Number(value(flag)) : value(flag)])) });
|
|
137
|
+
const modelOption = () => (value('--model') === undefined ? {} : { model: value('--model') });
|
|
137
138
|
let result;
|
|
138
139
|
if (operation === 'status') result = {...await inspectNode(value('--node') || 'local'),documentation:documentationStatus()};
|
|
139
140
|
else if (operation === 'catalog') result = { adapters: getActiveAdapters(), permissions: ['read', 'write', 'send', 'destructive'] };
|
|
@@ -176,7 +177,7 @@ async function runOperation(argv) {
|
|
|
176
177
|
else if (operation === 'route') {
|
|
177
178
|
const task = positional.join(' ');
|
|
178
179
|
if (!task) throw new Error('route requires a task description');
|
|
179
|
-
const options = { ...controls(), jev: has('--jev'), preferLocal: has('--prefer-local'), preferFrontier: has('--prefer-frontier'), node: value('--node'), cwd: value('--cwd'), allowUnknownAuth: has('--allow-unknown-auth'), ...remoteTimeout() };
|
|
180
|
+
const options = { ...controls(), jev: has('--jev'), preferLocal: has('--prefer-local'), preferFrontier: has('--prefer-frontier'), node: value('--node'), cwd: value('--cwd'), allowUnknownAuth: has('--allow-unknown-auth'), ...modelOption(), ...remoteTimeout() };
|
|
180
181
|
result = has('--execute') ? await executeRoutedTask(task, { ...options, gate: has('--gate'), gateOptions: { tier: value('--tier') || 'fast' } }) : await routeTask(task, options);
|
|
181
182
|
if (!result.ok) process.exitCode = 1;
|
|
182
183
|
}
|
|
@@ -192,7 +193,7 @@ async function runOperation(argv) {
|
|
|
192
193
|
result = await coordinateTask(task, {
|
|
193
194
|
...controls(), mode: value('--mode') || 'pipeline', execute: has('--execute'), orchestrator: value('--orchestrator'), explorer: value('--explorer'), architect: value('--architect'), implementer: value('--implementer'), verifier: value('--verifier'),
|
|
194
195
|
branch: value('--branch'), subagents: !has('--no-subagents'), autoReview: has('--auto-review'), gate: has('--gate'), gateOptions: { tier: value('--tier') || 'fast' }, allowSameFamily: has('--allow-same-family'), reviewer: value('--reviewer'),
|
|
195
|
-
node: value('--node') || 'local', cwd: value('--cwd'), allowUnknownAuth: has('--allow-unknown-auth'), ...remoteTimeout(),
|
|
196
|
+
node: value('--node') || 'local', cwd: value('--cwd'), allowUnknownAuth: has('--allow-unknown-auth'), ...modelOption(), ...remoteTimeout(),
|
|
196
197
|
});
|
|
197
198
|
if (!result.ok) process.exitCode = 1;
|
|
198
199
|
}
|
|
@@ -399,7 +400,9 @@ async function runOperation(argv) {
|
|
|
399
400
|
else if(operation==='dispatch') {
|
|
400
401
|
if(!args.includes('--stdin'))throw new Error('Task requests must arrive through stdin: pass --stdin with JSON {agent, cwd, prompt}');
|
|
401
402
|
const input=fs.readFileSync(0,'utf8');if(Buffer.byteLength(input)>65536)throw new Error('Task request too large');
|
|
402
|
-
const
|
|
403
|
+
const body=JSON.parse(input);
|
|
404
|
+
if(value('--model')!==undefined && body?.model!==undefined && body.model!==value('--model'))throw new Error('--model and the request model differ; give the model once');
|
|
405
|
+
const request=validateDispatchRequest({...body,...controls(),...modelOption()});
|
|
403
406
|
// A-107: a local dispatch streams its agent's events to a live log, announced before it starts.
|
|
404
407
|
const progressFile=(request.node||'local')==='local'?path.join(configDir(),'jobs',`${process.env.TR_METAHARNESS_JOB || `dispatch-${Date.now().toString(36)}-${request.agent}`}.events.jsonl`):undefined;
|
|
405
408
|
if(progressFile)console.error(JSON.stringify({progress:progressFile,agent:request.agent}));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "meta-harness",
|
|
3
3
|
"name": "Metaharness",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "Discover, configure, document, track, and run coding agent harnesses through Total Recall across mesh nodes.",
|
|
6
6
|
"gate": {
|
|
7
7
|
"tasks": ["Run work in another agent harness", "Choose which harness takes a task", "Check harness versions, sign-in and quota", "Read a harness's official documentation"],
|
|
@@ -167,7 +167,8 @@
|
|
|
167
167
|
{ "name": "maxTokens", "flag": "--max-tokens", "type": "number", "label": "Job token limit" },
|
|
168
168
|
{ "name": "resultSchema", "flag": "--result-schema", "type": "string", "label": "JSON Schema file" },
|
|
169
169
|
{ "name": "approvals", "flag": "--approvals", "type": "string", "label": "auto, operator, or rules" },
|
|
170
|
-
{ "name": "questionTimeoutMs", "flag": "--question-timeout-ms", "type": "number", "label": "Question timeout in milliseconds" }
|
|
170
|
+
{ "name": "questionTimeoutMs", "flag": "--question-timeout-ms", "type": "number", "label": "Question timeout in milliseconds" },
|
|
171
|
+
{ "name": "model", "flag": "--model", "type": "string", "label": "Model for this run, validated before launch (agy never runs Opus)" }
|
|
171
172
|
],
|
|
172
173
|
"timeout_ms": 600000
|
|
173
174
|
},
|
|
@@ -570,6 +571,7 @@
|
|
|
570
571
|
{ "name": "node", "flag": "--node", "type": "string", "label": "Selected mesh node" },
|
|
571
572
|
{ "name": "allowUnknownAuth", "flag": "--allow-unknown-auth", "type": "boolean", "label": "Explicitly allow a runner without a login probe" },
|
|
572
573
|
{ "name": "background", "flag": "--background", "type": "boolean", "label": "Return the owned job and log for a long run" },
|
|
574
|
+
{ "name": "model", "flag": "--model", "type": "string", "label": "Model to run; only harnesses that can run it are chosen (agy never runs Opus)" },
|
|
573
575
|
{ "name": "timeoutMs", "flag": "--timeout-ms", "type": "number", "label": "Remote wall-clock limit in milliseconds" }
|
|
574
576
|
]
|
|
575
577
|
},
|
|
@@ -627,6 +629,7 @@
|
|
|
627
629
|
{ "name": "node", "flag": "--node", "type": "string", "label": "Selected mesh node" },
|
|
628
630
|
{ "name": "allowUnknownAuth", "flag": "--allow-unknown-auth", "type": "boolean", "label": "Explicitly allow a runner without a login probe" },
|
|
629
631
|
{ "name": "background", "flag": "--background", "type": "boolean", "label": "Return the owned job and log for a long run" },
|
|
632
|
+
{ "name": "model", "flag": "--model", "type": "string", "label": "Model for the Implementer, or every council and race seat (agy never runs Opus)" },
|
|
630
633
|
{ "name": "timeoutMs", "flag": "--timeout-ms", "type": "number", "label": "Remote wall-clock limit in milliseconds" }
|
|
631
634
|
],
|
|
632
635
|
"timeout_ms": 600000
|
|
@@ -45,7 +45,8 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
45
45
|
if (remote) return remote;
|
|
46
46
|
if (options.node) localExecutionNode(options);
|
|
47
47
|
const cwd = options.cwd || process.cwd();
|
|
48
|
-
|
|
48
|
+
// The requested model travels with the controls, so every attempt and a queued replay run it.
|
|
49
|
+
const controls = { ...sessionOptions({ ...options, cwd }), ...(options.model !== undefined ? { model: options.model } : {}) };
|
|
49
50
|
if (!isGitRepo(cwd)) throw new Error('Routed execution writes on its own branch, so it must run inside a git repository');
|
|
50
51
|
const route = options.route || await routeTask(task, options);
|
|
51
52
|
const replay = { kind: 'route', gate: options.gate === true, gateOptions: options.gateOptions, controls };
|
|
@@ -160,7 +161,8 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
160
161
|
}
|
|
161
162
|
if (stopped) return { ...stopped, route, attempts, alerts };
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
// Escalation stays with harnesses that can run the requested model.
|
|
165
|
+
const remaining = pool.filter(item => !tried.has(item) && (!route.modelSelection || route.modelSelection.compatible.includes(item)));
|
|
164
166
|
const next = remaining.length ? evaluateGateCascade(harness, { testsPassed: false }, remaining) : null;
|
|
165
167
|
diagnostic = attempt.gate?.summary || attempt.error || 'The previous attempt failed.';
|
|
166
168
|
previous = harness;
|
|
@@ -18,6 +18,7 @@ import { runQualityGate } from './gate.mjs';
|
|
|
18
18
|
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
19
19
|
import { cancellationResult } from './worker.mjs';
|
|
20
20
|
import { sessionOptions } from './session/run.mjs';
|
|
21
|
+
import { createModelValidator, filterPoolByModel } from './model-selection.mjs';
|
|
21
22
|
|
|
22
23
|
const OPEN_THREADS = new Set();
|
|
23
24
|
const MAX_BLUEPRINT = 20000;
|
|
@@ -42,7 +43,12 @@ export function assignPipelineRoles(pool = [], options = {}) {
|
|
|
42
43
|
const byCost = [...pool].sort((a, b) => a.costScore - b.costScore);
|
|
43
44
|
const explorer = options.explorer ? findById(options.explorer) : byCost.find(h => h.id !== orchestrator.id) || orchestrator;
|
|
44
45
|
const architect = options.architect ? findById(options.architect) : orchestrator;
|
|
45
|
-
|
|
46
|
+
// A requested model is the Implementer's: only a harness that can run it writes the code.
|
|
47
|
+
const compatible = options.modelCompatible;
|
|
48
|
+
const writers = compatible ? sorted.filter(h => compatible.includes(h.id)) : sorted;
|
|
49
|
+
const implementer = options.implementer ? findById(options.implementer) : writers.find(h => h.id !== explorer.id && h.id !== architect.id) || writers.find(h => h.id !== architect.id) || (compatible ? writers[0] || null : architect);
|
|
50
|
+
if (compatible && implementer && !compatible.includes(implementer.id)) throw new Error(`Selected implementer ${implementer.id} cannot run model ${options.model}`);
|
|
51
|
+
if (!implementer) return { orchestrator, explorer, architect, implementer: null, verifier: null, heterogeneousFamilies: false };
|
|
46
52
|
const verifier = options.verifier ? findById(options.verifier) : sorted.find(h => h.family !== implementer.family) || null;
|
|
47
53
|
return { orchestrator, explorer, architect, implementer, verifier, heterogeneousFamilies: Boolean(verifier) && verifier.family !== implementer.family };
|
|
48
54
|
}
|
|
@@ -81,15 +87,18 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
81
87
|
const execute = options.execute === true;
|
|
82
88
|
const withSubagents = options.subagents !== false;
|
|
83
89
|
const stages = roles.orchestrator ? buildStages(roles, withSubagents) : [];
|
|
84
|
-
|
|
90
|
+
if (options.model !== undefined) for (const item of stages) if (item.role === 'Implementer') item.model = options.model;
|
|
91
|
+
const base = { mode: 'pipeline', task, ...modelFields(options), orchestrator: roles.orchestrator?.id || null, executedLive: execute, excluded: poolInfo.excluded, heterogeneousFamiliesVerified: roles.heterogeneousFamilies, stages };
|
|
85
92
|
|
|
86
93
|
if (!execute) {
|
|
87
94
|
const blockers = [];
|
|
88
95
|
if (!roles.orchestrator) blockers.push('No harness can take work right now.');
|
|
96
|
+
else if (!roles.implementer) blockers.push(`No harness that can take work can run model ${options.model}.`);
|
|
89
97
|
else if (!roles.heterogeneousFamilies) blockers.push('Only one model family is available, so no independent Verifier exists.');
|
|
90
98
|
return { ...base, ok: blockers.length === 0, threadId: null, blockers };
|
|
91
99
|
}
|
|
92
100
|
if (!roles.orchestrator) throw new Error(`No harness can take work right now: ${poolInfo.excluded.map(e => `${e.harness} (${e.reason})`).join(', ') || 'none installed'}`);
|
|
101
|
+
if (!roles.implementer) throw new Error(`No harness that can take work can run model ${options.model}`);
|
|
93
102
|
if (!roles.heterogeneousFamilies && options.allowSameFamily !== true) throw new Error('Refusing to run: only one model family is available, so no independent Verifier exists. Pass --allow-same-family to accept self-review.');
|
|
94
103
|
if (!isGitRepo(cwd)) throw new Error('A pipeline writes on its own branch, so it must run inside a git repository');
|
|
95
104
|
|
|
@@ -105,6 +114,7 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
105
114
|
stage.status = 'running';
|
|
106
115
|
const result = await dispatchWithReceipt({ agent: harness.id, node: 'local', cwd: workdir, contextCwd: cwd, contextTask: task, prompt,
|
|
107
116
|
...sessionOptions({ ...options, cwd }), goal: stage.role === 'Implementer' ? options.goal || task : prompt.slice(0, 4000),
|
|
117
|
+
...(stage.role === 'Implementer' && options.model !== undefined ? { model: options.model } : {}),
|
|
108
118
|
permissions: permissions || stage.permissions, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role, complexityScore: complexity.score });
|
|
109
119
|
stage.status = result.cancelled ? 'cancelled' : result.ok ? 'completed' : result.queued ? 'queued' : 'failed';
|
|
110
120
|
stage.output = result.response || result.error || null;
|
|
@@ -193,11 +203,11 @@ async function runCouncil(task, poolInfo, options) {
|
|
|
193
203
|
// One seat per model family, strongest first, so the seats can actually disagree.
|
|
194
204
|
const families = new Set();
|
|
195
205
|
const seats = options.harnesses || [...poolInfo.ready].sort((a, b) => b.qualityScore - a.qualityScore).filter(h => !families.has(h.family) && families.add(h.family)).slice(0, 3).map(h => h.id);
|
|
196
|
-
if (options.execute !== true) return { ok: seats.length > 0, mode: 'council', task, executedLive: false, consensus: 'planned', excluded: poolInfo.excluded, votes: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
206
|
+
if (options.execute !== true) return { ok: seats.length > 0, mode: 'council', task, ...modelFields(options), executedLive: false, consensus: 'planned', excluded: poolInfo.excluded, votes: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
197
207
|
if (!seats.length) throw new Error('No harness can take work right now');
|
|
198
208
|
const cwd = options.cwd || process.cwd();
|
|
199
209
|
const votes = await Promise.all(seats.map(async harness => {
|
|
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' });
|
|
210
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), ...(options.model !== undefined ? { model: options.model } : {}), 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
211
|
return { harness, ok: result.ok === true, failureKind: result.failureKind, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }),
|
|
202
212
|
...(result.queueId ? { queueId: result.queueId } : {}), feedback: result.response || null };
|
|
203
213
|
}));
|
|
@@ -205,12 +215,12 @@ async function runCouncil(task, poolInfo, options) {
|
|
|
205
215
|
const majority = votes.length / 2;
|
|
206
216
|
const consensus = count('APPROVE') > majority ? 'approved' : count('REJECT') > majority ? 'rejected' : 'no-consensus';
|
|
207
217
|
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 };
|
|
218
|
+
return { ok: !limited && votes.some(item => item.ok), ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'council', task, ...modelFields(options), executedLive: true, consensus, excluded: poolInfo.excluded, votes };
|
|
209
219
|
}
|
|
210
220
|
|
|
211
221
|
async function runRace(task, poolInfo, options) {
|
|
212
222
|
const seats = options.candidates || [...poolInfo.ready].sort((a, b) => b.qualityScore - a.qualityScore).slice(0, 2).map(h => h.id);
|
|
213
|
-
if (options.execute !== true) return { ok: seats.length > 1, mode: 'race', task, executedLive: false, excluded: poolInfo.excluded, candidates: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
223
|
+
if (options.execute !== true) return { ok: seats.length > 1, mode: 'race', task, ...modelFields(options), executedLive: false, excluded: poolInfo.excluded, candidates: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
214
224
|
if (seats.length < 2) throw new Error('A race needs at least two harnesses that can take work');
|
|
215
225
|
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.' };
|
|
216
226
|
const cwd = options.cwd || process.cwd();
|
|
@@ -223,7 +233,7 @@ async function runRace(task, poolInfo, options) {
|
|
|
223
233
|
try {
|
|
224
234
|
const started = Date.now();
|
|
225
235
|
directory = createWorktree(cwd, { branch });
|
|
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' });
|
|
236
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd: directory, contextCwd: cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), goal: options.goal || task, ...(options.model !== undefined ? { model: options.model } : {}), 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
237
|
const commit = result.ok || result.cancelled ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
|
|
228
238
|
if (result.cancelled) {
|
|
229
239
|
keep = Boolean(commit);
|
|
@@ -247,9 +257,11 @@ async function runRace(task, poolInfo, options) {
|
|
|
247
257
|
}));
|
|
248
258
|
const finishers = entries.filter(entry => entry.ok).sort((a, b) => a.completedAt - b.completedAt);
|
|
249
259
|
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 };
|
|
260
|
+
return { ok: !limited && finishers.length > 0, ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'race', task, ...modelFields(options), executedLive: true, gateApplied: true, verification: !limited && finishers.length ? 'passed' : 'failed', winner: limited ? null : finishers[0]?.harness || null, excluded: poolInfo.excluded, candidates: entries };
|
|
251
261
|
}
|
|
252
262
|
|
|
263
|
+
const modelFields = options => options.model !== undefined ? { model: options.model, modelSelection: options.modelSelection } : {};
|
|
264
|
+
|
|
253
265
|
export async function coordinateTask(taskDescription, options = {}) {
|
|
254
266
|
if (cancellationResult()) return cancellationResult();
|
|
255
267
|
const task = String(taskDescription || '').trim();
|
|
@@ -259,7 +271,14 @@ export async function coordinateTask(taskDescription, options = {}) {
|
|
|
259
271
|
const mode = options.mode || 'pipeline';
|
|
260
272
|
if (!['pipeline', 'orchestrate', 'council', 'race'].includes(mode)) throw new Error(`Unsupported coordination mode: ${mode}`);
|
|
261
273
|
localExecutionNode(options);
|
|
262
|
-
|
|
274
|
+
let poolInfo = await resolvePool(options);
|
|
275
|
+
if (options.model !== undefined) {
|
|
276
|
+
// Council and race seats all run the model, so their pool narrows. A pipeline keeps every harness
|
|
277
|
+
// for its other roles and gives the Implementer role to one that can run the model.
|
|
278
|
+
const filtered = filterPoolByModel(poolInfo, options.model, options.modelValidator || createModelValidator());
|
|
279
|
+
options = { ...options, modelSelection: filtered.selection, modelCompatible: filtered.selection.compatible };
|
|
280
|
+
if (mode === 'council' || mode === 'race') poolInfo = filtered.pool;
|
|
281
|
+
}
|
|
263
282
|
const wall = poolInfo.excluded.find(item => item.failureKind === 'quota');
|
|
264
283
|
const minimum = mode === 'race' || ((mode === 'pipeline' || mode === 'orchestrate') && options.allowSameFamily !== true) ? 2 : 1;
|
|
265
284
|
if (options.execute === true && wall && poolInfo.ready.length < minimum) {
|
|
@@ -6,11 +6,17 @@ import { isGitRepo, createWorktree, removeWorktree, commitAll, deleteBranch } fr
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { includedAdmission, openRouterAdmission, readIncludedPolicy } from './included-usage.mjs';
|
|
8
8
|
import { queueIncludedJob } from './usage-queue.mjs';
|
|
9
|
+
import { createModelValidator } from './model-selection.mjs';
|
|
9
10
|
|
|
10
11
|
export async function dispatchWithReceipt(request, { dispatchFn, node = request.node || 'local', role, complexityScore, escalatedFrom,
|
|
11
|
-
record = recordRunReceipt, replay, retryId, manageWrites = false, usagePolicy = readIncludedPolicy, admit = includedAdmission, admitOpenRouter = openRouterAdmission } = {}) {
|
|
12
|
+
record = recordRunReceipt, replay, retryId, manageWrites = false, validateModel, usagePolicy = readIncludedPolicy, admit = includedAdmission, admitOpenRouter = openRouterAdmission } = {}) {
|
|
12
13
|
const started = Date.now();
|
|
13
14
|
if (cancellationResult()) return cancellationResult();
|
|
15
|
+
// A model this harness must not or cannot run is refused before launch; it says nothing about the harness, so no receipt.
|
|
16
|
+
if (request.model !== undefined) {
|
|
17
|
+
const check = (validateModel || createModelValidator({ local: node === 'local' }))(request.agent, request.model);
|
|
18
|
+
if (!check.ok) return { ok: false, agent: request.agent, failureKind: 'model', error: check.reason, modelCheck: check, launchPrevented: true, exitCode: null, durationMs: Date.now() - started };
|
|
19
|
+
}
|
|
14
20
|
// A harness that cannot be told not to write does read-only work in a checkout that is thrown away.
|
|
15
21
|
const contain = node === 'local' && request.permissions?.write !== true && !READ_ONLY_ADAPTERS.includes(request.agent) && request.isolated !== true && isGitRepo(request.cwd);
|
|
16
22
|
const ownWrites = manageWrites && node === 'local' && request.permissions?.write === true && isGitRepo(request.cwd);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which harness may run a requested model. Shape, operator policy and harness support come first
|
|
3
|
+
* (worker.mjs, so a remote destination enforces them too). Then the AI Models plugin, which owns the
|
|
4
|
+
* shared model catalog, decides when it answers `ai-models validate`. When it cannot answer, this
|
|
5
|
+
* machine's synced harness model lists decide; a harness without a list is accepted as unverified.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { modelRefusal } from './worker.mjs';
|
|
9
|
+
import { getModelCatalog, normalizeModelName } from './model-catalog.mjs';
|
|
10
|
+
|
|
11
|
+
/** Ask the AI Models owner. Null means it gave no verdict (not installed, not implemented, or unreadable). */
|
|
12
|
+
export function queryAiModels(harness, model, { bin = process.env.TOTAL_RECALL_BIN || 'total-recall' } = {}) {
|
|
13
|
+
let output;
|
|
14
|
+
try {
|
|
15
|
+
output = execFileSync(bin, ['ai-models', 'validate', '--harness', harness, '--model', model, '--json'], { encoding: 'utf8', timeout: 20000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
// A refusal may exit nonzero with its verdict on stdout.
|
|
18
|
+
output = String(error.stdout || '');
|
|
19
|
+
}
|
|
20
|
+
const line = output.trim().split('\n').filter(Boolean).at(-1);
|
|
21
|
+
try {
|
|
22
|
+
const answer = JSON.parse(line);
|
|
23
|
+
return answer && typeof answer.ok === 'boolean' ? answer : null;
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One validator per decision: after the owner fails to answer once, it is not asked again. */
|
|
28
|
+
export function createModelValidator({ aiModels = queryAiModels, catalog = getModelCatalog, local = true } = {}) {
|
|
29
|
+
let ownerAnswers = true, listed;
|
|
30
|
+
return (harness, model) => {
|
|
31
|
+
const refusal = modelRefusal(harness, model);
|
|
32
|
+
if (refusal) return { ok: false, harness, model, source: 'policy', reason: refusal };
|
|
33
|
+
if (ownerAnswers) {
|
|
34
|
+
const answer = aiModels(harness, model);
|
|
35
|
+
if (answer) return { ok: answer.ok, harness, model, source: 'ai-models', ...(answer.ok ? {} : { reason: answer.reason || answer.error || `AI Models does not list ${model} for ${harness}` }) };
|
|
36
|
+
ownerAnswers = false;
|
|
37
|
+
}
|
|
38
|
+
// Another node's catalog is not this one's: its destination validates against its own.
|
|
39
|
+
if (!local) return { ok: true, harness, model, source: 'unverified', note: 'The destination node validates the model before launch.' };
|
|
40
|
+
listed ??= catalog().harnessModels || {};
|
|
41
|
+
const models = listed[harness];
|
|
42
|
+
if (Array.isArray(models) && models.length) {
|
|
43
|
+
const key = normalizeModelName(model);
|
|
44
|
+
if (models.some(row => row.id === model || normalizeModelName(row.id) === key)) return { ok: true, harness, model, source: 'catalog' };
|
|
45
|
+
return { ok: false, harness, model, source: 'catalog', reason: `${harness} does not list ${model}. Listed: ${models.slice(0, 12).map(row => row.id).join(', ')}` };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, harness, model, source: 'unverified', note: `No model list for ${harness}; only a completed run proves ${model} works there.` };
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Split a pool by whether each harness can run the model. */
|
|
52
|
+
export function filterPoolByModel(pool, model, validate) {
|
|
53
|
+
const checks = pool.ready.map(harness => validate(harness.id, model));
|
|
54
|
+
return {
|
|
55
|
+
pool: { ...pool, ready: pool.ready.filter((_, index) => checks[index].ok),
|
|
56
|
+
excluded: [...pool.excluded, ...checks.filter(check => !check.ok).map(check => ({ harness: check.harness, reason: check.reason, failureKind: 'model' }))] },
|
|
57
|
+
selection: { model, compatible: checks.filter(check => check.ok).map(check => check.harness), checks },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -10,11 +10,11 @@ const BOOLEAN_FLAGS = {
|
|
|
10
10
|
const VALUE_FLAGS = {
|
|
11
11
|
mode: '--mode', orchestrator: '--orchestrator', explorer: '--explorer', architect: '--architect', implementer: '--implementer',
|
|
12
12
|
goal: '--goal', maxTurns: '--max-turns', maxBudgetUsd: '--max-budget-usd', maxTokens: '--max-tokens', approvalPolicy: '--approvals', questionTimeoutMs: '--question-timeout-ms', resultSchema: '--result-schema',
|
|
13
|
-
verifier: '--verifier', reviewer: '--reviewer', author: '--author', threadId: '--thread', branch: '--branch',
|
|
13
|
+
verifier: '--verifier', reviewer: '--reviewer', author: '--author', threadId: '--thread', branch: '--branch', model: '--model',
|
|
14
14
|
};
|
|
15
15
|
const OPERATION_KEYS = {
|
|
16
|
-
route: ['execute', 'gate', 'jev', 'preferLocal', 'preferFrontier', 'allowUnknownAuth'],
|
|
17
|
-
coordinate: ['execute', 'gate', 'autoReview', 'allowSameFamily', 'allowUnknownAuth', 'mode', 'orchestrator', 'explorer', 'architect', 'implementer', 'verifier', 'reviewer', 'branch'],
|
|
16
|
+
route: ['execute', 'gate', 'jev', 'preferLocal', 'preferFrontier', 'allowUnknownAuth', 'model'],
|
|
17
|
+
coordinate: ['execute', 'gate', 'autoReview', 'allowSameFamily', 'allowUnknownAuth', 'mode', 'orchestrator', 'explorer', 'architect', 'implementer', 'verifier', 'reviewer', 'branch', 'model'],
|
|
18
18
|
review: ['staged', 'allowUnknownAuth', 'reviewer', 'author', 'threadId'],
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -23,6 +23,7 @@ import { resolvePool, exclusionReason, isLocalExecutionNode } from './pool.mjs';
|
|
|
23
23
|
import { readCodexRateLimits, measuredWalls } from './telemetry.mjs';
|
|
24
24
|
import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy } from './included-usage.mjs';
|
|
25
25
|
import { derivedDir } from './paths.mjs';
|
|
26
|
+
import { createModelValidator, filterPoolByModel } from './model-selection.mjs';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Helper to safely query host Total Recall CLI without assumptions
|
|
@@ -273,7 +274,10 @@ export async function routeTask(taskDescription, options = {}) {
|
|
|
273
274
|
// fleet telemetry places local work, and the balance matters only to a metered harness.
|
|
274
275
|
const shared = scoped.inspect ? null : inspectAgents({ env: process.env });
|
|
275
276
|
const lean = shared ? { ...scoped, inspect: () => shared } : scoped;
|
|
276
|
-
const [
|
|
277
|
+
const [resolved, receipts] = await Promise.all([resolvePool({ ...lean, policy }), readReceipts()]);
|
|
278
|
+
// A requested model narrows the pool to the harnesses that can run it, and says why the others cannot.
|
|
279
|
+
const { pool, selection: modelSelection } = options.model === undefined ? { pool: resolved, selection: null }
|
|
280
|
+
: filterPoolByModel(resolved, options.model, options.modelValidator || createModelValidator({ local: !remote }));
|
|
277
281
|
const quotaReport = await inspectQuotas({
|
|
278
282
|
...lean,
|
|
279
283
|
policy,
|
|
@@ -289,10 +293,12 @@ export async function routeTask(taskDescription, options = {}) {
|
|
|
289
293
|
if (typeof selectedQuota.usagePercent === 'number' && selectedQuota.usagePercent > 85) warnings.push(`${routing.selected} is at ${selectedQuota.usagePercent}% measured usage.`);
|
|
290
294
|
if (selectedQuota.status === 'unverified') warnings.push(`${routing.selected} has no login probe; only a completed run proves it works.`);
|
|
291
295
|
if (selectedQuota.billingModel === 'metered') warnings.push(`${routing.selected} bills per token.`);
|
|
292
|
-
if (!routing.selected) warnings.push('No harness can take this task right now.');
|
|
296
|
+
if (!routing.selected) warnings.push(modelSelection ? `No harness that can run ${options.model} can take this task right now.` : 'No harness can take this task right now.');
|
|
297
|
+
const selectedModel = modelSelection?.checks.find(check => check.harness === routing.selected);
|
|
298
|
+
if (selectedModel?.source === 'unverified') warnings.push(selectedModel.note);
|
|
293
299
|
return {
|
|
294
300
|
ok: Boolean(routing.selected), task: taskDescription, classifier, complexity,
|
|
295
|
-
selected: routing.selected, tier: complexity.tier, candidates: routing.candidates, excluded: pool.excluded,
|
|
301
|
+
selected: routing.selected, tier: complexity.tier, model: options.model ?? null, ...(modelSelection ? { modelSelection } : {}), candidates: routing.candidates, excluded: pool.excluded,
|
|
296
302
|
...(policy.enabled && !routing.selected ? { deferred: pool.excluded.find(row => row.failureKind === 'quota') || null } : {}),
|
|
297
303
|
placement: complexity.tier === 'local' ? placeLocalWork(quotaReport.fleet) : null,
|
|
298
304
|
scoring: 'Adapter quality and cost scores are configured priors, adjusted by pass rates measured on this machine.',
|
|
@@ -73,6 +73,43 @@ export const ADAPTERS = [
|
|
|
73
73
|
{ id: 'hermes', name: 'Hermes', binary: 'hermes', args: ['chat', '--oneshot', '--format', 'stream-json', '--query-file', '-'], input: 'stdin', docs: 'https://github.com/NousResearch/hermes-agent/blob/main/website/docs/reference/cli-commands.md' }
|
|
74
74
|
];
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* How each harness takes a model for one run, from its official CLI reference (read 2026-11-14 through
|
|
78
|
+
* `meta-harness docs-search`). A flag goes after the subcommand, before the flag that carries the prompt.
|
|
79
|
+
* DSH has no per-run model flag: its headless profile owns the model (`total-recall dsh model`).
|
|
80
|
+
*/
|
|
81
|
+
export const MODEL_SELECTION = Object.freeze({
|
|
82
|
+
claude: { flag: '--model' }, codex: { flag: '--model' }, agy: { flag: '--model' }, cursor: { flag: '--model' },
|
|
83
|
+
grok: { flag: '--model' }, muse: { flag: '--model' }, pi: { flag: '--model' }, hermes: { flag: '--model' },
|
|
84
|
+
openclaw: { flag: '--model' }, aider: { flag: '--model' }, cline: { flag: '--model' },
|
|
85
|
+
goose: { env: 'GOOSE_MODEL' },
|
|
86
|
+
// OpenHands ignores LLM_MODEL unless the run also passes --override-with-envs; the override is not persisted.
|
|
87
|
+
openhands: { env: 'LLM_MODEL', args: ['--override-with-envs'] },
|
|
88
|
+
ollama: { positional: true },
|
|
89
|
+
});
|
|
90
|
+
// Operator policy: Antigravity never runs an Opus model, although `agy models` lists them.
|
|
91
|
+
const MODEL_POLICY = [{ harness: 'agy', test: model => /opus/i.test(model), reason: 'Antigravity (agy) must not run Opus models. Choose a Gemini or Sonnet model, or route Opus work to claude.' }];
|
|
92
|
+
|
|
93
|
+
/** Why this harness cannot run this model, or null. Shape, operator policy and harness support only; catalogs live elsewhere. */
|
|
94
|
+
export function modelRefusal(harness, model) {
|
|
95
|
+
if (typeof model !== 'string' || !model.trim() || model.length > 200 || /[\0-\x1f\s]/.test(model) || model.startsWith('-')) return 'Invalid requested model';
|
|
96
|
+
const policy = MODEL_POLICY.find(rule => rule.harness === harness && rule.test(model));
|
|
97
|
+
if (policy) return policy.reason;
|
|
98
|
+
if (!Object.hasOwn(MODEL_SELECTION, harness)) {
|
|
99
|
+
return harness === 'dsh' ? 'DSH selects its model through its headless profile; set it with `total-recall dsh model`.' : `${harness} does not declare how it selects a model`;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Put the model where the harness reads it. Returns the environment the launch adds. */
|
|
105
|
+
export function applyModel(harness, model, args) {
|
|
106
|
+
const selection = MODEL_SELECTION[harness];
|
|
107
|
+
const at = args.length && !args[0].startsWith('-') ? 1 : 0;
|
|
108
|
+
if (selection.flag) args.splice(at, 0, selection.flag, model);
|
|
109
|
+
if (selection.args) args.splice(at, 0, ...selection.args);
|
|
110
|
+
return selection.env ? { [selection.env]: model } : {};
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
export const EXTENDED_ADAPTERS = [
|
|
77
114
|
{ id: 'ollama', name: 'Ollama', binary: 'ollama', args: ['run'], input: 'stdin', docs: 'https://ollama.com/library' },
|
|
78
115
|
{ id: 'openclaw', name: 'OpenClaw', binary: 'openclaw', args: ['agent', '--message'], input: 'argument', docs: 'https://openclaw.ai/docs' },
|
|
@@ -535,7 +572,12 @@ export async function totalRecallBootstrap(request, { probe = runProcess, env =
|
|
|
535
572
|
if (docs.ok !== true) throw notReady('Official agent documentation is unavailable or stale');
|
|
536
573
|
// Node built-ins only here (this file ships to remote nodes): the result is the pretty-printed JSON after any log lines.
|
|
537
574
|
let found = null;
|
|
538
|
-
try {
|
|
575
|
+
try {
|
|
576
|
+
const res = await probe(binary, ['help', task, '--json'], { cwd, timeoutMs: 30000, maxBytes: 1024 * 1024, env: agentEnvironment(env) });
|
|
577
|
+
const out = String(res.stdout || '');
|
|
578
|
+
const at = out.indexOf('{\n');
|
|
579
|
+
if (at >= 0 || out.trim().startsWith('{')) found = JSON.parse(at >= 0 ? out.slice(at) : out);
|
|
580
|
+
}
|
|
539
581
|
catch (error) { if (error.code === 'NOT_READY') throw error; found = null; }
|
|
540
582
|
const matches = Array.isArray(found?.matches) ? found.matches.slice(0, 12) : [];
|
|
541
583
|
const references = `${matches.length ? `Commands that match this task (Total Recall semantic command search):\n${matches.map(m => `- ${m.verb} — ${m.description}`).join('\n')}\n` : ''}For anything else, run total-recall help "<what you need>" to find the command, then total-recall <namespace> --help for its arguments. Do not load whole command inventories.`;
|
|
@@ -661,6 +703,7 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
661
703
|
credentials[name] = value;
|
|
662
704
|
}
|
|
663
705
|
const args = [...adapter.args];
|
|
706
|
+
let modelEnv = {};
|
|
664
707
|
const openClawSession = openRouter && adapter.id === 'openclaw' ? randomUUID() : null;
|
|
665
708
|
if (openRouter) {
|
|
666
709
|
if (adapter.id === 'pi') args.push('--model', openRouter.model);
|
|
@@ -668,9 +711,10 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
668
711
|
if (openClawSession) args.splice(1, 0, '--agent', 'main', '--session-id', openClawSession,
|
|
669
712
|
'--model', `openrouter/${openRouter.model}`, '--json');
|
|
670
713
|
} else if (request.model !== undefined) {
|
|
671
|
-
|
|
672
|
-
if (
|
|
673
|
-
|
|
714
|
+
const refusal = modelRefusal(adapter.custom ? `custom adapter ${adapter.id}` : adapter.id, request.model);
|
|
715
|
+
if (refusal) throw notReady(refusal);
|
|
716
|
+
if (adapter.id === 'ollama' && !state.chatModels.includes(request.model)) throw notReady(`Ollama has no chat model ${request.model} on this node`);
|
|
717
|
+
modelEnv = applyModel(adapter.id, request.model, args);
|
|
674
718
|
}
|
|
675
719
|
if (request.tools !== undefined || request.settingSources !== undefined || request.allowedTools !== undefined) {
|
|
676
720
|
if (adapter.id !== 'claude') throw notReady(`${adapter.name} cannot enforce the requested tools/settings restriction`);
|
|
@@ -705,7 +749,7 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
705
749
|
if (adapter.id === 'agy') args[args.indexOf('--mode') + 1] = allowWrites ? 'accept-edits' : 'plan';
|
|
706
750
|
if (adapter.id === 'muse' && !allowWrites) args.push('--disable-write');
|
|
707
751
|
if (adapter.id === 'pi') args[args.indexOf('--tools') + 1] = allowWrites ? 'read,bash,edit,write' : 'read';
|
|
708
|
-
if (adapter.id === 'ollama') args.push(env.TR_OLLAMA_MODEL || state.chatModels[0]);
|
|
752
|
+
if (adapter.id === 'ollama') args.push(request.model ?? (env.TR_OLLAMA_MODEL || state.chatModels[0]));
|
|
709
753
|
const progressFile = request.progressFile || path.join(
|
|
710
754
|
env.TR_SECRETS_BRAIN || path.join(env.AGENT_DIR || path.join(env.HOME || os.homedir(), '.agent'), 'skills/total-recall'),
|
|
711
755
|
'config/meta-harness/jobs', `${request.jobId || process.env.TR_METAHARNESS_JOB || `session-${randomUUID()}`}.events.jsonl`);
|
|
@@ -737,7 +781,7 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
737
781
|
};
|
|
738
782
|
try {
|
|
739
783
|
parsed = await (sessionRunner || runSession)({ request, adapter, binary, args,
|
|
740
|
-
env: { ...agentEnvironment(env), ...credentials, TR_META_HARNESS_RUN: '1' }, prompt, progress: liveProgress, onFrame: captureFrame,
|
|
784
|
+
env: { ...agentEnvironment(env), ...credentials, ...modelEnv, TR_META_HARNESS_RUN: '1' }, prompt, progress: liveProgress, onFrame: captureFrame,
|
|
741
785
|
register: stop => { const id = Symbol('session'); ACTIVE_GROUPS.set(id, stop); return () => ACTIVE_GROUPS.delete(id); } });
|
|
742
786
|
if (cancellationResult()) parsed = { ...parsed, ...cancellationResult() };
|
|
743
787
|
if (frames.length) Object.assign(parsed, toolEvidence(nativeToolResults(adapter.id, frames)));
|
package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: ui-chunk:chunk
|
|
3
|
+
plugin: meta-harness
|
|
4
|
+
id: session-controls
|
|
5
|
+
title: Session controls and reviews
|
|
6
|
+
description: Fork, resume, cancel, reply, approve, interrupt, set goals, manage MCP servers, watch progress, and inspect independent reviews.
|
|
7
|
+
timestamp: "2026-11-14T08:51:00Z"
|
|
8
|
+
intents: [control harness sessions, approve or interrupt run, watch session, inspect independent reviews, manage mcp]
|
|
9
|
+
surfaces: [page]
|
|
10
|
+
operations: [fork, resume, cancel, reply, approve, interrupt, goal, mcp, watch, reviews]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<section>
|
|
14
|
+
<h2>Session controls & reviews</h2>
|
|
15
|
+
<p>Manage active harness runs (fork, resume, reply, approve, interrupt, cancel, goal, watch), configure MCP bindings, and review independent verification runs.</p>
|
|
16
|
+
</section>
|