total-recall-brain 3.41.4 → 3.41.6
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 +10 -6
- package/docs/reference/cli-reference.md +18 -0
- package/frontend/dist/assets/index-4tGQfTa2.js +5605 -0
- package/frontend/dist/assets/index-DtwsDyFM.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +11 -0
- package/frontend/src/App.tsx +47 -122
- package/frontend/src/components/plugins/plugin-frame.ts +1 -0
- package/frontend/src/index.css +152 -86
- package/frontend/src/pages/HelpPage.tsx +13 -10
- package/frontend/src/pages/MemoryPage.tsx +28 -30
- package/frontend/src/pages/NetworkPage.tsx +3 -3
- package/frontend/src/pages/PluginsPage.tsx +6 -5
- package/frontend/src/pages/RulesPage.tsx +16 -12
- package/package.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +3114 -36
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +3122 -34
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +4 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +3127 -49
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +136 -7
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +135 -26
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +3121 -33
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +228 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +24 -30
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +29 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +73 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +182 -365
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/agy.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/claude.mjs +275 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cline.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/codex.mjs +187 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/cursor.mjs +158 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/devin.mjs +157 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/dsh.mjs +183 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/grok.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +87 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/openclaw.mjs +173 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/pi.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate.mjs +12 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/json-reply.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +72 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-catalog.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/paths.mjs +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +169 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +136 -346
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/ssss-store.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/telemetry.mjs +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +117 -35
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +398 -57
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +304 -46
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +367 -41
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +197 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +153 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +261 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +151 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +114 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +96 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +899 -662
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/openwiki.mjs +8987 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/mermaid-diagrams/SKILL.md +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/vendor/skills/write-connector/SKILL.md +56 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3097 -81
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +4 -1
- package/src/cli/agent.mjs +12 -1
- package/src/cli/compact.mjs +108 -0
- package/src/cli/connect.mjs +56 -1
- package/src/cli/context.mjs +55 -23
- package/src/cli/doctor.mjs +5 -2
- package/src/cli/forget.mjs +2 -0
- package/src/cli/harness.mjs +62 -50
- package/src/cli/memory-entry.mjs +2 -2
- package/src/cli/preflight.mjs +152 -16
- package/src/cli/release.mjs +386 -0
- package/src/cli/remember.mjs +19 -1
- package/src/cli/rules.mjs +54 -12
- package/src/cli/startup.mjs +1 -1
- package/src/cli/status.mjs +12 -2
- package/src/core/claude-hooks.mjs +41 -3
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-semantic-index.mjs +3 -2
- package/src/core/conflict-detector.mjs +23 -12
- package/src/core/context-compactor.mjs +145 -0
- package/src/core/context-policy.mjs +20 -0
- package/src/core/duplicate-guard.mjs +350 -0
- package/src/core/meta-harness.mjs +31 -11
- package/src/core/preflight-gate.mjs +2 -2
- package/src/core/preflight-receipt.mjs +29 -1
- package/src/core/scaffold-plugins.mjs +7 -0
- package/src/core/stdio-flush.mjs +23 -0
- package/src/core/surface.mjs +33 -10
- package/src/server/invoke-durability.mjs +39 -0
- package/src/server/invoke-write-child.mjs +25 -0
- package/src/server/routes/invoke.mjs +7 -0
- package/src/server/routes/plugins.mjs +2 -2
- package/frontend/dist/assets/index-BIQmqDKP.css +0 -1
- package/frontend/dist/assets/index-CRzjQLu7.js +0 -5631
- package/frontend/src/components/DaemonLogsTab.tsx +0 -54
- package/frontend/src/components/ResearchAgendaTab.tsx +0 -712
- package/frontend/src/components/TaskQueueTab.tsx +0 -193
- package/frontend/src/components/VoiceInput.tsx +0 -112
- package/frontend/src/pages/ChatPage.tsx +0 -1059
- package/frontend/src/pages/CollabPage.tsx +0 -470
- package/frontend/src/pages/DesignDocsPage.tsx +0 -621
- package/frontend/src/pages/FilesPage.tsx +0 -393
- package/frontend/src/pages/HealthPage.tsx +0 -359
- package/frontend/src/pages/InboxPage.tsx +0 -83
- package/frontend/src/pages/InstructionsPage.tsx +0 -583
- package/frontend/src/pages/IntegrationsPage.tsx +0 -761
- package/frontend/src/pages/MeshPage.css +0 -58
- package/frontend/src/pages/MeshPage.tsx +0 -1219
- package/frontend/src/pages/OkfPage.tsx +0 -535
- package/frontend/src/pages/SandboxPage.tsx +0 -113
- package/frontend/src/pages/SkillsPage.tsx +0 -1994
- package/frontend/src/pages/TasksPage.tsx +0 -427
- package/frontend/src/pages/UsagePage.tsx +0 -702
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +0 -65
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +0 -76
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +0 -19
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -266
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +0 -446
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +0 -152
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +0 -71
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +0 -35
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +0 -175
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +0 -575
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Harness Multi-Platform Thread Manager
|
|
3
|
+
* Unifies thread tracking and session supervision across integrated agent platforms:
|
|
4
|
+
* agy, claude, codex, dsh, hermes, openclaw, cursor, devin, etc.
|
|
5
|
+
*
|
|
6
|
+
* Persists thread state, PR branch associations, turn history, and review status.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { threadsFile } from './paths.mjs';
|
|
12
|
+
|
|
13
|
+
/** A missing file is an empty store. A corrupt one is an error, so the next write cannot erase it. */
|
|
14
|
+
export function readThreads() {
|
|
15
|
+
let content;
|
|
16
|
+
try { content = fs.readFileSync(threadsFile(), 'utf8'); }
|
|
17
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
18
|
+
let parsed;
|
|
19
|
+
try { parsed = JSON.parse(content); }
|
|
20
|
+
catch { throw new Error(`The thread store ${threadsFile()} is corrupt. It was left in place; repair or move it.`); }
|
|
21
|
+
return Array.isArray(parsed) ? parsed : (parsed.threads || []);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Serialize read-modify-write across processes. A lock older than 30 seconds belongs to a dead process. */
|
|
25
|
+
function withLock(work) {
|
|
26
|
+
const lock = `${threadsFile()}.lock`;
|
|
27
|
+
fs.mkdirSync(path.dirname(lock), { recursive: true, mode: 0o700 });
|
|
28
|
+
const deadline = Date.now() + 10000;
|
|
29
|
+
for (;;) {
|
|
30
|
+
try { fs.mkdirSync(lock); break; }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error.code !== 'EEXIST') throw error;
|
|
33
|
+
try { if (Date.now() - fs.statSync(lock).mtimeMs > 30000) { fs.rmdirSync(lock); continue; } } catch { continue; }
|
|
34
|
+
if (Date.now() > deadline) throw new Error('The thread store is locked by another process');
|
|
35
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
try { return work(); } finally { fs.rmdirSync(lock); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeThreads(threads) {
|
|
42
|
+
const file = threadsFile();
|
|
43
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
44
|
+
const payload = {
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
threads: Array.isArray(threads) ? threads : []
|
|
48
|
+
};
|
|
49
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
50
|
+
fs.writeFileSync(temporary, JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
51
|
+
fs.renameSync(temporary, file);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function listThreads(filter = {}) {
|
|
55
|
+
const threads = readThreads();
|
|
56
|
+
return threads
|
|
57
|
+
.filter(t => {
|
|
58
|
+
if (filter.platform && t.platform !== filter.platform) return false;
|
|
59
|
+
if (filter.status && t.status !== filter.status) return false;
|
|
60
|
+
if (filter.branch && t.branch !== filter.branch) return false;
|
|
61
|
+
return true;
|
|
62
|
+
})
|
|
63
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getThread(id) {
|
|
67
|
+
const threads = readThreads();
|
|
68
|
+
return threads.find(t => t.id === id) || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createThread(data = {}) {
|
|
72
|
+
return withLock(() => {
|
|
73
|
+
const threads = readThreads();
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
const id = data.id || `thread-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
76
|
+
|
|
77
|
+
const thread = {
|
|
78
|
+
id,
|
|
79
|
+
title: String(data.title || 'Untitled Agent Thread').slice(0, 140),
|
|
80
|
+
task: String(data.task || data.title || '').trim(),
|
|
81
|
+
platform: data.platform || 'universal',
|
|
82
|
+
model: data.model || 'auto',
|
|
83
|
+
branch: data.branch || null,
|
|
84
|
+
worktreePath: data.worktreePath || null,
|
|
85
|
+
prNumber: data.prNumber || null,
|
|
86
|
+
prUrl: data.prUrl || null,
|
|
87
|
+
status: data.status || 'active', // 'active' | 'review_pending' | 'approved' | 'completed' | 'failed'
|
|
88
|
+
createdAt: now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
turns: Array.isArray(data.turns) ? data.turns : [],
|
|
91
|
+
review: data.review || null,
|
|
92
|
+
summary: data.summary || null
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
threads.unshift(thread);
|
|
96
|
+
writeThreads(threads);
|
|
97
|
+
return thread;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function appendThreadTurn(id, turn = {}) {
|
|
102
|
+
return withLock(() => {
|
|
103
|
+
const threads = readThreads();
|
|
104
|
+
const index = threads.findIndex(t => t.id === id);
|
|
105
|
+
if (index === -1) throw new Error(`Thread not found: ${id}`);
|
|
106
|
+
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
const entry = {
|
|
109
|
+
role: turn.role || 'agent',
|
|
110
|
+
agent: turn.agent || 'unknown',
|
|
111
|
+
content: String(turn.content || '').slice(0, 50000),
|
|
112
|
+
timestamp: now
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
threads[index].turns.push(entry);
|
|
116
|
+
threads[index].updatedAt = now;
|
|
117
|
+
writeThreads(threads);
|
|
118
|
+
return threads[index];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function updateThread(id, updates = {}) {
|
|
123
|
+
return withLock(() => {
|
|
124
|
+
const threads = readThreads();
|
|
125
|
+
const index = threads.findIndex(t => t.id === id);
|
|
126
|
+
if (index === -1) throw new Error(`Thread not found: ${id}`);
|
|
127
|
+
|
|
128
|
+
const now = new Date().toISOString();
|
|
129
|
+
threads[index] = {
|
|
130
|
+
...threads[index],
|
|
131
|
+
...updates,
|
|
132
|
+
id, // protect immutable ID
|
|
133
|
+
updatedAt: now
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
writeThreads(threads);
|
|
137
|
+
return threads[index];
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function closeThread(id, resolution = 'completed') {
|
|
142
|
+
return updateThread(id, {
|
|
143
|
+
status: resolution,
|
|
144
|
+
closedAt: new Date().toISOString()
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function deleteThread(id) {
|
|
149
|
+
return withLock(() => {
|
|
150
|
+
const threads = readThreads();
|
|
151
|
+
const filtered = threads.filter(t => t.id !== id);
|
|
152
|
+
if (filtered.length === threads.length) return false;
|
|
153
|
+
writeThreads(filtered);
|
|
154
|
+
return true;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -4,24 +4,34 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
|
|
7
|
+
/** These adapters can be told not to write. Any other runs read-only work only inside a disposable checkout. */
|
|
8
|
+
export const READ_ONLY_ADAPTERS = ['codex', 'claude', 'agy', 'muse', 'pi'];
|
|
9
|
+
|
|
10
|
+
/** The harness or node could not start the work. This says nothing about the quality of the harness. */
|
|
11
|
+
export function notReady(message) {
|
|
12
|
+
const error = new Error(message);
|
|
13
|
+
error.code = 'NOT_READY';
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
export const ADAPTERS = [
|
|
8
18
|
{ id: 'claude', name: 'Claude Code', binary: 'claude', args: ['--output-format', 'json', '--permission-mode', 'acceptEdits', '-p'], input: 'stdin', auth: ['auth', 'status'], docs: 'https://code.claude.com/docs/en/headless' },
|
|
9
19
|
{ id: 'codex', name: 'Codex', binary: 'codex', args: ['exec', '--sandbox', 'workspace-write', '--json', '-'], input: 'stdin', auth: ['login', 'status'], docs: 'https://developers.openai.com/codex/noninteractive' },
|
|
10
20
|
{ id: 'agy', name: 'Antigravity', binary: 'agy', args: ['--mode', 'accept-edits', '--sandbox', '--output-format', 'json', '-p'], input: 'argument', docs: 'https://antigravity.google/docs/cli/reference' },
|
|
11
21
|
{ id: 'cursor', name: 'Cursor', binary: 'cursor-agent', args: ['--print', '--output-format', 'json'], input: 'argument', auth: ['status'], docs: 'https://docs.cursor.com/en/cli/reference/parameters' },
|
|
12
|
-
{ id: 'dsh', name: 'DSH', binary: 'dsh', args: ['--profile', 'headless', '--json', '-'], input: 'stdin', docs: 'https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md' },
|
|
22
|
+
{ id: 'dsh', name: 'DSH', binary: 'dsh', args: ['--profile', 'headless', '--json', '-'], input: 'stdin', credentials: { OPENROUTER_API_KEY: ['DEVELOPER_OPENROUTER_API_KEY', 'OPENROUTER_API_KEY'] }, docs: 'https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/README.md' },
|
|
13
23
|
{ id: 'grok', name: 'Grok', binary: 'grok', args: ['--output-format', 'json', '-p'], input: 'argument', docs: 'https://docs.x.ai/build/cli/headless-scripting' },
|
|
14
24
|
{ id: 'muse', name: 'Muse', binary: 'muse', args: ['exec', '--json'], input: 'file', docs: 'https://dev.meta.ai/docs/muse-code/' },
|
|
15
|
-
{ id: 'pi', name: 'Pi', binary: 'pi', args: ['--print', '--mode', 'json', '--
|
|
25
|
+
{ id: 'pi', name: 'Pi', binary: 'pi', args: ['--print', '--mode', 'json', '--provider', 'openrouter', '--tools', 'read,bash', '--no-extensions'], input: 'stdin', credentials: { OPENROUTER_API_KEY: ['DEVELOPER_OPENROUTER_API_KEY', 'OPENROUTER_API_KEY'] }, docs: 'https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/docs/cli.md' },
|
|
16
26
|
{ 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' }
|
|
17
27
|
];
|
|
18
28
|
|
|
19
29
|
export const EXTENDED_ADAPTERS = [
|
|
20
|
-
{ id: 'ollama', name: 'Ollama', binary: 'ollama', args: ['run'
|
|
30
|
+
{ id: 'ollama', name: 'Ollama', binary: 'ollama', args: ['run'], input: 'stdin', docs: 'https://ollama.com/library' },
|
|
21
31
|
{ id: 'openclaw', name: 'OpenClaw', binary: 'openclaw', args: ['agent', '--message'], input: 'argument', docs: 'https://openclaw.ai/docs' },
|
|
22
32
|
{ id: 'aider', name: 'Aider', binary: 'aider', args: ['--yes', '--message'], input: 'argument', docs: 'https://aider.chat/docs' },
|
|
23
33
|
{ id: 'goose', name: 'Goose', binary: 'goose', args: ['run', '--text'], input: 'argument', docs: 'https://block.github.io/goose/' },
|
|
24
|
-
{ id: 'openhands', name: 'OpenHands', binary: 'openhands', args: ['run'], input: 'argument', docs: 'https://docs.
|
|
34
|
+
{ id: 'openhands', name: 'OpenHands', binary: 'openhands', args: ['run'], input: 'argument', docs: 'https://docs.openhands.dev/' },
|
|
25
35
|
{ id: 'cline', name: 'Cline', binary: 'cline', args: ['task', '--json'], input: 'argument', docs: 'https://cline.bot/docs' }
|
|
26
36
|
];
|
|
27
37
|
|
|
@@ -72,6 +82,7 @@ export function getActiveAdapters() {
|
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
export function agentEnvironment(source = process.env) {
|
|
85
|
+
// Provider keys are never ambient. An adapter that needs one declares it under `credentials`.
|
|
75
86
|
const allowed = ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'TMPDIR', 'TMP', 'TEMP', 'LANG', 'LC_ALL', 'TERM', 'SYSTEMROOT', 'SystemRoot', 'APPDATA', 'LOCALAPPDATA', 'USERPROFILE', 'CODEX_HOME', 'CLAUDE_CONFIG_DIR', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'DSH_HOME', 'HERMES_HOME', 'PI_CODING_AGENT_DIR'];
|
|
76
87
|
return Object.fromEntries(allowed.filter(key => typeof source[key] === 'string').map(key => [key, source[key]]));
|
|
77
88
|
}
|
|
@@ -117,13 +128,54 @@ export function runProcess(binary, args, { cwd, input = '', timeoutMs = 300000,
|
|
|
117
128
|
});
|
|
118
129
|
}
|
|
119
130
|
|
|
131
|
+
const frameReason = frame => frame.finished_reason || frame.status || frame.type || null;
|
|
132
|
+
/** Turn "Resets in 119h40m44s" or "try again at 6:24 PM" into an instant. Returns null when no time is stated. */
|
|
133
|
+
export function parseResetTime(text, now = new Date()) {
|
|
134
|
+
const span = String(text).match(/resets? in\s+(?:(\d+)h)?\s*(?:(\d+)m)?\s*(?:(\d+)s)?/i);
|
|
135
|
+
if (span && (span[1] || span[2] || span[3])) return new Date(now.getTime() + ((+span[1] || 0) * 3600 + (+span[2] || 0) * 60 + (+span[3] || 0)) * 1000).toISOString();
|
|
136
|
+
const clock = String(text).match(/try again at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
|
|
137
|
+
if (clock) {
|
|
138
|
+
const at = new Date(now); at.setSeconds(0, 0);
|
|
139
|
+
at.setHours((+clock[1] % 12) + (/pm/i.test(clock[3]) ? 12 : 0), +clock[2] || 0);
|
|
140
|
+
if (at <= now) at.setDate(at.getDate() + 1);
|
|
141
|
+
return at.toISOString();
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
export function classifyFailure(message, now) {
|
|
146
|
+
const text = String(message || '');
|
|
147
|
+
if (/resource_exhausted|quota|usage limit|rate.?limit|too many requests|\b429\b|out of credits|insufficient credits/i.test(text)) return { failureKind: 'quota', resetsAt: parseResetTime(text, now) };
|
|
148
|
+
if (/not logged in|not authenticated|unauthenticated|please (log|sign) in|\b401\b|invalid api key|missing_credential|no api key/i.test(text)) return { failureKind: 'auth' };
|
|
149
|
+
return { failureKind: 'agent' };
|
|
150
|
+
}
|
|
151
|
+
function frameError(frame) {
|
|
152
|
+
const value = frame.error ?? (frame.is_error ? frame.result : undefined) ?? (frame.type === 'error' ? frame.message : undefined);
|
|
153
|
+
return typeof value === 'string' ? value : value?.message || null;
|
|
154
|
+
}
|
|
155
|
+
function agentFailure(message, stderr = '') {
|
|
156
|
+
const error = String(message || stderr || 'Agent reported failure').trim().slice(0, 500);
|
|
157
|
+
return { ok: false, error, ...classifyFailure(`${error}\n${stderr}`) };
|
|
158
|
+
}
|
|
159
|
+
const PLAIN_TEXT_ADAPTERS = ['ollama', 'openclaw', 'aider', 'goose', 'openhands', 'cline'];
|
|
120
160
|
export function parseAgentResult(id, result) {
|
|
121
|
-
|
|
161
|
+
const stderr = String(result.stderr || '').trim().slice(-2000);
|
|
162
|
+
if (result.reason) return { ok: false, error: result.reason, failureKind: 'process' };
|
|
163
|
+
if (PLAIN_TEXT_ADAPTERS.includes(id) || loadCustomAdapters().some(c => c.id === id)) {
|
|
164
|
+
if (result.exitCode !== 0) return agentFailure(null, stderr || `Agent process exited ${result.exitCode}`);
|
|
165
|
+
const text = String(result.stdout || '').trim();
|
|
166
|
+
return text ? { ok: true, response: text, commandExecutionVerified: false } : { ok: false, error: 'Agent returned no result', failureKind: 'agent' };
|
|
167
|
+
}
|
|
122
168
|
let frames;
|
|
123
169
|
try { const value = JSON.parse(result.stdout); frames = Array.isArray(value) ? value : [value]; }
|
|
124
|
-
catch {
|
|
125
|
-
|
|
126
|
-
|
|
170
|
+
catch {
|
|
171
|
+
try { frames = String(result.stdout).trim().split('\n').filter(Boolean).map(line => JSON.parse(line)); }
|
|
172
|
+
catch { return result.exitCode === 0 ? { ok: false, error: 'Agent output is not valid structured data', failureKind: 'agent' } : agentFailure(null, stderr || `Agent process exited ${result.exitCode}`); }
|
|
173
|
+
}
|
|
174
|
+
frames = (frames || []).filter(frame => frame && typeof frame === 'object');
|
|
175
|
+
const failed = frames.find(frame => frameError(frame) || frame.is_error || ['error', 'turn.failed'].includes(frame.type) || ['FAILED', 'ERROR', 'CANCELED', 'WAITING'].includes(frame.status) || ['print_timeout', 'interrupted', 'error'].includes(frame.finished_reason));
|
|
176
|
+
if (failed) return agentFailure(frameError(failed) || frameReason(failed), stderr);
|
|
177
|
+
if (result.exitCode !== 0) return agentFailure(null, stderr || `Agent process exited ${result.exitCode}`);
|
|
178
|
+
if (!frames.length) return { ok: false, error: 'Agent returned no result', failureKind: 'agent' };
|
|
127
179
|
const last = frames.at(-1);
|
|
128
180
|
let text;
|
|
129
181
|
if (id === 'codex' && last.type === 'turn.completed') text = frames.filter(frame => frame.type === 'item.completed' && frame.item?.type === 'agent_message').map(frame => frame.item.text).join('\n');
|
|
@@ -136,59 +188,73 @@ export function parseAgentResult(id, result) {
|
|
|
136
188
|
if (messages.some(message => ['error', 'aborted'].includes(message.stopReason))) return { ok: false, error: 'Pi turn failed' };
|
|
137
189
|
text = messages.filter(message => message.role === 'assistant').flatMap(message => message.content || []).filter(part => part.type === 'text').map(part => part.text).join('\n');
|
|
138
190
|
}
|
|
139
|
-
if (['ollama', 'openclaw', 'aider', 'goose', 'openhands', 'cline'].includes(id) || loadCustomAdapters().some(c => c.id === id)) {
|
|
140
|
-
text = result.stdout.trim();
|
|
141
|
-
}
|
|
142
191
|
// The official headless contract documents JSONL plus exit status, not MSP
|
|
143
192
|
// notifications. Preserve the actual log rather than invent a headless schema.
|
|
144
193
|
// Exit zero certifies a finished turn only; it never certifies the requested work.
|
|
145
194
|
if (id === 'muse') return { ok: true, response: result.stdout.trim(), events: frames, completionEvidence: 'headless-process-exit', commandExecutionVerified: false };
|
|
146
|
-
return typeof text === 'string' && text.trim() ? { ok: true, response: text, commandExecutionVerified: false } : { ok: false, error: 'Agent did not produce a recognized completed result' };
|
|
195
|
+
return typeof text === 'string' && text.trim() ? { ok: true, response: text, commandExecutionVerified: false } : { ok: false, error: 'Agent did not produce a recognized completed result', failureKind: 'agent' };
|
|
147
196
|
}
|
|
148
197
|
|
|
149
198
|
/** Rebuild the instruction payload from the selected node/workspace, never a cached prompt. */
|
|
150
199
|
export async function totalRecallBootstrap(request, { probe = runProcess, env = process.env } = {}) {
|
|
151
200
|
const binary = findBinary('total-recall', env);
|
|
152
|
-
if (!binary) throw
|
|
201
|
+
if (!binary) throw notReady('Total Recall is not installed on the selected node');
|
|
202
|
+
// Rules come from the operator's project, even when the agent itself works in a temporary worktree.
|
|
203
|
+
const cwd = request.contextCwd || request.cwd;
|
|
153
204
|
const invoke = async args => {
|
|
154
|
-
const result = await probe(binary, args, { cwd
|
|
155
|
-
if (result.reason || result.exitCode !== 0)
|
|
205
|
+
const result = await probe(binary, args, { cwd, timeoutMs: 120000, maxBytes: 4 * 1024 * 1024, env: agentEnvironment(env) });
|
|
206
|
+
if (result.reason || result.exitCode !== 0) {
|
|
207
|
+
let detail = String(result.stderr || '').trim();
|
|
208
|
+
try { detail = JSON.parse(result.stdout).problems?.join('; ') || detail; } catch { /* plain output */ }
|
|
209
|
+
throw notReady(`Total Recall ${args[0]} failed in ${cwd}, so dispatch was refused${detail ? `: ${detail.slice(0, 300)}` : ''}`);
|
|
210
|
+
}
|
|
156
211
|
return result.stdout;
|
|
157
212
|
};
|
|
158
213
|
const actions = request.permissions?.write ? 'read,edit,test' : 'read';
|
|
159
|
-
|
|
160
|
-
const capsule = JSON.parse(await invoke(['context',
|
|
161
|
-
if (capsule.ready !== true || typeof capsule.context !== 'string') throw
|
|
214
|
+
const task = String(request.contextTask || request.prompt).slice(0, 2000);
|
|
215
|
+
const capsule = JSON.parse(await invoke(['context', task, '--action', actions, '--budget', '24000', '--format', 'json']));
|
|
216
|
+
if (capsule.ready !== true || typeof capsule.context !== 'string') throw notReady('Required Total Recall context is not ready');
|
|
217
|
+
await invoke(['preflight', 'ack', '--task', task, '--action', actions, '--json']);
|
|
162
218
|
const help = await invoke(['--help']);
|
|
163
219
|
const plugins = JSON.parse(await invoke(['plugin', 'library', '--json']));
|
|
164
220
|
const commands = JSON.parse(await invoke(['composable-cli', 'library', '--json']));
|
|
165
|
-
const docs = JSON.parse(await invoke(['
|
|
166
|
-
if (docs.ok !== true) throw
|
|
221
|
+
const docs = JSON.parse(await invoke(['meta-harness', 'docs-ensure', '--agent', request.agent, '--json']));
|
|
222
|
+
if (docs.ok !== true) throw notReady('Official agent documentation is unavailable or stale');
|
|
167
223
|
return `Requester: the operator of this Total Recall workspace.\nUse Total Recall and its owning plugin CLI for this task. The following context and command inventories were obtained automatically on this node before dispatch. Official documentation is cached at ${docs.directory}. Read the applicable cached references before changing an integration.\n\nTask context:\n${capsule.context}\n\nComplete CLI help:\n${help}\n\nInstalled plugin command library:\n${JSON.stringify(plugins)}\n\nComposed command library:\n${JSON.stringify(commands)}\n\nRequested task:\n${request.prompt}`;
|
|
168
224
|
}
|
|
169
225
|
|
|
226
|
+
/** Authentication is `true` or `false` only when a real probe says so. Everything else stays 'unknown' until a run proves it. */
|
|
170
227
|
export async function inspectAgents({ probe = runProcess, env = process.env, ids } = {}) {
|
|
171
228
|
const agents = [];
|
|
172
229
|
for (const adapter of getActiveAdapters()) {
|
|
173
230
|
if (ids && !ids.includes(adapter.id)) continue;
|
|
174
231
|
const binary = findBinary(adapter.binary, env);
|
|
175
|
-
const item = { id: adapter.id, name: adapter.name, installed: Boolean(binary), authenticated: 'unknown', executionVerified: false, docs: adapter.docs };
|
|
232
|
+
const item = { id: adapter.id, name: adapter.name, installed: Boolean(binary), authenticated: binary ? 'unknown' : false, executionVerified: false, docs: adapter.docs };
|
|
176
233
|
if (binary) {
|
|
177
234
|
const version = await probe(binary, ['--version'], { timeoutMs: 8000 });
|
|
178
235
|
item.version = version.exitCode === 0 ? (version.stdout || version.stderr).match(/\b\d+\.\d+(?:\.\d+)?\b/)?.[0] || null : null;
|
|
179
236
|
item.versionVerified = Boolean(item.version);
|
|
180
|
-
if (adapter.
|
|
181
|
-
const auth = await probe(binary,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
237
|
+
if (adapter.id === 'codex') {
|
|
238
|
+
const auth = await probe(binary, ['login', 'status'], { timeoutMs: 15000 });
|
|
239
|
+
item.authenticated = auth.exitCode === 0 && /Logged in using ChatGPT/i.test(auth.stdout + auth.stderr);
|
|
240
|
+
} else if (adapter.id === 'claude') {
|
|
241
|
+
const auth = await probe(binary, ['auth', 'status'], { timeoutMs: 15000 });
|
|
242
|
+
try { const value = JSON.parse(auth.stdout); item.authenticated = auth.exitCode === 0 && value.loggedIn === true && value.authMethod === 'claude.ai' && value.apiProvider === 'firstParty' && !value.apiKeySource && Boolean(value.subscriptionType); } catch { item.authenticated = false; }
|
|
243
|
+
} else if (adapter.id === 'cursor') {
|
|
244
|
+
const auth = await probe(binary, ['status'], { timeoutMs: 15000 });
|
|
245
|
+
item.authenticated = auth.exitCode === 0 && /logged in|authenticated/i.test(auth.stdout) && !/not authenticated|not logged in/i.test(auth.stdout);
|
|
246
|
+
} else if (adapter.id === 'ollama') {
|
|
247
|
+
const list = await probe(binary, ['list'], { timeoutMs: 5000 });
|
|
248
|
+
item.authenticated = list.exitCode === 0;
|
|
249
|
+
// Embedding models cannot take a task. Without a chat model this runtime cannot be dispatched.
|
|
250
|
+
const names = String(list.stdout || '').split('\n').slice(1).map(line => line.trim().split(/\s+/)[0]).filter(Boolean);
|
|
251
|
+
item.chatModels = names.filter(name => !/embed|bge-|minilm|rerank/i.test(name));
|
|
252
|
+
if (!item.chatModels.length) item.blockedBy = 'no-chat-model';
|
|
187
253
|
}
|
|
188
254
|
}
|
|
189
255
|
agents.push(item);
|
|
190
256
|
}
|
|
191
|
-
return { agents, evidence: '
|
|
257
|
+
return { agents, evidence: 'Version and login probes only; actual execution requires a completed run' };
|
|
192
258
|
}
|
|
193
259
|
|
|
194
260
|
export async function dispatchAgent(request, { probe = runProcess, env = process.env, bootstrap = totalRecallBootstrap } = {}) {
|
|
@@ -196,26 +262,42 @@ export async function dispatchAgent(request, { probe = runProcess, env = process
|
|
|
196
262
|
if (!adapter || !path.isAbsolute(request.cwd || '') || typeof request.prompt !== 'string' || !request.prompt.trim() || Buffer.byteLength(request.prompt) > 65536) throw new Error('Explicit supported agent, absolute workspace and bounded prompt required');
|
|
197
263
|
if (!fs.statSync(request.cwd).isDirectory()) throw new Error('Selected workspace is not a directory');
|
|
198
264
|
const binary = findBinary(adapter.binary, env);
|
|
199
|
-
if (!binary) throw
|
|
265
|
+
if (!binary) throw notReady(`${adapter.name} is not installed on the selected node`);
|
|
200
266
|
const inspected = await inspectAgents({ probe, env, ids: [adapter.id] });
|
|
201
267
|
const state = inspected.agents.find(item => item.id === adapter.id);
|
|
202
|
-
if (!state.versionVerified
|
|
203
|
-
if (state.authenticated ===
|
|
268
|
+
if (!state.versionVerified) throw notReady(`${adapter.name} did not report a version`);
|
|
269
|
+
if (state.authenticated === false) throw notReady(`${adapter.name} is not signed in on this node`);
|
|
270
|
+
if (state.blockedBy === 'no-chat-model') throw notReady(`${adapter.name} has no chat model installed on this node`);
|
|
271
|
+
if (state.authenticated === 'unknown' && request.allowUnknownAuth !== true) throw notReady(`${adapter.name} has no login probe; select it explicitly to verify it through execution`);
|
|
272
|
+
if (request.permissions?.write !== true && !READ_ONLY_ADAPTERS.includes(adapter.id) && request.isolated !== true) throw notReady(`${adapter.name} has no read-only mode, so a read-only run must use a disposable checkout`);
|
|
204
273
|
const prompt = await bootstrap(request, { probe, env });
|
|
274
|
+
const credentials = {};
|
|
275
|
+
for (const [name, candidates] of Object.entries(adapter.credentials || {})) {
|
|
276
|
+
const auth = findBinary('total-recall', env);
|
|
277
|
+
let value = null;
|
|
278
|
+
for (const secret of candidates) {
|
|
279
|
+
const result = auth ? await probe(auth, ['auth', 'secret', 'get', secret], { timeoutMs: 15000, env }) : { exitCode: 1 };
|
|
280
|
+
if (result.exitCode === 0 && String(result.stdout).trim()) { value = String(result.stdout).trim(); break; }
|
|
281
|
+
}
|
|
282
|
+
if (!value) throw notReady(`${adapter.name} needs ${name}, and the auth store has none of: ${candidates.join(', ')}`);
|
|
283
|
+
credentials[name] = value;
|
|
284
|
+
}
|
|
205
285
|
const args = [...adapter.args];
|
|
206
286
|
const allowWrites = request.permissions?.write === true;
|
|
207
287
|
if (adapter.id === 'codex') args[args.indexOf('--sandbox') + 1] = allowWrites ? 'workspace-write' : 'read-only';
|
|
208
288
|
if (adapter.id === 'claude') args[args.indexOf('--permission-mode') + 1] = allowWrites ? 'acceptEdits' : 'plan';
|
|
209
289
|
if (adapter.id === 'agy') args[args.indexOf('--mode') + 1] = allowWrites ? 'accept-edits' : 'plan';
|
|
210
290
|
if (adapter.id === 'muse' && !allowWrites) args.push('--disable-write');
|
|
291
|
+
if (adapter.id === 'pi') args[args.indexOf('--tools') + 1] = allowWrites ? 'read,bash,edit,write' : 'read';
|
|
292
|
+
if (adapter.id === 'ollama') args.push(env.TR_OLLAMA_MODEL || state.chatModels[0]);
|
|
211
293
|
let input = adapter.input === 'stdin' ? prompt : '', temporary;
|
|
212
294
|
if (adapter.input === 'argument' || adapter.input === 'file') {
|
|
213
|
-
temporary = fs.mkdtempSync(path.join(os.tmpdir(), '
|
|
295
|
+
temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'meta-harness-prompt-'));
|
|
214
296
|
const file = path.join(temporary, 'prompt.txt'); fs.writeFileSync(file, prompt, { mode: 0o600 });
|
|
215
297
|
if (adapter.input === 'file') args.push('--prompt-file', file);
|
|
216
298
|
else args.push(`Read ${file} completely before taking action. It contains the required Total Recall task context, full CLI/plugin/composed-command inventories, official documentation location and the requested task. Carry out that task using the supplied context.`);
|
|
217
299
|
}
|
|
218
|
-
try { return { agent: adapter.id, ...parseAgentResult(adapter.id, await probe(binary, args, { cwd: request.cwd, input, timeoutMs:
|
|
300
|
+
try { return { agent: adapter.id, ...parseAgentResult(adapter.id, await probe(binary, args, { cwd: request.cwd, input, timeoutMs: request.timeoutMs || 900000, env: { ...agentEnvironment(env), ...credentials } })) }; }
|
|
219
301
|
finally { if (temporary) fs.rmSync(temporary, { recursive: true, force: true }); }
|
|
220
302
|
}
|
|
221
303
|
|
|
@@ -229,5 +311,5 @@ export async function runWorker(request) {
|
|
|
229
311
|
const result = await runProcess(binary, request.argv.slice(1), { cwd: request.cwd });
|
|
230
312
|
return { ok: result.exitCode === 0 && !result.reason, output: result.stdout, error: result.reason || (result.exitCode ? 'CLI command failed' : undefined), exitCode: result.exitCode, commandExecutionVerified: result.exitCode === 0 && !result.reason };
|
|
231
313
|
}
|
|
232
|
-
throw new Error('Unknown
|
|
314
|
+
throw new Error('Unknown meta-harness worker operation');
|
|
233
315
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Isolated git checkouts for writing and verifying agents. Nothing here merges into the operator's branch. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const SECRET_FILE = /^(\.env(\..*)?|.*\.(pem|key|p12|pfx)|secrets\.enc|id_(rsa|ed25519|ecdsa)|credentials(\.json)?)$/i;
|
|
8
|
+
const git = (cwd, args, timeout = 30000) => spawnSync('git', args, { cwd, encoding: 'utf8', timeout, maxBuffer: 64 * 1024 * 1024 });
|
|
9
|
+
function must(cwd, args, what) {
|
|
10
|
+
const result = git(cwd, args);
|
|
11
|
+
if (result.status !== 0) throw new Error(`${what} failed: ${String(result.stderr || result.error || '').trim().slice(0, 300)}`);
|
|
12
|
+
return result.stdout.trim();
|
|
13
|
+
}
|
|
14
|
+
export const isGitRepo = cwd => git(cwd, ['rev-parse', '--is-inside-work-tree']).status === 0;
|
|
15
|
+
export const headCommit = cwd => must(cwd, ['rev-parse', 'HEAD'], 'Reading HEAD');
|
|
16
|
+
export const isDirty = cwd => git(cwd, ['status', '--porcelain']).stdout.trim().length > 0;
|
|
17
|
+
export const diffBetween = (cwd, base, head) => must(cwd, ['diff', `${base}...${head}`], 'Reading the diff');
|
|
18
|
+
|
|
19
|
+
/** `branch` creates a new branch from `ref`. Without it, the checkout is detached at `ref` (read-only use). */
|
|
20
|
+
export function createWorktree(cwd, { branch, ref = 'HEAD', prefix = 'meta-harness-' } = {}) {
|
|
21
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
22
|
+
const args = branch ? ['worktree', 'add', '-b', branch, directory, ref] : ['worktree', 'add', '--detach', directory, ref];
|
|
23
|
+
const result = git(cwd, args, 120000);
|
|
24
|
+
if (result.status !== 0) {
|
|
25
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
26
|
+
throw new Error(`Creating a worktree failed: ${String(result.stderr).trim().slice(0, 300)}`);
|
|
27
|
+
}
|
|
28
|
+
return directory;
|
|
29
|
+
}
|
|
30
|
+
/** Gates need dependencies. Link them only for the gate, after the agent has finished, so no agent can write through the link. */
|
|
31
|
+
export function linkDependencies(cwd, directory) {
|
|
32
|
+
const modules = path.join(cwd, 'node_modules');
|
|
33
|
+
if (fs.existsSync(modules) && !fs.existsSync(path.join(directory, 'node_modules'))) fs.symlinkSync(modules, path.join(directory, 'node_modules'));
|
|
34
|
+
}
|
|
35
|
+
export const deleteBranch = (cwd, branch) => git(cwd, ['branch', '-D', branch]).status === 0;
|
|
36
|
+
export function removeWorktree(cwd, directory) {
|
|
37
|
+
if (!directory) return;
|
|
38
|
+
try { if (fs.lstatSync(path.join(directory, 'node_modules')).isSymbolicLink()) fs.rmSync(path.join(directory, 'node_modules')); } catch { /* no link */ }
|
|
39
|
+
git(cwd, ['worktree', 'remove', '--force', directory], 60000);
|
|
40
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
/** Commit whatever the agent left. Returns the commit, or null when it changed nothing. */
|
|
43
|
+
export function commitAll(directory, message) {
|
|
44
|
+
must(directory, ['add', '-A', '--', '.', ':!node_modules'], 'Staging agent changes');
|
|
45
|
+
if (git(directory, ['diff', '--cached', '--quiet']).status === 0) return null;
|
|
46
|
+
const staged = git(directory, ['diff', '--cached', '--name-only']).stdout.split('\n').filter(Boolean);
|
|
47
|
+
const secret = staged.filter(file => SECRET_FILE.test(path.basename(file)));
|
|
48
|
+
if (secret.length) throw new Error(`Refusing to commit files that look like credentials: ${secret.join(', ')}`);
|
|
49
|
+
must(directory, ['commit', '-m', message], 'Committing agent changes');
|
|
50
|
+
return headCommit(directory);
|
|
51
|
+
}
|