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.
Files changed (89) hide show
  1. package/bin/total-recall.mjs +5 -0
  2. package/frontend/dist/assets/index-CpsWeV4X.css +1 -0
  3. package/frontend/dist/assets/{index-D1goIPjC.js → index-poecbHSl.js} +100 -100
  4. package/frontend/dist/brand/total-recall-lockup-transparent.png +0 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/frontend/dist/sw.js +1 -1
  7. package/frontend/src/App.tsx +15 -81
  8. package/frontend/src/components/DashboardOverview.tsx +164 -0
  9. package/frontend/src/components/brand/BrandMark.tsx +8 -6
  10. package/frontend/src/components/plugins/PluginWorkspace.tsx +112 -57
  11. package/frontend/src/components/workspace/PluginHome.tsx +3 -3
  12. package/frontend/src/index.css +43 -11
  13. package/package.json +1 -1
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +4 -4
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -2
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +30 -5
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/skills/auth/SKILL.md +35 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +494 -79
  19. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +4 -4
  20. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +3 -2
  21. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +33 -8
  22. package/scaffold/.agent/skills/total-recall/plugins/automations/skills/automations/SKILL.md +29 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/dream-consolidation.md +17 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -2
  26. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +31 -4
  27. package/scaffold/.agent/skills/total-recall/plugins/code-quality/skills/code-quality/SKILL.md +27 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  30. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +17 -3
  31. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  32. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -2
  33. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +24 -4
  34. package/scaffold/.agent/skills/total-recall/plugins/embeddings/skills/embeddings/SKILL.md +25 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
  36. package/scaffold/.agent/skills/total-recall/plugins/git/package.json +3 -2
  37. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +300 -58
  38. package/scaffold/.agent/skills/total-recall/plugins/git/skills/git/SKILL.md +36 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +5 -33
  40. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-fleet-ops.md +16 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-publish.md +16 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
  43. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -2
  44. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +47 -14
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +36 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +6 -3
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +5 -2
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +4 -2
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +28 -9
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +7 -1
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-selection.mjs +59 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +3 -3
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +9 -3
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +50 -6
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md +16 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +175 -17
  58. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  59. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
  60. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +242 -31
  61. package/scaffold/.agent/skills/total-recall/plugins/openwiki/skills/openwiki/SKILL.md +32 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +41 -15
  63. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +29 -22
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +20 -33
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +42 -49
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/markdown-links.mjs +112 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +3 -2
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +219 -65
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +2 -1
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +21 -3
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +51 -2
  72. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  73. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  74. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +22 -5
  75. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +3 -2
  76. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +169 -37
  77. package/scaffold/.agent/skills/total-recall/plugins/ssss/skills/ssss/SKILL.md +30 -0
  78. package/src/cli/context.mjs +6 -2
  79. package/src/cli/plugin/create.mjs +2 -1
  80. package/src/cli/plugin/monetization.mjs +66 -32
  81. package/src/cli/plugin/search.mjs +25 -3
  82. package/src/cli/preflight.mjs +40 -6
  83. package/src/core/context-compactor.mjs +2 -0
  84. package/src/core/gate-mode.mjs +13 -24
  85. package/src/core/plugin-distribution.mjs +35 -1
  86. package/src/core/preflight-gate.mjs +28 -8
  87. package/src/core/preflight-receipt.mjs +11 -0
  88. package/src/server/routes/vault.mjs +1 -0
  89. 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.0",
5
+ "version": "0.3.1",
6
6
  "description": "Control-server mesh administration, Tailscale SSH, and peer node discovery.",
7
7
  "license": "MIT",
8
- "requires": [
9
- {
10
- "id": "auth",
11
- "range": ">=1.2.1"
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": ["Run a command on another machine", "Copy a file to another machine", "Check or enroll machines on the mesh", "Mesh access, policy and the control server"],
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
- { "pattern": "ssh\\s", "use": "total-recall mesh exec <node> \"<command>\"" },
65
- { "pattern": "scp\\s", "use": "total-recall mesh file put <node> <source> <destination>" },
66
- { "pattern": "tailscale\\s+(status|ping)\\b", "use": "total-recall mesh status" }
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
- { "name": "ip", "type": "string", "label": "Mesh IPv4 address", "positional": true, "required": true },
185
- { "name": "port", "flag": "--port", "type": "string", "label": "Leader port" }
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 request=validateDispatchRequest({...JSON.parse(input),...controls()});
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,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-meta-harness",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Total Recall Metaharness plugin",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "meta-harness",
3
3
  "name": "Metaharness",
4
- "version": "0.4.0",
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
- const controls = sessionOptions({ ...options, cwd });
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
- const remaining = pool.filter(item => !tried.has(item));
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
- const implementer = options.implementer ? findById(options.implementer) : sorted.find(h => h.id !== explorer.id && h.id !== architect.id) || sorted.find(h => h.id !== architect.id) || architect;
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
- const base = { mode: 'pipeline', task, orchestrator: roles.orchestrator?.id || null, executedLive: execute, excluded: poolInfo.excluded, heterogeneousFamiliesVerified: roles.heterogeneousFamilies, stages };
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
- const poolInfo = await resolvePool(options);
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 [pool, receipts] = await Promise.all([resolvePool({ ...lean, policy }), readReceipts()]);
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 { const out = await invoke(['help', task, '--json']); const at = out.indexOf('{\n'); found = JSON.parse(at >= 0 ? out.slice(at) : out); }
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
- if (typeof request.model !== 'string' || !request.model.trim() || /[\0\r\n]/.test(request.model)) throw new Error('Invalid requested model');
672
- if (adapter.id === 'ollama') throw new Error('Select the local chat model through its configured runtime');
673
- args.push('--model', request.model);
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)));
@@ -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 &amp; 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>