total-recall-brain 3.47.0 → 3.47.2

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 (73) hide show
  1. package/bin/total-recall.mjs +5 -0
  2. package/package.json +1 -1
  3. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +4 -4
  4. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -2
  5. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +30 -5
  6. package/scaffold/.agent/skills/total-recall/plugins/auth/skills/auth/SKILL.md +35 -0
  7. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +494 -79
  8. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +4 -4
  9. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +3 -2
  10. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +33 -8
  11. package/scaffold/.agent/skills/total-recall/plugins/automations/skills/automations/SKILL.md +29 -0
  12. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/dream-consolidation.md +17 -0
  13. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  14. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -2
  15. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +31 -4
  16. package/scaffold/.agent/skills/total-recall/plugins/code-quality/skills/code-quality/SKILL.md +27 -0
  17. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  18. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  19. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +17 -3
  20. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  21. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -2
  22. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +24 -4
  23. package/scaffold/.agent/skills/total-recall/plugins/embeddings/skills/embeddings/SKILL.md +25 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +4 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/git/package.json +3 -2
  26. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +293 -58
  27. package/scaffold/.agent/skills/total-recall/plugins/git/skills/git/SKILL.md +36 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -2
  30. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +47 -14
  31. package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +36 -0
  32. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +6 -3
  33. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +6 -6
  34. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
  35. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +5 -2
  36. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +4 -2
  37. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +28 -9
  38. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +8 -2
  39. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +2 -2
  40. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +7 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-selection.mjs +59 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +2 -2
  43. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +3 -3
  44. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +11 -5
  45. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +50 -6
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md +16 -0
  47. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +56 -7
  48. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  49. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
  50. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +199 -36
  51. package/scaffold/.agent/skills/total-recall/plugins/openwiki/skills/openwiki/SKILL.md +32 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +34 -8
  53. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +6 -0
  54. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +15 -7
  55. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +4 -3
  56. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  57. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  58. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +22 -5
  59. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +3 -2
  60. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +169 -37
  61. package/scaffold/.agent/skills/total-recall/plugins/ssss/skills/ssss/SKILL.md +30 -0
  62. package/src/cli/plugin/create.mjs +2 -1
  63. package/src/cli/plugin/monetization.mjs +66 -32
  64. package/src/cli/plugin/search.mjs +25 -3
  65. package/src/cli/preflight.mjs +40 -6
  66. package/src/cli/update.mjs +20 -1
  67. package/src/core/automatic-brain-updates.mjs +8 -4
  68. package/src/core/brain-updates.mjs +25 -6
  69. package/src/core/gate-mode.mjs +13 -24
  70. package/src/core/plugin-distribution.mjs +35 -1
  71. package/src/core/preflight-gate.mjs +28 -8
  72. package/src/core/preflight-receipt.mjs +11 -0
  73. package/src/core/release-defaults-handoff.mjs +89 -0
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "tr-plugin-meta-harness",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "tr-plugin-meta-harness",
9
- "version": "0.4.0",
9
+ "version": "0.4.2",
10
10
  "dependencies": {
11
11
  "@gregiteen/ssss-cli": "0.14.5"
12
12
  },
13
13
  "devDependencies": {
14
- "total-recall-brain": "3.45.0"
14
+ "total-recall-brain": "3.47.1"
15
15
  }
16
16
  },
17
17
  "node_modules/@alcalzone/ansi-tokenize": {
@@ -5546,9 +5546,9 @@
5546
5546
  }
5547
5547
  },
5548
5548
  "node_modules/total-recall-brain": {
5549
- "version": "3.45.0",
5550
- "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.45.0.tgz",
5551
- "integrity": "sha512-HDYfROazZbsLHQsFPnlBcSpe1HL1QEi7JG8x/eNRUI06RxVQsZWl8LwBo2lTWjWRDrKmCy7BUmSXQDuWF3lrEw==",
5549
+ "version": "3.47.1",
5550
+ "resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.1.tgz",
5551
+ "integrity": "sha512-9yDByVZMNxNE10hjPf0vEmvBLGGUpD8sqAN7svuZpJEb2TSTPA1eSSwaZ5H4QScJQTT3DcV8MeQjuyLy8urnEw==",
5552
5552
  "dev": true,
5553
5553
  "license": "MIT",
5554
5554
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tr-plugin-meta-harness",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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.2",
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);
@@ -73,7 +79,7 @@ export async function dispatchWithReceipt(request, { dispatchFn, node = request.
73
79
  // exhausted until its reset and picks another included harness instead of choosing it again.
74
80
  if (result.failureKind === 'quota') {
75
81
  try { await record({ harness: request.agent, role, task: String(request.contextTask || request.prompt || '').slice(0, 200), success: false,
76
- failureKind: 'quota', error: result.error, resetsAt: result.resetsAt || null }); } catch { /* bookkeeping must not hide the refusal */ }
82
+ failureKind: 'quota', billingMode, error: result.error, resetsAt: result.resetsAt || null }); } catch { /* bookkeeping must not hide the refusal */ }
77
83
  }
78
84
  return { ...result, launchPrevented, durationMs };
79
85
  }
@@ -7,7 +7,7 @@ import { createInterface } from 'node:readline';
7
7
  import { pathToFileURL } from 'node:url';
8
8
  import { configDir } from './paths.mjs';
