total-recall-brain 3.47.0 → 3.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +5 -0
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +30 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/skills/auth/SKILL.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +494 -79
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +33 -8
- package/scaffold/.agent/skills/total-recall/plugins/automations/skills/automations/SKILL.md +29 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/dream-consolidation.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +31 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/skills/code-quality/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +24 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/skills/embeddings/SKILL.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +293 -58
- package/scaffold/.agent/skills/total-recall/plugins/git/skills/git/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +47 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +28 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +7 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-selection.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +50 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +56 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +199 -36
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/skills/openwiki/SKILL.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +34 -8
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +6 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +15 -7
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +22 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +169 -37
- package/scaffold/.agent/skills/total-recall/plugins/ssss/skills/ssss/SKILL.md +30 -0
- package/src/cli/plugin/create.mjs +2 -1
- package/src/cli/plugin/monetization.mjs +66 -32
- package/src/cli/plugin/search.mjs +25 -3
- package/src/cli/preflight.mjs +40 -6
- package/src/core/gate-mode.mjs +13 -24
- package/src/core/plugin-distribution.mjs +35 -1
- package/src/core/preflight-gate.mjs +28 -8
- package/src/core/preflight-receipt.mjs +11 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "meta-harness",
|
|
3
3
|
"name": "Metaharness",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"description": "Discover, configure, document, track, and run coding agent harnesses through Total Recall across mesh nodes.",
|
|
6
6
|
"gate": {
|
|
7
7
|
"tasks": ["Run work in another agent harness", "Choose which harness takes a task", "Check harness versions, sign-in and quota", "Read a harness's official documentation"],
|
|
@@ -167,7 +167,8 @@
|
|
|
167
167
|
{ "name": "maxTokens", "flag": "--max-tokens", "type": "number", "label": "Job token limit" },
|
|
168
168
|
{ "name": "resultSchema", "flag": "--result-schema", "type": "string", "label": "JSON Schema file" },
|
|
169
169
|
{ "name": "approvals", "flag": "--approvals", "type": "string", "label": "auto, operator, or rules" },
|
|
170
|
-
{ "name": "questionTimeoutMs", "flag": "--question-timeout-ms", "type": "number", "label": "Question timeout in milliseconds" }
|
|
170
|
+
{ "name": "questionTimeoutMs", "flag": "--question-timeout-ms", "type": "number", "label": "Question timeout in milliseconds" },
|
|
171
|
+
{ "name": "model", "flag": "--model", "type": "string", "label": "Model for this run, validated before launch (agy never runs Opus)" }
|
|
171
172
|
],
|
|
172
173
|
"timeout_ms": 600000
|
|
173
174
|
},
|
|
@@ -570,6 +571,7 @@
|
|
|
570
571
|
{ "name": "node", "flag": "--node", "type": "string", "label": "Selected mesh node" },
|
|
571
572
|
{ "name": "allowUnknownAuth", "flag": "--allow-unknown-auth", "type": "boolean", "label": "Explicitly allow a runner without a login probe" },
|
|
572
573
|
{ "name": "background", "flag": "--background", "type": "boolean", "label": "Return the owned job and log for a long run" },
|
|
574
|
+
{ "name": "model", "flag": "--model", "type": "string", "label": "Model to run; only harnesses that can run it are chosen (agy never runs Opus)" },
|
|
573
575
|
{ "name": "timeoutMs", "flag": "--timeout-ms", "type": "number", "label": "Remote wall-clock limit in milliseconds" }
|
|
574
576
|
]
|
|
575
577
|
},
|
|
@@ -627,6 +629,7 @@
|
|
|
627
629
|
{ "name": "node", "flag": "--node", "type": "string", "label": "Selected mesh node" },
|
|
628
630
|
{ "name": "allowUnknownAuth", "flag": "--allow-unknown-auth", "type": "boolean", "label": "Explicitly allow a runner without a login probe" },
|
|
629
631
|
{ "name": "background", "flag": "--background", "type": "boolean", "label": "Return the owned job and log for a long run" },
|
|
632
|
+
{ "name": "model", "flag": "--model", "type": "string", "label": "Model for the Implementer, or every council and race seat (agy never runs Opus)" },
|
|
630
633
|
{ "name": "timeoutMs", "flag": "--timeout-ms", "type": "number", "label": "Remote wall-clock limit in milliseconds" }
|
|
631
634
|
],
|
|
632
635
|
"timeout_ms": 600000
|
|
@@ -45,7 +45,8 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
45
45
|
if (remote) return remote;
|
|
46
46
|
if (options.node) localExecutionNode(options);
|
|
47
47
|
const cwd = options.cwd || process.cwd();
|
|
48
|
-
|
|
48
|
+
// The requested model travels with the controls, so every attempt and a queued replay run it.
|
|
49
|
+
const controls = { ...sessionOptions({ ...options, cwd }), ...(options.model !== undefined ? { model: options.model } : {}) };
|
|
49
50
|
if (!isGitRepo(cwd)) throw new Error('Routed execution writes on its own branch, so it must run inside a git repository');
|
|
50
51
|
const route = options.route || await routeTask(task, options);
|
|
51
52
|
const replay = { kind: 'route', gate: options.gate === true, gateOptions: options.gateOptions, controls };
|
|
@@ -160,7 +161,8 @@ export async function executeRoutedTask(task, options = {}) {
|
|
|
160
161
|
}
|
|
161
162
|
if (stopped) return { ...stopped, route, attempts, alerts };
|
|
162
163
|
|
|
163
|
-
|
|
164
|
+
// Escalation stays with harnesses that can run the requested model.
|
|
165
|
+
const remaining = pool.filter(item => !tried.has(item) && (!route.modelSelection || route.modelSelection.compatible.includes(item)));
|
|
164
166
|
const next = remaining.length ? evaluateGateCascade(harness, { testsPassed: false }, remaining) : null;
|
|
165
167
|
diagnostic = attempt.gate?.summary || attempt.error || 'The previous attempt failed.';
|
|
166
168
|
previous = harness;
|
|
@@ -18,6 +18,7 @@ import { runQualityGate } from './gate.mjs';
|
|
|
18
18
|
import { forwardRemoteOperation } from './remote-operation.mjs';
|
|
19
19
|
import { cancellationResult } from './worker.mjs';
|
|
20
20
|
import { sessionOptions } from './session/run.mjs';
|
|
21
|
+
import { createModelValidator, filterPoolByModel } from './model-selection.mjs';
|
|
21
22
|
|
|
22
23
|
const OPEN_THREADS = new Set();
|
|
23
24
|
const MAX_BLUEPRINT = 20000;
|
|
@@ -42,7 +43,12 @@ export function assignPipelineRoles(pool = [], options = {}) {
|
|
|
42
43
|
const byCost = [...pool].sort((a, b) => a.costScore - b.costScore);
|
|
43
44
|
const explorer = options.explorer ? findById(options.explorer) : byCost.find(h => h.id !== orchestrator.id) || orchestrator;
|
|
44
45
|
const architect = options.architect ? findById(options.architect) : orchestrator;
|
|
45
|
-
|
|
46
|
+
// A requested model is the Implementer's: only a harness that can run it writes the code.
|
|
47
|
+
const compatible = options.modelCompatible;
|
|
48
|
+
const writers = compatible ? sorted.filter(h => compatible.includes(h.id)) : sorted;
|
|
49
|
+
const implementer = options.implementer ? findById(options.implementer) : writers.find(h => h.id !== explorer.id && h.id !== architect.id) || writers.find(h => h.id !== architect.id) || (compatible ? writers[0] || null : architect);
|
|
50
|
+
if (compatible && implementer && !compatible.includes(implementer.id)) throw new Error(`Selected implementer ${implementer.id} cannot run model ${options.model}`);
|
|
51
|
+
if (!implementer) return { orchestrator, explorer, architect, implementer: null, verifier: null, heterogeneousFamilies: false };
|
|
46
52
|
const verifier = options.verifier ? findById(options.verifier) : sorted.find(h => h.family !== implementer.family) || null;
|
|
47
53
|
return { orchestrator, explorer, architect, implementer, verifier, heterogeneousFamilies: Boolean(verifier) && verifier.family !== implementer.family };
|
|
48
54
|
}
|
|
@@ -81,15 +87,18 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
81
87
|
const execute = options.execute === true;
|
|
82
88
|
const withSubagents = options.subagents !== false;
|
|
83
89
|
const stages = roles.orchestrator ? buildStages(roles, withSubagents) : [];
|
|
84
|
-
|
|
90
|
+
if (options.model !== undefined) for (const item of stages) if (item.role === 'Implementer') item.model = options.model;
|
|
91
|
+
const base = { mode: 'pipeline', task, ...modelFields(options), orchestrator: roles.orchestrator?.id || null, executedLive: execute, excluded: poolInfo.excluded, heterogeneousFamiliesVerified: roles.heterogeneousFamilies, stages };
|
|
85
92
|
|
|
86
93
|
if (!execute) {
|
|
87
94
|
const blockers = [];
|
|
88
95
|
if (!roles.orchestrator) blockers.push('No harness can take work right now.');
|
|
96
|
+
else if (!roles.implementer) blockers.push(`No harness that can take work can run model ${options.model}.`);
|
|
89
97
|
else if (!roles.heterogeneousFamilies) blockers.push('Only one model family is available, so no independent Verifier exists.');
|
|
90
98
|
return { ...base, ok: blockers.length === 0, threadId: null, blockers };
|
|
91
99
|
}
|
|
92
100
|
if (!roles.orchestrator) throw new Error(`No harness can take work right now: ${poolInfo.excluded.map(e => `${e.harness} (${e.reason})`).join(', ') || 'none installed'}`);
|
|
101
|
+
if (!roles.implementer) throw new Error(`No harness that can take work can run model ${options.model}`);
|
|
93
102
|
if (!roles.heterogeneousFamilies && options.allowSameFamily !== true) throw new Error('Refusing to run: only one model family is available, so no independent Verifier exists. Pass --allow-same-family to accept self-review.');
|
|
94
103
|
if (!isGitRepo(cwd)) throw new Error('A pipeline writes on its own branch, so it must run inside a git repository');
|
|
95
104
|
|
|
@@ -105,6 +114,7 @@ async function runPipeline(task, poolInfo, options) {
|
|
|
105
114
|
stage.status = 'running';
|
|
106
115
|
const result = await dispatchWithReceipt({ agent: harness.id, node: 'local', cwd: workdir, contextCwd: cwd, contextTask: task, prompt,
|
|
107
116
|
...sessionOptions({ ...options, cwd }), goal: stage.role === 'Implementer' ? options.goal || task : prompt.slice(0, 4000),
|
|
117
|
+
...(stage.role === 'Implementer' && options.model !== undefined ? { model: options.model } : {}),
|
|
108
118
|
permissions: permissions || stage.permissions, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role, complexityScore: complexity.score });
|
|
109
119
|
stage.status = result.cancelled ? 'cancelled' : result.ok ? 'completed' : result.queued ? 'queued' : 'failed';
|
|
110
120
|
stage.output = result.response || result.error || null;
|
|
@@ -193,11 +203,11 @@ async function runCouncil(task, poolInfo, options) {
|
|
|
193
203
|
// One seat per model family, strongest first, so the seats can actually disagree.
|
|
194
204
|
const families = new Set();
|
|
195
205
|
const seats = options.harnesses || [...poolInfo.ready].sort((a, b) => b.qualityScore - a.qualityScore).filter(h => !families.has(h.family) && families.add(h.family)).slice(0, 3).map(h => h.id);
|
|
196
|
-
if (options.execute !== true) return { ok: seats.length > 0, mode: 'council', task, executedLive: false, consensus: 'planned', excluded: poolInfo.excluded, votes: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
206
|
+
if (options.execute !== true) return { ok: seats.length > 0, mode: 'council', task, ...modelFields(options), executedLive: false, consensus: 'planned', excluded: poolInfo.excluded, votes: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
197
207
|
if (!seats.length) throw new Error('No harness can take work right now');
|
|
198
208
|
const cwd = options.cwd || process.cwd();
|
|
199
209
|
const votes = await Promise.all(seats.map(async harness => {
|
|
200
|
-
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), prompt: `[ROLE: COUNCIL MEMBER]\nDo not change any file. Assess this proposal, with its trade-offs and risks:\n${task}\n\nEnd your reply with exactly one JSON object: {"vote":"APPROVE"|"REJECT","reason":"..."}`, permissions: READ, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Council' });
|
|
210
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), ...(options.model !== undefined ? { model: options.model } : {}), prompt: `[ROLE: COUNCIL MEMBER]\nDo not change any file. Assess this proposal, with its trade-offs and risks:\n${task}\n\nEnd your reply with exactly one JSON object: {"vote":"APPROVE"|"REJECT","reason":"..."}`, permissions: READ, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Council' });
|
|
201
211
|
return { harness, ok: result.ok === true, failureKind: result.failureKind, ...(result.ok ? parseVote(result.response) : { vote: 'ABSTAIN', reason: null, error: result.error }),
|
|
202
212
|
...(result.queueId ? { queueId: result.queueId } : {}), feedback: result.response || null };
|
|
203
213
|
}));
|
|
@@ -205,12 +215,12 @@ async function runCouncil(task, poolInfo, options) {
|
|
|
205
215
|
const majority = votes.length / 2;
|
|
206
216
|
const consensus = count('APPROVE') > majority ? 'approved' : count('REJECT') > majority ? 'rejected' : 'no-consensus';
|
|
207
217
|
const limited = votes.some(item => item.failureKind === 'limit');
|
|
208
|
-
return { ok: !limited && votes.some(item => item.ok), ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'council', task, executedLive: true, consensus, excluded: poolInfo.excluded, votes };
|
|
218
|
+
return { ok: !limited && votes.some(item => item.ok), ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'council', task, ...modelFields(options), executedLive: true, consensus, excluded: poolInfo.excluded, votes };
|
|
209
219
|
}
|
|
210
220
|
|
|
211
221
|
async function runRace(task, poolInfo, options) {
|
|
212
222
|
const seats = options.candidates || [...poolInfo.ready].sort((a, b) => b.qualityScore - a.qualityScore).slice(0, 2).map(h => h.id);
|
|
213
|
-
if (options.execute !== true) return { ok: seats.length > 1, mode: 'race', task, executedLive: false, excluded: poolInfo.excluded, candidates: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
223
|
+
if (options.execute !== true) return { ok: seats.length > 1, mode: 'race', task, ...modelFields(options), executedLive: false, excluded: poolInfo.excluded, candidates: seats.map(harness => ({ harness, status: 'planned' })) };
|
|
214
224
|
if (seats.length < 2) throw new Error('A race needs at least two harnesses that can take work');
|
|
215
225
|
if (!options.gate) return { ok: false, mode: 'race', task, executedLive: false, verification: 'unverified', gateApplied: false, error: 'A race requires a deterministic quality gate before any candidate can win.' };
|
|
216
226
|
const cwd = options.cwd || process.cwd();
|
|
@@ -223,7 +233,7 @@ async function runRace(task, poolInfo, options) {
|
|
|
223
233
|
try {
|
|
224
234
|
const started = Date.now();
|
|
225
235
|
directory = createWorktree(cwd, { branch });
|
|
226
|
-
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd: directory, contextCwd: cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), goal: options.goal || task, prompt: `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit.\nTask:\n${task}`, permissions: WRITE, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Race' });
|
|
236
|
+
const result = await dispatchWithReceipt({ agent: harness, node: 'local', cwd: directory, contextCwd: cwd, contextTask: task, ...sessionOptions({ ...options, cwd }), goal: options.goal || task, ...(options.model !== undefined ? { model: options.model } : {}), prompt: `[ROLE: IMPLEMENTER]\nWork only in this checkout. Implement the task and its tests. Do not commit.\nTask:\n${task}`, permissions: WRITE, allowUnknownAuth: options.allowUnknownAuth === true }, { dispatchFn: options.dispatchFn, role: 'Race' });
|
|
227
237
|
const commit = result.ok || result.cancelled ? commitAll(directory, `meta-harness race: ${task.slice(0, 60)}\n\nAgent: ${harness}`) : null;
|
|
228
238
|
if (result.cancelled) {
|
|
229
239
|
keep = Boolean(commit);
|
|
@@ -247,9 +257,11 @@ async function runRace(task, poolInfo, options) {
|
|
|
247
257
|
}));
|
|
248
258
|
const finishers = entries.filter(entry => entry.ok).sort((a, b) => a.completedAt - b.completedAt);
|
|
249
259
|
const limited = entries.some(item => item.failureKind === 'limit');
|
|
250
|
-
return { ok: !limited && finishers.length > 0, ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'race', task, executedLive: true, gateApplied: true, verification: !limited && finishers.length ? 'passed' : 'failed', winner: limited ? null : finishers[0]?.harness || null, excluded: poolInfo.excluded, candidates: entries };
|
|
260
|
+
return { ok: !limited && finishers.length > 0, ...(limited ? { failureKind: 'limit' } : {}), ...cancellationResult(), mode: 'race', task, ...modelFields(options), executedLive: true, gateApplied: true, verification: !limited && finishers.length ? 'passed' : 'failed', winner: limited ? null : finishers[0]?.harness || null, excluded: poolInfo.excluded, candidates: entries };
|
|
251
261
|
}
|
|
252
262
|
|
|
263
|
+
const modelFields = options => options.model !== undefined ? { model: options.model, modelSelection: options.modelSelection } : {};
|
|
264
|
+
|
|
253
265
|
export async function coordinateTask(taskDescription, options = {}) {
|
|
254
266
|
if (cancellationResult()) return cancellationResult();
|
|
255
267
|
const task = String(taskDescription || '').trim();
|
|
@@ -259,7 +271,14 @@ export async function coordinateTask(taskDescription, options = {}) {
|
|
|
259
271
|
const mode = options.mode || 'pipeline';
|
|
260
272
|
if (!['pipeline', 'orchestrate', 'council', 'race'].includes(mode)) throw new Error(`Unsupported coordination mode: ${mode}`);
|
|
261
273
|
localExecutionNode(options);
|
|
262
|
-
|
|
274
|
+
let poolInfo = await resolvePool(options);
|
|
275
|
+
if (options.model !== undefined) {
|
|
276
|
+
// Council and race seats all run the model, so their pool narrows. A pipeline keeps every harness
|
|
277
|
+
// for its other roles and gives the Implementer role to one that can run the model.
|
|
278
|
+
const filtered = filterPoolByModel(poolInfo, options.model, options.modelValidator || createModelValidator());
|
|
279
|
+
options = { ...options, modelSelection: filtered.selection, modelCompatible: filtered.selection.compatible };
|
|
280
|
+
if (mode === 'council' || mode === 'race') poolInfo = filtered.pool;
|
|
281
|
+
}
|
|
263
282
|
const wall = poolInfo.excluded.find(item => item.failureKind === 'quota');
|
|
264
283
|
const minimum = mode === 'race' || ((mode === 'pipeline' || mode === 'orchestrate') && options.allowSameFamily !== true) ? 2 : 1;
|
|
265
284
|
if (options.execute === true && wall && poolInfo.ready.length < minimum) {
|
|
@@ -6,11 +6,17 @@ import { isGitRepo, createWorktree, removeWorktree, commitAll, deleteBranch } fr
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { includedAdmission, openRouterAdmission, readIncludedPolicy } from './included-usage.mjs';
|
|
8
8
|
import { queueIncludedJob } from './usage-queue.mjs';
|
|
9
|
+
import { createModelValidator } from './model-selection.mjs';
|
|
9
10
|
|
|
10
11
|
export async function dispatchWithReceipt(request, { dispatchFn, node = request.node || 'local', role, complexityScore, escalatedFrom,
|
|
11
|
-
record = recordRunReceipt, replay, retryId, manageWrites = false, usagePolicy = readIncludedPolicy, admit = includedAdmission, admitOpenRouter = openRouterAdmission } = {}) {
|
|
12
|
+
record = recordRunReceipt, replay, retryId, manageWrites = false, validateModel, usagePolicy = readIncludedPolicy, admit = includedAdmission, admitOpenRouter = openRouterAdmission } = {}) {
|
|
12
13
|
const started = Date.now();
|
|
13
14
|
if (cancellationResult()) return cancellationResult();
|
|
15
|
+
// A model this harness must not or cannot run is refused before launch; it says nothing about the harness, so no receipt.
|
|
16
|
+
if (request.model !== undefined) {
|
|
17
|
+
const check = (validateModel || createModelValidator({ local: node === 'local' }))(request.agent, request.model);
|
|
18
|
+
if (!check.ok) return { ok: false, agent: request.agent, failureKind: 'model', error: check.reason, modelCheck: check, launchPrevented: true, exitCode: null, durationMs: Date.now() - started };
|
|
19
|
+
}
|
|
14
20
|
// A harness that cannot be told not to write does read-only work in a checkout that is thrown away.
|
|
15
21
|
const contain = node === 'local' && request.permissions?.write !== true && !READ_ONLY_ADAPTERS.includes(request.agent) && request.isolated !== true && isGitRepo(request.cwd);
|
|
16
22
|
const ownWrites = manageWrites && node === 'local' && request.permissions?.write === true && isGitRepo(request.cwd);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which harness may run a requested model. Shape, operator policy and harness support come first
|
|
3
|
+
* (worker.mjs, so a remote destination enforces them too). Then the AI Models plugin, which owns the
|
|
4
|
+
* shared model catalog, decides when it answers `ai-models validate`. When it cannot answer, this
|
|
5
|
+
* machine's synced harness model lists decide; a harness without a list is accepted as unverified.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { modelRefusal } from './worker.mjs';
|
|
9
|
+
import { getModelCatalog, normalizeModelName } from './model-catalog.mjs';
|
|
10
|
+
|
|
11
|
+
/** Ask the AI Models owner. Null means it gave no verdict (not installed, not implemented, or unreadable). */
|
|
12
|
+
export function queryAiModels(harness, model, { bin = process.env.TOTAL_RECALL_BIN || 'total-recall' } = {}) {
|
|
13
|
+
let output;
|
|
14
|
+
try {
|
|
15
|
+
output = execFileSync(bin, ['ai-models', 'validate', '--harness', harness, '--model', model, '--json'], { encoding: 'utf8', timeout: 20000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
// A refusal may exit nonzero with its verdict on stdout.
|
|
18
|
+
output = String(error.stdout || '');
|
|
19
|
+
}
|
|
20
|
+
const line = output.trim().split('\n').filter(Boolean).at(-1);
|
|
21
|
+
try {
|
|
22
|
+
const answer = JSON.parse(line);
|
|
23
|
+
return answer && typeof answer.ok === 'boolean' ? answer : null;
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One validator per decision: after the owner fails to answer once, it is not asked again. */
|
|
28
|
+
export function createModelValidator({ aiModels = queryAiModels, catalog = getModelCatalog, local = true } = {}) {
|
|
29
|
+
let ownerAnswers = true, listed;
|
|
30
|
+
return (harness, model) => {
|
|
31
|
+
const refusal = modelRefusal(harness, model);
|
|
32
|
+
if (refusal) return { ok: false, harness, model, source: 'policy', reason: refusal };
|
|
33
|
+
if (ownerAnswers) {
|
|
34
|
+
const answer = aiModels(harness, model);
|
|
35
|
+
if (answer) return { ok: answer.ok, harness, model, source: 'ai-models', ...(answer.ok ? {} : { reason: answer.reason || answer.error || `AI Models does not list ${model} for ${harness}` }) };
|
|
36
|
+
ownerAnswers = false;
|
|
37
|
+
}
|
|
38
|
+
// Another node's catalog is not this one's: its destination validates against its own.
|
|
39
|
+
if (!local) return { ok: true, harness, model, source: 'unverified', note: 'The destination node validates the model before launch.' };
|
|
40
|
+
listed ??= catalog().harnessModels || {};
|
|
41
|
+
const models = listed[harness];
|
|
42
|
+
if (Array.isArray(models) && models.length) {
|
|
43
|
+
const key = normalizeModelName(model);
|
|
44
|
+
if (models.some(row => row.id === model || normalizeModelName(row.id) === key)) return { ok: true, harness, model, source: 'catalog' };
|
|
45
|
+
return { ok: false, harness, model, source: 'catalog', reason: `${harness} does not list ${model}. Listed: ${models.slice(0, 12).map(row => row.id).join(', ')}` };
|
|
46
|
+
}
|
|
47
|
+
return { ok: true, harness, model, source: 'unverified', note: `No model list for ${harness}; only a completed run proves ${model} works there.` };
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Split a pool by whether each harness can run the model. */
|
|
52
|
+
export function filterPoolByModel(pool, model, validate) {
|
|
53
|
+
const checks = pool.ready.map(harness => validate(harness.id, model));
|
|
54
|
+
return {
|
|
55
|
+
pool: { ...pool, ready: pool.ready.filter((_, index) => checks[index].ok),
|
|
56
|
+
excluded: [...pool.excluded, ...checks.filter(check => !check.ok).map(check => ({ harness: check.harness, reason: check.reason, failureKind: 'model' }))] },
|
|
57
|
+
selection: { model, compatible: checks.filter(check => check.ok).map(check => check.harness), checks },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -10,11 +10,11 @@ const BOOLEAN_FLAGS = {
|
|
|
10
10
|
const VALUE_FLAGS = {
|
|
11
11
|
mode: '--mode', orchestrator: '--orchestrator', explorer: '--explorer', architect: '--architect', implementer: '--implementer',
|
|
12
12
|
goal: '--goal', maxTurns: '--max-turns', maxBudgetUsd: '--max-budget-usd', maxTokens: '--max-tokens', approvalPolicy: '--approvals', questionTimeoutMs: '--question-timeout-ms', resultSchema: '--result-schema',
|
|
13
|
-
verifier: '--verifier', reviewer: '--reviewer', author: '--author', threadId: '--thread', branch: '--branch',
|
|
13
|
+
verifier: '--verifier', reviewer: '--reviewer', author: '--author', threadId: '--thread', branch: '--branch', model: '--model',
|
|
14
14
|
};
|
|
15
15
|
const OPERATION_KEYS = {
|
|
16
|
-
route: ['execute', 'gate', 'jev', 'preferLocal', 'preferFrontier', 'allowUnknownAuth'],
|
|
17
|
-
coordinate: ['execute', 'gate', 'autoReview', 'allowSameFamily', 'allowUnknownAuth', 'mode', 'orchestrator', 'explorer', 'architect', 'implementer', 'verifier', 'reviewer', 'branch'],
|
|
16
|
+
route: ['execute', 'gate', 'jev', 'preferLocal', 'preferFrontier', 'allowUnknownAuth', 'model'],
|
|
17
|
+
coordinate: ['execute', 'gate', 'autoReview', 'allowSameFamily', 'allowUnknownAuth', 'mode', 'orchestrator', 'explorer', 'architect', 'implementer', 'verifier', 'reviewer', 'branch', 'model'],
|
|
18
18
|
review: ['staged', 'allowUnknownAuth', 'reviewer', 'author', 'threadId'],
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -23,6 +23,7 @@ import { resolvePool, exclusionReason, isLocalExecutionNode } from './pool.mjs';
|
|
|
23
23
|
import { readCodexRateLimits, measuredWalls } from './telemetry.mjs';
|
|
24
24
|
import { claudeProofCurrent, INCLUDED_AGENTS, readIncludedPolicy } from './included-usage.mjs';
|
|
25
25
|
import { derivedDir } from './paths.mjs';
|
|
26
|
+
import { createModelValidator, filterPoolByModel } from './model-selection.mjs';
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Helper to safely query host Total Recall CLI without assumptions
|
|
@@ -273,7 +274,10 @@ export async function routeTask(taskDescription, options = {}) {
|
|
|
273
274
|
// fleet telemetry places local work, and the balance matters only to a metered harness.
|
|
274
275
|
const shared = scoped.inspect ? null : inspectAgents({ env: process.env });
|
|
275
276
|
const lean = shared ? { ...scoped, inspect: () => shared } : scoped;
|
|
276
|
-
const [
|
|
277
|
+
const [resolved, receipts] = await Promise.all([resolvePool({ ...lean, policy }), readReceipts()]);
|
|
278
|
+
// A requested model narrows the pool to the harnesses that can run it, and says why the others cannot.
|
|
279
|
+
const { pool, selection: modelSelection } = options.model === undefined ? { pool: resolved, selection: null }
|
|
280
|
+
: filterPoolByModel(resolved, options.model, options.modelValidator || createModelValidator({ local: !remote }));
|
|
277
281
|
const quotaReport = await inspectQuotas({
|
|
278
282
|
...lean,
|
|
279
283
|
policy,
|
|
@@ -289,10 +293,12 @@ export async function routeTask(taskDescription, options = {}) {
|
|
|
289
293
|
if (typeof selectedQuota.usagePercent === 'number' && selectedQuota.usagePercent > 85) warnings.push(`${routing.selected} is at ${selectedQuota.usagePercent}% measured usage.`);
|
|
290
294
|
if (selectedQuota.status === 'unverified') warnings.push(`${routing.selected} has no login probe; only a completed run proves it works.`);
|
|
291
295
|
if (selectedQuota.billingModel === 'metered') warnings.push(`${routing.selected} bills per token.`);
|
|
292
|
-
if (!routing.selected) warnings.push('No harness can take this task right now.');
|
|
296
|
+
if (!routing.selected) warnings.push(modelSelection ? `No harness that can run ${options.model} can take this task right now.` : 'No harness can take this task right now.');
|
|
297
|
+
const selectedModel = modelSelection?.checks.find(check => check.harness === routing.selected);
|
|
298
|
+
if (selectedModel?.source === 'unverified') warnings.push(selectedModel.note);
|
|
293
299
|
return {
|
|
294
300
|
ok: Boolean(routing.selected), task: taskDescription, classifier, complexity,
|
|
295
|
-
selected: routing.selected, tier: complexity.tier, candidates: routing.candidates, excluded: pool.excluded,
|
|
301
|
+
selected: routing.selected, tier: complexity.tier, model: options.model ?? null, ...(modelSelection ? { modelSelection } : {}), candidates: routing.candidates, excluded: pool.excluded,
|
|
296
302
|
...(policy.enabled && !routing.selected ? { deferred: pool.excluded.find(row => row.failureKind === 'quota') || null } : {}),
|
|
297
303
|
placement: complexity.tier === 'local' ? placeLocalWork(quotaReport.fleet) : null,
|
|
298
304
|
scoring: 'Adapter quality and cost scores are configured priors, adjusted by pass rates measured on this machine.',
|
|
@@ -73,6 +73,43 @@ export const ADAPTERS = [
|
|
|
73
73
|
{ id: 'hermes', name: 'Hermes', binary: 'hermes', args: ['chat', '--oneshot', '--format', 'stream-json', '--query-file', '-'], input: 'stdin', docs: 'https://github.com/NousResearch/hermes-agent/blob/main/website/docs/reference/cli-commands.md' }
|
|
74
74
|
];
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* How each harness takes a model for one run, from its official CLI reference (read 2026-11-14 through
|
|
78
|
+
* `meta-harness docs-search`). A flag goes after the subcommand, before the flag that carries the prompt.
|
|
79
|
+
* DSH has no per-run model flag: its headless profile owns the model (`total-recall dsh model`).
|
|
80
|
+
*/
|
|
81
|
+
export const MODEL_SELECTION = Object.freeze({
|
|
82
|
+
claude: { flag: '--model' }, codex: { flag: '--model' }, agy: { flag: '--model' }, cursor: { flag: '--model' },
|
|
83
|
+
grok: { flag: '--model' }, muse: { flag: '--model' }, pi: { flag: '--model' }, hermes: { flag: '--model' },
|
|
84
|
+
openclaw: { flag: '--model' }, aider: { flag: '--model' }, cline: { flag: '--model' },
|
|
85
|
+
goose: { env: 'GOOSE_MODEL' },
|
|
86
|
+
// OpenHands ignores LLM_MODEL unless the run also passes --override-with-envs; the override is not persisted.
|
|
87
|
+
openhands: { env: 'LLM_MODEL', args: ['--override-with-envs'] },
|
|
88
|
+
ollama: { positional: true },
|
|
89
|
+
});
|
|
90
|
+
// Operator policy: Antigravity never runs an Opus model, although `agy models` lists them.
|
|
91
|
+
const MODEL_POLICY = [{ harness: 'agy', test: model => /opus/i.test(model), reason: 'Antigravity (agy) must not run Opus models. Choose a Gemini or Sonnet model, or route Opus work to claude.' }];
|
|
92
|
+
|
|
93
|
+
/** Why this harness cannot run this model, or null. Shape, operator policy and harness support only; catalogs live elsewhere. */
|
|
94
|
+
export function modelRefusal(harness, model) {
|
|
95
|
+
if (typeof model !== 'string' || !model.trim() || model.length > 200 || /[\0-\x1f\s]/.test(model) || model.startsWith('-')) return 'Invalid requested model';
|
|
96
|
+
const policy = MODEL_POLICY.find(rule => rule.harness === harness && rule.test(model));
|
|
97
|
+
if (policy) return policy.reason;
|
|
98
|
+
if (!Object.hasOwn(MODEL_SELECTION, harness)) {
|
|
99
|
+
return harness === 'dsh' ? 'DSH selects its model through its headless profile; set it with `total-recall dsh model`.' : `${harness} does not declare how it selects a model`;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Put the model where the harness reads it. Returns the environment the launch adds. */
|
|
105
|
+
export function applyModel(harness, model, args) {
|
|
106
|
+
const selection = MODEL_SELECTION[harness];
|
|
107
|
+
const at = args.length && !args[0].startsWith('-') ? 1 : 0;
|
|
108
|
+
if (selection.flag) args.splice(at, 0, selection.flag, model);
|
|
109
|
+
if (selection.args) args.splice(at, 0, ...selection.args);
|
|
110
|
+
return selection.env ? { [selection.env]: model } : {};
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
export const EXTENDED_ADAPTERS = [
|
|
77
114
|
{ id: 'ollama', name: 'Ollama', binary: 'ollama', args: ['run'], input: 'stdin', docs: 'https://ollama.com/library' },
|
|
78
115
|
{ id: 'openclaw', name: 'OpenClaw', binary: 'openclaw', args: ['agent', '--message'], input: 'argument', docs: 'https://openclaw.ai/docs' },
|
|
@@ -535,7 +572,12 @@ export async function totalRecallBootstrap(request, { probe = runProcess, env =
|
|
|
535
572
|
if (docs.ok !== true) throw notReady('Official agent documentation is unavailable or stale');
|
|
536
573
|
// Node built-ins only here (this file ships to remote nodes): the result is the pretty-printed JSON after any log lines.
|
|
537
574
|
let found = null;
|
|
538
|
-
try {
|
|
575
|
+
try {
|
|
576
|
+
const res = await probe(binary, ['help', task, '--json'], { cwd, timeoutMs: 30000, maxBytes: 1024 * 1024, env: agentEnvironment(env) });
|
|
577
|
+
const out = String(res.stdout || '');
|
|
578
|
+
const at = out.indexOf('{\n');
|
|
579
|
+
if (at >= 0 || out.trim().startsWith('{')) found = JSON.parse(at >= 0 ? out.slice(at) : out);
|
|
580
|
+
}
|
|
539
581
|
catch (error) { if (error.code === 'NOT_READY') throw error; found = null; }
|
|
540
582
|
const matches = Array.isArray(found?.matches) ? found.matches.slice(0, 12) : [];
|
|
541
583
|
const references = `${matches.length ? `Commands that match this task (Total Recall semantic command search):\n${matches.map(m => `- ${m.verb} — ${m.description}`).join('\n')}\n` : ''}For anything else, run total-recall help "<what you need>" to find the command, then total-recall <namespace> --help for its arguments. Do not load whole command inventories.`;
|
|
@@ -661,6 +703,7 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
661
703
|
credentials[name] = value;
|
|
662
704
|
}
|
|
663
705
|
const args = [...adapter.args];
|
|
706
|
+
let modelEnv = {};
|
|
664
707
|
const openClawSession = openRouter && adapter.id === 'openclaw' ? randomUUID() : null;
|
|
665
708
|
if (openRouter) {
|
|
666
709
|
if (adapter.id === 'pi') args.push('--model', openRouter.model);
|
|
@@ -668,9 +711,10 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
668
711
|
if (openClawSession) args.splice(1, 0, '--agent', 'main', '--session-id', openClawSession,
|
|
669
712
|
'--model', `openrouter/${openRouter.model}`, '--json');
|
|
670
713
|
} else if (request.model !== undefined) {
|
|
671
|
-
|
|
672
|
-
if (
|
|
673
|
-
|
|
714
|
+
const refusal = modelRefusal(adapter.custom ? `custom adapter ${adapter.id}` : adapter.id, request.model);
|
|
715
|
+
if (refusal) throw notReady(refusal);
|
|
716
|
+
if (adapter.id === 'ollama' && !state.chatModels.includes(request.model)) throw notReady(`Ollama has no chat model ${request.model} on this node`);
|
|
717
|
+
modelEnv = applyModel(adapter.id, request.model, args);
|
|
674
718
|
}
|
|
675
719
|
if (request.tools !== undefined || request.settingSources !== undefined || request.allowedTools !== undefined) {
|
|
676
720
|
if (adapter.id !== 'claude') throw notReady(`${adapter.name} cannot enforce the requested tools/settings restriction`);
|
|
@@ -705,7 +749,7 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
705
749
|
if (adapter.id === 'agy') args[args.indexOf('--mode') + 1] = allowWrites ? 'accept-edits' : 'plan';
|
|
706
750
|
if (adapter.id === 'muse' && !allowWrites) args.push('--disable-write');
|
|
707
751
|
if (adapter.id === 'pi') args[args.indexOf('--tools') + 1] = allowWrites ? 'read,bash,edit,write' : 'read';
|
|
708
|
-
if (adapter.id === 'ollama') args.push(env.TR_OLLAMA_MODEL || state.chatModels[0]);
|
|
752
|
+
if (adapter.id === 'ollama') args.push(request.model ?? (env.TR_OLLAMA_MODEL || state.chatModels[0]));
|
|
709
753
|
const progressFile = request.progressFile || path.join(
|
|
710
754
|
env.TR_SECRETS_BRAIN || path.join(env.AGENT_DIR || path.join(env.HOME || os.homedir(), '.agent'), 'skills/total-recall'),
|
|
711
755
|
'config/meta-harness/jobs', `${request.jobId || process.env.TR_METAHARNESS_JOB || `session-${randomUUID()}`}.events.jsonl`);
|
|
@@ -737,7 +781,7 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
737
781
|
};
|
|
738
782
|
try {
|
|
739
783
|
parsed = await (sessionRunner || runSession)({ request, adapter, binary, args,
|
|
740
|
-
env: { ...agentEnvironment(env), ...credentials, TR_META_HARNESS_RUN: '1' }, prompt, progress: liveProgress, onFrame: captureFrame,
|
|
784
|
+
env: { ...agentEnvironment(env), ...credentials, ...modelEnv, TR_META_HARNESS_RUN: '1' }, prompt, progress: liveProgress, onFrame: captureFrame,
|
|
741
785
|
register: stop => { const id = Symbol('session'); ACTIVE_GROUPS.set(id, stop); return () => ACTIVE_GROUPS.delete(id); } });
|
|
742
786
|
if (cancellationResult()) parsed = { ...parsed, ...cancellationResult() };
|
|
743
787
|
if (frames.length) Object.assign(parsed, toolEvidence(nativeToolResults(adapter.id, frames)));
|
package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: ui-chunk:chunk
|
|
3
|
+
plugin: meta-harness
|
|
4
|
+
id: session-controls
|
|
5
|
+
title: Session controls and reviews
|
|
6
|
+
description: Fork, resume, cancel, reply, approve, interrupt, set goals, manage MCP servers, watch progress, and inspect independent reviews.
|
|
7
|
+
timestamp: "2026-11-14T08:51:00Z"
|
|
8
|
+
intents: [control harness sessions, approve or interrupt run, watch session, inspect independent reviews, manage mcp]
|
|
9
|
+
surfaces: [page]
|
|
10
|
+
operations: [fork, resume, cancel, reply, approve, interrupt, goal, mcp, watch, reviews]
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<section>
|
|
14
|
+
<h2>Session controls & reviews</h2>
|
|
15
|
+
<p>Manage active harness runs (fork, resume, reply, approve, interrupt, cancel, goal, watch), configure MCP bindings, and review independent verification runs.</p>
|
|
16
|
+
</section>
|
|
@@ -11,7 +11,7 @@ import { CREDIT_REFUSAL, NO_MODEL, describeRetention, hasWiki, isDue, readProjec
|
|
|
11
11
|
|
|
12
12
|
const DEFAULT_REPO_CONCURRENCY = 2;
|
|
13
13
|
import { watchUpstream, propagateUpdates, readUpstreamState, bundledGeneratorVersion } from './src/upstream-watcher.mjs';
|
|
14
|
-
import { loadOpenWikiConfig, setOpenWikiConfigValue, resetOpenWikiConfig, resolveConfigPath, ALLOWED_CONFIG_KEYS, ALLOWED_PROVIDERS } from './src/config.mjs';
|
|
14
|
+
import { loadOpenWikiConfig, setOpenWikiConfigValue, resetOpenWikiConfig, resolveConfigPath, setExcludedRoots, ALLOWED_CONFIG_KEYS, ALLOWED_PROVIDERS } from './src/config.mjs';
|
|
15
15
|
import { discoverAllHubs } from './src/mesh-vaults.mjs';
|
|
16
16
|
import { searchWikiHubs } from './src/search.mjs';
|
|
17
17
|
import { buildKnowledgeGraph } from './src/graph.mjs';
|
|
@@ -83,8 +83,12 @@ export async function refreshCommand(args, {
|
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
85
|
if (!state.due) {
|
|
86
|
-
if (state.
|
|
86
|
+
if (state.excluded) {
|
|
87
|
+
reports.push({ ...project, action: 'excluded', reason: state.reason });
|
|
88
|
+
} else if (state.skipped) {
|
|
87
89
|
reports.push({ ...project, action: 'skipped', reason: state.reason });
|
|
90
|
+
} else if (state.uninitialized && !state.held) {
|
|
91
|
+
reports.push({ ...project, action: 'no-wiki', uninitialized: true, reason: state.reason, generatedAt: state.generatedAt });
|
|
88
92
|
} else if (state.held) {
|
|
89
93
|
reports.push({ ...project, action: 'held', uninitialized: Boolean(state.uninitialized), reason: state.reason, generationCommit: state.generationCommit, generationTime: state.generationTime, generatedAt: state.generatedAt, commitsSince: state.commitsSince });
|
|
90
94
|
} else {
|
|
@@ -202,9 +206,10 @@ export async function refreshCommand(args, {
|
|
|
202
206
|
note: payloadBudgetNote,
|
|
203
207
|
counts: {
|
|
204
208
|
registered: projects.length,
|
|
205
|
-
withPages: reports.filter((r) => r.action
|
|
209
|
+
withPages: reports.filter((r) => !['skipped', 'excluded'].includes(r.action) && !r.uninitialized).length,
|
|
206
210
|
due: due.length,
|
|
207
211
|
uninitialized: reports.filter((r) => r.uninitialized).length,
|
|
212
|
+
excluded: reports.filter((r) => r.action === 'excluded').length,
|
|
208
213
|
refreshed: reports.filter((r) => r.action === 'refreshed' || r.action === 'initialized').length,
|
|
209
214
|
failed: failed.length,
|
|
210
215
|
partial: partial.length,
|
|
@@ -217,11 +222,11 @@ export async function refreshCommand(args, {
|
|
|
217
222
|
};
|
|
218
223
|
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
219
224
|
else {
|
|
220
|
-
console.log(`OpenWiki refresh (${apply ? 'apply' : 'report'}): ${payload.counts.registered} registered, ${payload.counts.due} due
|
|
225
|
+
console.log(`OpenWiki refresh (${apply ? 'apply' : 'report'}): ${payload.counts.registered} registered, ${payload.counts.due} due, ${payload.counts.uninitialized} without a finished wiki, ${payload.counts.excluded} excluded`);
|
|
221
226
|
// Put failures last so bounded scheduler tails retain their causes.
|
|
222
227
|
for (const r of [...reports].sort((a, b) => Number(['failed', 'partial'].includes(a.action)) - Number(['failed', 'partial'].includes(b.action)))) {
|
|
223
|
-
if (r.action === 'current') continue;
|
|
224
|
-
const age = r.action === 'skipped' ? 'outside repository generation' : r.generationTime ? `generated ${r.generationTime} at ${r.generationCommit}` : r.uninitialized ? '
|
|
228
|
+
if (r.action === 'current' || r.action === 'excluded') continue;
|
|
229
|
+
const age = r.action === 'skipped' ? 'outside repository generation' : r.generationTime ? `generated ${r.generationTime} at ${r.generationCommit}` : r.uninitialized ? 'no finished wiki' : (r.generatedAt ? `last attempt ${r.generatedAt}; generation commit unknown` : 'never generated');
|
|
225
230
|
console.log(` ${r.action.padEnd(9)} ${r.name.padEnd(28)} ${age}${Number.isFinite(r.commitsSince) && r.commitsSince !== Infinity ? `, ${r.commitsSince} commits since` : ''}${r.reason ? ` — ${r.reason}` : ''}`);
|
|
226
231
|
}
|
|
227
232
|
if (payloadBudgetNote) console.log(payloadBudgetNote);
|
|
@@ -730,6 +735,46 @@ export async function scheduleCommand(args = [], { core = null, config = loadOpe
|
|
|
730
735
|
}
|
|
731
736
|
}
|
|
732
737
|
|
|
738
|
+
/**
|
|
739
|
+
* List, add or remove repositories the refresh never touches. Takes registered
|
|
740
|
+
* project names or absolute paths; with none, lists the current exclusions.
|
|
741
|
+
*/
|
|
742
|
+
export async function excludeCommand(args = [], { read = readProjects, load = loadOpenWikiConfig, save = setExcludedRoots } = {}) {
|
|
743
|
+
const json = args.includes('--json');
|
|
744
|
+
const usage = 'Usage: total-recall openwiki exclude [<name|absolute path>...] [--remove] [--json]';
|
|
745
|
+
if (args.includes('--help') || args.includes('-h')) { console.log(usage); return; }
|
|
746
|
+
const remove = args.includes('--remove');
|
|
747
|
+
const targets = args.filter((a) => !a.startsWith('--'));
|
|
748
|
+
try {
|
|
749
|
+
if (remove && !targets.length) throw new Error(usage);
|
|
750
|
+
let roots = load().exclude || [];
|
|
751
|
+
if (targets.length) {
|
|
752
|
+
let projects = [];
|
|
753
|
+
try { projects = read({ exclude: [] }); } catch { projects = []; }
|
|
754
|
+
const chosen = [];
|
|
755
|
+
for (const target of targets) {
|
|
756
|
+
const matches = projects.filter((p) => p.name === target || (path.isAbsolute(target) && path.resolve(p.root) === path.resolve(target)));
|
|
757
|
+
if (matches.length > 1) throw new Error(`${target} names ${matches.length} registered projects; pass its absolute path`);
|
|
758
|
+
if (matches.length === 1) chosen.push(path.resolve(matches[0].root));
|
|
759
|
+
else if (path.isAbsolute(target)) chosen.push(path.resolve(target));
|
|
760
|
+
else throw new Error(`${target} is not a registered project name or an absolute path`);
|
|
761
|
+
}
|
|
762
|
+
roots = save(remove ? roots.filter((root) => !chosen.includes(root)) : [...roots, ...chosen]).exclude;
|
|
763
|
+
}
|
|
764
|
+
const payload = { ok: true, changed: targets.length > 0, exclude: roots };
|
|
765
|
+
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
766
|
+
else {
|
|
767
|
+
console.log(`OpenWiki never refreshes ${roots.length} ${roots.length === 1 ? 'repository' : 'repositories'}${roots.length ? ':' : '.'}`);
|
|
768
|
+
for (const root of roots) console.log(` ${root}`);
|
|
769
|
+
}
|
|
770
|
+
return payload;
|
|
771
|
+
} catch (err) {
|
|
772
|
+
if (json) console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
773
|
+
else console.error(`OpenWiki exclude: ${err.message}`);
|
|
774
|
+
process.exitCode = 1;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
733
778
|
async function autoRefreshCommand(args = []) {
|
|
734
779
|
const forwardArgs = args.includes('--apply') ? [...args] : ['--apply', ...args];
|
|
735
780
|
if (!forwardArgs.some((a) => a === '--budget-ms' || a.startsWith('--budget-ms='))) {
|
|
@@ -760,6 +805,7 @@ IMPLEMENTED.watch = watchCommand;
|
|
|
760
805
|
IMPLEMENTED.propagate = propagateCommand;
|
|
761
806
|
IMPLEMENTED.config = configCommand;
|
|
762
807
|
IMPLEMENTED.schedule = scheduleCommand;
|
|
808
|
+
IMPLEMENTED.exclude = excludeCommand;
|
|
763
809
|
IMPLEMENTED.search = searchCommand;
|
|
764
810
|
IMPLEMENTED.graph = graphCommand;
|
|
765
811
|
IMPLEMENTED.token = tokenCommand;
|
|
@@ -830,7 +876,10 @@ export async function run(argv = process.argv) {
|
|
|
830
876
|
Create missing plugin-owned documentation pages
|
|
831
877
|
refresh [--repo <name>] [--apply] [--force] [--json]
|
|
832
878
|
Report which registered projects' generated pages are stale;
|
|
833
|
-
--apply regenerates stale pages (writes openwiki/ only)
|
|
879
|
+
--apply regenerates stale pages (writes openwiki/ only).
|
|
880
|
+
Never creates a wiki: use generate for that
|
|
881
|
+
exclude [<name|path>...] [--remove] [--json]
|
|
882
|
+
List, add or remove repositories the refresh never touches
|
|
834
883
|
auto-refresh [--repo <name>] [--force] [--json]
|
|
835
884
|
Automated daemon trigger to regenerate stale pages across repositories
|
|
836
885
|
config [show|get <key>|set <key> <value>|reset] [--json]
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tr-plugin-openwiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "tr-plugin-openwiki",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@gregiteen/ssss-cli": "0.14.5"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"total-recall-brain": "3.
|
|
15
|
+
"total-recall-brain": "3.47.0"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"node_modules/@alcalzone/ansi-tokenize": {
|
|
@@ -5529,9 +5529,9 @@
|
|
|
5529
5529
|
}
|
|
5530
5530
|
},
|
|
5531
5531
|
"node_modules/total-recall-brain": {
|
|
5532
|
-
"version": "3.
|
|
5533
|
-
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.
|
|
5534
|
-
"integrity": "sha512-
|
|
5532
|
+
"version": "3.47.0",
|
|
5533
|
+
"resolved": "https://registry.npmjs.org/total-recall-brain/-/total-recall-brain-3.47.0.tgz",
|
|
5534
|
+
"integrity": "sha512-S4UIdbbTyxw7C4+rix+7oHMm/6gYkcxwNBcP6kRDySX40p5IgqVTKgHk1n8rmaKiyYFoIX/HZHlHSjNrHgn0Gw==",
|
|
5535
5535
|
"dev": true,
|
|
5536
5536
|
"license": "MIT",
|
|
5537
5537
|
"workspaces": [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tr-plugin-openwiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-repo knowledge graph and documentation wiki engine for Total Recall",
|
|
6
6
|
"scripts": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"src/",
|
|
19
19
|
"vendor/",
|
|
20
20
|
"capability/",
|
|
21
|
-
"templates/"
|
|
21
|
+
"templates/",
|
|
22
|
+
"skills/"
|
|
22
23
|
],
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@gregiteen/ssss-cli": "0.14.5"
|