9
9
  import { agentEnvironment, CODEX_PLAN_ARGS, findBinary, OPENROUTER_AGENTS, readClaudeMaxOrganization } from './worker.mjs';
10
- import { activeQuotaWalls, readReceipts } from './learner.mjs';
10
+ import { activeQuotaWalls, includedBillingReceipts, readReceipts } from './learner.mjs';
11
11
 
12
12
  export const INCLUDED_AGENTS = ['claude', 'codex'];
13
13
  const VERSION = 1;
@@ -235,7 +235,7 @@ export async function includedAdmission(agent, cwd, policy, { readKey = authKey,
235
235
  if (agent === 'claude' && !policy.claudePlan && !claudeProofCurrent(policy)) {
236
236
  return { ok: false, failureKind: 'billing-policy', error: 'Verify the native Claude Max login with Usage credits off, or bind a verified included grant.' };
237
237
  }
238
- const wall = activeQuotaWalls((await receipts()).filter(run => run.billingMode === 'included'))[agent];
238
+ const wall = activeQuotaWalls(includedBillingReceipts(await receipts()))[agent];
239
239
  if (wall) return { ok: false, failureKind: 'quota', error: 'This harness is waiting for included usage.', resetsAt: wall.resetReported ? wall.until : null };
240
240
  if (agent === 'codex') return codex(cwd);
241
241
  if (policy.claudePlan) return { ok: true, claudePlan: policy.claudePlan.organization };
@@ -60,6 +60,13 @@ export async function recordRunReceipt(receipt = {}) {
60
60
  return entry;
61
61
  }
62
62
 
63
+ /**
64
+ * Receipts that count under the included usage policy: included runs, plus quota refusals recorded before
65
+ * refusals carried a billingMode. Those legacy walls expire at their reset, so nothing stale lingers.
66
+ */
67
+ export const includedBillingReceipts = (entries = []) =>
68
+ entries.filter(run => run.billingMode === 'included' || (!run.billingMode && run.failureKind === 'quota'));
69
+
63
70
  /** A harness is walled from its last quota failure until the reported reset, or until a later success proves it clear. */
64
71
  export function activeQuotaWalls(entries = [], now = Date.now(), unknownResetMs = 60 * 60 * 1000) {
65
72
  const latest = new Map();
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  /** The harnesses that can actually take work right now, and why the others cannot. */
2
2
  import { ADAPTER_CATALOG } from '../capability/index.mjs';
3
3
  import { inspectAgents, getActiveAdapters } from './worker.mjs';
4
- import { readReceipts, activeQuotaWalls, activeCredentialBlocks } from './learner.mjs';
4
+ import { readReceipts, activeQuotaWalls, activeCredentialBlocks, includedBillingReceipts } from './learner.mjs';
5
5
  import { measuredWalls } from './telemetry.mjs';
6
6
  import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy } from './included-usage.mjs';
7
7
  import os from 'node:os';
@@ -29,7 +29,7 @@ export async function resolvePool({ availableHarnesses, inspect = inspectAgents,
29
29
  if (!policy.enabled && Array.isArray(availableHarnesses) && availableHarnesses.length) return { ready: availableHarnesses.map(id => describe(id)), excluded: [], walls: {} };
30
30
  const claudePlan = policy.enabled ? policy.claudePlan?.organization : undefined;
31
31
  const [{ agents = [] }, entries] = await Promise.all([inspect({ env: process.env, claudePlan }), receipts()]);
32
- const billingEntries = policy.enabled ? entries.filter(run => run.billingMode === 'included') : entries;
32
+ const billingEntries = policy.enabled ? includedBillingReceipts(entries) : entries;
33
33
  const walls = { ...activeQuotaWalls(billingEntries), ...(!policy.enabled ? measuredWalls() : {}) };
34
34
  const blocks = activeCredentialBlocks(billingEntries);
35
35
  const ready = [], excluded = [];
@@ -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
 
@@ -18,11 +18,12 @@ import { classifyTaskComplexity, selectOptimalHarness, computeEmpiricalScorecard
18
18
  import { inspectAgents } from './worker.mjs';
19
19
  import { inspectNode } from './executor.mjs';
20
20
  import { getModelCatalog, joinHarnessModels } from './model-catalog.mjs';
21
- import { readReceipts, activeQuotaWalls, activeCredentialBlocks } from './learner.mjs';
21
+ import { readReceipts, activeQuotaWalls, activeCredentialBlocks, includedBillingReceipts } from './learner.mjs';
22
22
  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
@@ -168,7 +169,7 @@ export async function inspectQuotas(options = {}) {
168
169
  const inspected = remote ? await (options.inspectNodeFn || inspectNode)(options.node) :
169
170
  await (options.inspect || inspectAgents)({ env: process.env, cwd: options.cwd, claudePlan });
170
171
  const codexLimits = remote ? null : readCodexRateLimits();
171
- const receipts = remote ? [] : (await readReceipts()).filter(run => !policy.enabled || run.billingMode === 'included');
172
+ const receipts = remote ? [] : (policy.enabled ? includedBillingReceipts : runs => runs)(await (options.receipts || readReceipts)());
172
173
  const walls = remote ? {} : { ...activeQuotaWalls(receipts), ...(!policy.enabled ? measuredWalls(codexLimits) : {}) };
173
174
  const metered = METERED_HARNESSES;
174
175
 
@@ -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>