@ours.network/fleet 0.9.3 → 0.9.5
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/README.md +52 -30
- package/dist/cli.js +153 -15
- package/dist/config.d.ts +24 -0
- package/dist/config.js +79 -1
- package/dist/docs.d.ts +7 -0
- package/dist/docs.js +177 -0
- package/dist/doctor.js +50 -6
- package/dist/harness/acp-agent.d.ts +11 -0
- package/dist/harness/acp-agent.js +27 -0
- package/dist/harness/claude-code.js +31 -1
- package/dist/harness/codex.js +40 -2
- package/dist/harness/types.d.ts +12 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/monitor.d.ts +35 -5
- package/dist/monitor.js +187 -33
- package/dist/runner.js +73 -18
- package/dist/session/acp.d.ts +49 -0
- package/dist/session/acp.js +280 -0
- package/dist/session/control.d.ts +41 -0
- package/dist/session/control.js +218 -0
- package/dist/session/events.d.ts +14 -0
- package/dist/session/events.js +67 -0
- package/dist/session/tmux.d.ts +20 -0
- package/dist/session/tmux.js +46 -0
- package/dist/session/types.d.ts +47 -0
- package/dist/session/types.js +1 -0
- package/dist/spawn.d.ts +5 -0
- package/dist/spawn.js +24 -1
- package/package.json +7 -2
package/dist/docs.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, AI-friendly CLI and configuration reference.
|
|
3
|
+
*
|
|
4
|
+
* Keep this concise enough to place directly in an agent context. Unlike
|
|
5
|
+
* Commander's per-command help, this describes how the pieces compose.
|
|
6
|
+
*/
|
|
7
|
+
export const AI_DOCS = `# ours-fleet reference
|
|
8
|
+
|
|
9
|
+
ours-fleet runs persistent or temporary, identity-bound AI roles. A role selects
|
|
10
|
+
a harness independently from its session backend:
|
|
11
|
+
|
|
12
|
+
- harness: \`claude-code\` or \`codex\`
|
|
13
|
+
- session: \`tmux\` (default) or \`acp\`
|
|
14
|
+
- lifetime: permanent (supervised, restartable) or \`spawn --temp\`
|
|
15
|
+
|
|
16
|
+
## Discover and validate
|
|
17
|
+
|
|
18
|
+
\`\`\`sh
|
|
19
|
+
ours-fleet docs # this complete reference (\`man\` is an alias)
|
|
20
|
+
ours-fleet help <command> # exact flags for one command
|
|
21
|
+
ours-fleet config [-c FILE] # validate and print the merged plan; no changes
|
|
22
|
+
ours-fleet doctor [-c FILE] [--harness codex|claude-code]
|
|
23
|
+
\`\`\`
|
|
24
|
+
|
|
25
|
+
Default configuration is \`~/fleet.yaml\` plus sorted \`~/fleet.d/*.yaml\` role
|
|
26
|
+
drop-ins. An explicit \`-c FILE\` replaces \`~/fleet.yaml\`; fleet.d still adds
|
|
27
|
+
roles. Validate with \`config\` and \`doctor\` before starting or restarting.
|
|
28
|
+
|
|
29
|
+
## Lifecycle and console commands
|
|
30
|
+
|
|
31
|
+
\`\`\`sh
|
|
32
|
+
ours-fleet init
|
|
33
|
+
ours-fleet up|down [Name...]
|
|
34
|
+
ours-fleet restart [Name...] # preserve/resume harness context
|
|
35
|
+
ours-fleet force-restart [Name...] # fresh context; briefing is reloaded
|
|
36
|
+
ours-fleet ls
|
|
37
|
+
ours-fleet status|peek|attach|logs Name
|
|
38
|
+
ours-fleet logs -f Name
|
|
39
|
+
ours-fleet send Name "prompt"
|
|
40
|
+
ours-fleet send Name --key Enter # tmux only
|
|
41
|
+
ours-fleet rm Name
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
\`peek\`, \`attach\`, and text \`send\` work with tmux and ACP. ACP attachment
|
|
45
|
+
also accepts \`/permit <permission-id> <option-id>\`, \`/interrupt\`, and
|
|
46
|
+
\`/detach\`. Raw \`--key\` input is tmux-only.
|
|
47
|
+
|
|
48
|
+
## Spawn
|
|
49
|
+
|
|
50
|
+
\`\`\`sh
|
|
51
|
+
ours-fleet spawn [--temp] Name \\
|
|
52
|
+
--harness codex|claude-code --session tmux|acp \\
|
|
53
|
+
--mission "one line" --cwd /absolute/path --identity Identity \\
|
|
54
|
+
--coordinator Coordinator --model MODEL \\
|
|
55
|
+
--approval ask|allow|deny \\
|
|
56
|
+
--filesystem read-only|workspace|unrestricted \\
|
|
57
|
+
--unattended deny|wait \\
|
|
58
|
+
--bio-file /path/bio.md --persona-file /path/persona.md
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
Permanent spawn writes \`~/fleet.d/Name.yaml\` and starts a supervised role.
|
|
62
|
+
\`--temp\` writes ephemeral state, starts a detached supervisor, and removes the
|
|
63
|
+
role after exit/reboot. Both lifetimes support \`--session acp\`.
|
|
64
|
+
|
|
65
|
+
Codex-specific spawn flags: \`--sandbox\`, \`--permission-mode\`, \`--launcher\`,
|
|
66
|
+
\`--profile\`, \`--search\`, repeatable \`--codex-config key=value\`, repeatable
|
|
67
|
+
\`--add-dir\`, and \`--monitor\`. Run \`ours-fleet help spawn\` for exact values.
|
|
68
|
+
|
|
69
|
+
## fleet.yaml
|
|
70
|
+
|
|
71
|
+
\`\`\`yaml
|
|
72
|
+
vars:
|
|
73
|
+
work_root: /home/me/work
|
|
74
|
+
start_stagger_ms: 0
|
|
75
|
+
defaults:
|
|
76
|
+
harness: codex
|
|
77
|
+
session: acp
|
|
78
|
+
model: gpt-model-id
|
|
79
|
+
permissions:
|
|
80
|
+
approval: ask
|
|
81
|
+
filesystem: workspace
|
|
82
|
+
unattended: deny
|
|
83
|
+
monitor:
|
|
84
|
+
enabled: true
|
|
85
|
+
roles:
|
|
86
|
+
Coordinator:
|
|
87
|
+
harness: codex
|
|
88
|
+
session: acp
|
|
89
|
+
identity: Coordinator
|
|
90
|
+
cwd: \${work_root}/project
|
|
91
|
+
mission: Coordinate work and delegate implementation.
|
|
92
|
+
model: gpt-model-id
|
|
93
|
+
permissions:
|
|
94
|
+
approval: ask
|
|
95
|
+
filesystem: workspace
|
|
96
|
+
unattended: deny
|
|
97
|
+
session_options: # advanced overrides; normally omit
|
|
98
|
+
# acp:
|
|
99
|
+
# command: [/custom/codex-acp, --flag]
|
|
100
|
+
tmux:
|
|
101
|
+
boot_grace_ms: 10000
|
|
102
|
+
monitor:
|
|
103
|
+
enabled: true
|
|
104
|
+
wake_sources: [message_received, file_received, local_contact_request, pending_message]
|
|
105
|
+
batch_ms: 2000
|
|
106
|
+
inject: notification
|
|
107
|
+
turn_fail_threshold: 3
|
|
108
|
+
harness_options:
|
|
109
|
+
launcher: auto
|
|
110
|
+
sandbox: workspace-write
|
|
111
|
+
approval: on-request
|
|
112
|
+
search: false
|
|
113
|
+
profile: fleet
|
|
114
|
+
add_dirs: [/data/shared]
|
|
115
|
+
config:
|
|
116
|
+
model_reasoning_effort: high
|
|
117
|
+
bio: Public role card and when peers should engage it.
|
|
118
|
+
persona: Local operating contract, boundaries, and escalation policy.
|
|
119
|
+
briefing_file: /absolute/custom-briefing.md
|
|
120
|
+
coordinator: AnotherCoordinator
|
|
121
|
+
env:
|
|
122
|
+
KEY: value
|
|
123
|
+
oversee:
|
|
124
|
+
- { role: Worker, interval: 5m }
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
Role values override defaults. \`\${name}\` substitutes entries from \`vars\`.
|
|
128
|
+
Other role fields include \`max_tokens\`, \`autocompact_pct\`, and \`isolation\`.
|
|
129
|
+
Use README.md for the complete isolation policy and resource-cap schema.
|
|
130
|
+
|
|
131
|
+
## Permissions
|
|
132
|
+
|
|
133
|
+
Prefer the harness-neutral \`permissions\` block:
|
|
134
|
+
|
|
135
|
+
- \`approval: ask|allow|deny\`: whether actions may request or receive approval
|
|
136
|
+
- \`filesystem: read-only|workspace|unrestricted\`: filesystem intent
|
|
137
|
+
- \`unattended: deny|wait\`: what ACP does when no console can answer a request
|
|
138
|
+
|
|
139
|
+
The backend translates this common intent. Harness-native settings in
|
|
140
|
+
\`harness_options\` take precedence where supplied. Do not choose
|
|
141
|
+
\`allow\`/\`unrestricted\`, Codex \`never\`/\`danger-full-access\`, or Claude
|
|
142
|
+
\`bypassPermissions\` without explicit authorization.
|
|
143
|
+
|
|
144
|
+
Claude \`harness_options\`: \`permission_mode\` (default, acceptEdits, plan,
|
|
145
|
+
dontAsk, bypassPermissions), \`plugins\`, \`mem_palace\`, and
|
|
146
|
+
\`mem_palace_midsession_autosave\`.
|
|
147
|
+
|
|
148
|
+
Codex \`harness_options\`: \`launcher\` (auto, ours-codex, codex), \`sandbox\`
|
|
149
|
+
(read-only, workspace-write, danger-full-access), \`approval\` or
|
|
150
|
+
\`permission_mode\` (untrusted, on-request, never), \`profile\`, \`search\`,
|
|
151
|
+
\`config\`, \`add_dirs\`, and \`monitor\`.
|
|
152
|
+
|
|
153
|
+
## ACP adapters
|
|
154
|
+
|
|
155
|
+
The maintained \`@agentclientprotocol/codex-acp\` and
|
|
156
|
+
\`@agentclientprotocol/claude-agent-acp\` runtimes are bundled automatically as
|
|
157
|
+
optional ours-fleet dependencies. The supervisor resolves their executable
|
|
158
|
+
entrypoints internally, so default ACP roles do not depend on global PATH.
|
|
159
|
+
The maintained Claude adapter requires Node 22; tmux and Codex ACP continue to
|
|
160
|
+
work on the ours-fleet core minimum of Node 20.
|
|
161
|
+
|
|
162
|
+
Override an adapter only when necessary with \`session_options.acp.command\`
|
|
163
|
+
(string or argv list). If optional dependencies were deliberately omitted,
|
|
164
|
+
ours-fleet falls back to a compatible globally installed \`codex-acp\` or
|
|
165
|
+
\`claude-agent-acp\`. \`ours-fleet doctor -c FILE\` verifies the resolved adapter.
|
|
166
|
+
|
|
167
|
+
## Reliable mail wake
|
|
168
|
+
|
|
169
|
+
The supervisor monitor is enabled by default. It consumes body-free daemon
|
|
170
|
+
events and advances its durable cursor only after delivery is accepted. ACP uses
|
|
171
|
+
a structured \`session/prompt\`; tmux uses verified console injection. Message
|
|
172
|
+
bodies are released only when the role calls the ours \`get_messages\` tool.
|
|
173
|
+
|
|
174
|
+
Set \`monitor.enabled: false\` only to retain legacy in-session monitoring.
|
|
175
|
+
Inspect \`ours-fleet status Name\`, \`peek Name\`, role logs, and
|
|
176
|
+
\`~/.ours-fleet/agents/Name/.monitor-status\` when diagnosing delivery.
|
|
177
|
+
`;
|
package/dist/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import { realExec } from './exec.js';
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
5
5
|
import { getAdapter } from './harness/registry.js';
|
|
6
|
+
import { resolveBundledAcpAgent } from './harness/acp-agent.js';
|
|
6
7
|
import { agentDir, home, deriveXdgRuntimeDir } from './paths.js';
|
|
7
8
|
import { resolveIsolation } from './isolation/policy.js';
|
|
8
9
|
import { makeBubblewrapBackend } from './isolation/bubblewrap.js';
|
|
@@ -43,11 +44,14 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
43
44
|
name: 'node', ok: major >= 20,
|
|
44
45
|
detail: major >= 20 ? `v${process.versions.node}` : `v${process.versions.node} — need >= 20`,
|
|
45
46
|
});
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
const roles = loadConfigSafe(opts.configPath);
|
|
48
|
+
if (roles.length === 0 || roles.some(role => (role.session ?? 'tmux') === 'tmux')) {
|
|
49
|
+
const tmux = await exec('tmux', ['-V']);
|
|
50
|
+
checks.push({
|
|
51
|
+
name: 'tmux', ok: tmux.code === 0,
|
|
52
|
+
detail: tmux.code === 0 ? tmux.stdout.trim() : 'not found — apt install tmux / brew install tmux',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
51
55
|
const mcp = await exec('ours-mcp', ['--version']);
|
|
52
56
|
checks.push({
|
|
53
57
|
name: 'ours-mcp', ok: mcp.code === 0,
|
|
@@ -84,7 +88,6 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
84
88
|
// opt-in per role (OQ-1), so a missing bwrap must not fail doctor for fleets that
|
|
85
89
|
// don't use it. Only a role that DECLARES isolation and cannot get it under
|
|
86
90
|
// `strict` is a hard failure.
|
|
87
|
-
const roles = loadConfigSafe(opts.configPath);
|
|
88
91
|
const bw = await makeBubblewrapBackend(exec).available();
|
|
89
92
|
checks.push({
|
|
90
93
|
name: 'isolation: bubblewrap', ok: true,
|
|
@@ -169,6 +172,47 @@ export async function doctor(opts = {}, exec = realExec, platform = process.plat
|
|
|
169
172
|
checks.push({ name: h, ok: false, detail: e.message });
|
|
170
173
|
}
|
|
171
174
|
}
|
|
175
|
+
for (const role of roles.filter(role => role.session === 'acp')) {
|
|
176
|
+
const configured = role.session_options?.acp?.command;
|
|
177
|
+
const bundled = configured == null
|
|
178
|
+
? role.harness === 'codex'
|
|
179
|
+
? resolveBundledAcpAgent('@agentclientprotocol/codex-acp', 'codex-acp', 'codex-acp')
|
|
180
|
+
: role.harness === 'claude-code'
|
|
181
|
+
? resolveBundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp')
|
|
182
|
+
: undefined
|
|
183
|
+
: undefined;
|
|
184
|
+
const command = Array.isArray(configured)
|
|
185
|
+
? configured[0]
|
|
186
|
+
: typeof configured === 'string'
|
|
187
|
+
? configured.trim().split(/\s+/)[0]
|
|
188
|
+
: role.harness === 'codex'
|
|
189
|
+
? 'codex-acp'
|
|
190
|
+
: role.harness === 'claude-code'
|
|
191
|
+
? 'claude-agent-acp'
|
|
192
|
+
: '';
|
|
193
|
+
if (!command) {
|
|
194
|
+
checks.push({
|
|
195
|
+
name: `acp: ${role.name}`, ok: false,
|
|
196
|
+
detail: `harness '${role.harness}' has no default ACP agent; set session_options.acp.command`,
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (bundled?.bundled) {
|
|
201
|
+
checks.push({
|
|
202
|
+
name: `acp: ${role.name}`, ok: true,
|
|
203
|
+
detail: `${command} bundled with ours-fleet`,
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const result = await exec('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', 'sh', command]);
|
|
208
|
+
checks.push({
|
|
209
|
+
name: `acp: ${role.name}`,
|
|
210
|
+
ok: result.code === 0,
|
|
211
|
+
detail: result.code === 0
|
|
212
|
+
? `${command} available`
|
|
213
|
+
: `${command} not found or failed — install the ACP adapter or set session_options.acp.command`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
172
216
|
return { ok: checks.every(c => c.ok), checks };
|
|
173
217
|
}
|
|
174
218
|
function loadConfigSafe(configPath) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve an ACP agent shipped as an ours-fleet dependency. Running the JS
|
|
3
|
+
* entrypoint through this process's Node avoids depending on npm exposing a
|
|
4
|
+
* transitive dependency's bin on the user's global PATH.
|
|
5
|
+
*/
|
|
6
|
+
export interface AcpAgentResolution {
|
|
7
|
+
argv: string[];
|
|
8
|
+
bundled: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveBundledAcpAgent(packageName: string, binName: string, fallbackCommand: string): AcpAgentResolution;
|
|
11
|
+
export declare function bundledAcpAgent(packageName: string, binName: string, fallbackCommand: string): string[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
export function resolveBundledAcpAgent(packageName, binName, fallbackCommand) {
|
|
6
|
+
try {
|
|
7
|
+
const manifestPath = require.resolve(`${packageName}/package.json`);
|
|
8
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
9
|
+
const relative = typeof manifest.bin === 'string'
|
|
10
|
+
? manifest.bin
|
|
11
|
+
: manifest.bin?.[binName];
|
|
12
|
+
if (!relative)
|
|
13
|
+
return { argv: [fallbackCommand], bundled: false };
|
|
14
|
+
const entrypoint = resolve(dirname(manifestPath), relative);
|
|
15
|
+
if (!existsSync(entrypoint))
|
|
16
|
+
return { argv: [fallbackCommand], bundled: false };
|
|
17
|
+
return { argv: [process.execPath, entrypoint], bundled: true };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Supports development installs that intentionally omit optional
|
|
21
|
+
// dependencies and existing hosts with a globally installed adapter.
|
|
22
|
+
return { argv: [fallbackCommand], bundled: false };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function bundledAcpAgent(packageName, binName, fallbackCommand) {
|
|
26
|
+
return resolveBundledAcpAgent(packageName, binName, fallbackCommand).argv;
|
|
27
|
+
}
|
|
@@ -3,14 +3,20 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { home } from '../paths.js';
|
|
4
4
|
import { realExec } from '../exec.js';
|
|
5
5
|
import { registerAdapter } from './registry.js';
|
|
6
|
+
import { bundledAcpAgent } from './acp-agent.js';
|
|
6
7
|
const OPTION_KEYS = ['plugins', 'mem_palace', 'mem_palace_midsession_autosave', 'permission_mode'];
|
|
7
8
|
/** Claude Code's accepted --permission-mode values. */
|
|
8
9
|
const PERMISSION_MODES = ['default', 'acceptEdits', 'plan', 'dontAsk', 'bypassPermissions'];
|
|
9
10
|
/** Resolve & validate the per-role permission mode, throwing on an unknown value. */
|
|
10
11
|
function permissionMode(role) {
|
|
11
12
|
const pm = role.harness_options?.permission_mode;
|
|
12
|
-
if (pm == null)
|
|
13
|
+
if (pm == null) {
|
|
14
|
+
if (role.permissions?.approval === 'allow')
|
|
15
|
+
return 'dontAsk';
|
|
16
|
+
if (role.permissions?.approval === 'deny')
|
|
17
|
+
return 'plan';
|
|
13
18
|
return undefined;
|
|
19
|
+
}
|
|
14
20
|
if (!PERMISSION_MODES.includes(pm))
|
|
15
21
|
throw new Error(`invalid harness_options.permission_mode "${pm}"; allowed: ${PERMISSION_MODES.join(', ')}`);
|
|
16
22
|
return pm;
|
|
@@ -108,6 +114,30 @@ export function makeClaudeCodeAdapter(exec = realExec) {
|
|
|
108
114
|
this.vocabulary.restartPrompt(role.identity, join(stateDir, 'WORKLOG.md'), role)];
|
|
109
115
|
return { argv, env: prep.env };
|
|
110
116
|
},
|
|
117
|
+
buildAcpLaunch(role, prep) {
|
|
118
|
+
const configured = role.session_options?.acp?.command;
|
|
119
|
+
const argv = Array.isArray(configured)
|
|
120
|
+
? [...configured]
|
|
121
|
+
: typeof configured === 'string'
|
|
122
|
+
? ['sh', '-c', configured]
|
|
123
|
+
: bundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp');
|
|
124
|
+
return { argv, env: prep.env };
|
|
125
|
+
},
|
|
126
|
+
translatePermissions(permissions) {
|
|
127
|
+
const native = permissions.approval === 'allow'
|
|
128
|
+
? 'dontAsk'
|
|
129
|
+
: permissions.approval === 'deny'
|
|
130
|
+
? 'plan'
|
|
131
|
+
: 'default';
|
|
132
|
+
const exact = permissions.filesystem === 'workspace' && permissions.approval === 'ask';
|
|
133
|
+
return {
|
|
134
|
+
native: { permission_mode: native },
|
|
135
|
+
exact,
|
|
136
|
+
warnings: exact ? [] : [
|
|
137
|
+
'Claude permission modes do not exactly represent independent approval and filesystem intent; fleet isolation remains the outer boundary',
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
},
|
|
111
141
|
vocabulary: {
|
|
112
142
|
bindTool: 'choose_identity',
|
|
113
143
|
createTool: 'create_identity',
|
package/dist/harness/codex.js
CHANGED
|
@@ -2,6 +2,7 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { agentDir } from '../paths.js';
|
|
3
3
|
import { realExec } from '../exec.js';
|
|
4
4
|
import { registerAdapter } from './registry.js';
|
|
5
|
+
import { bundledAcpAgent } from './acp-agent.js';
|
|
5
6
|
const OPTION_KEYS = [
|
|
6
7
|
'launcher', 'sandbox', 'approval', 'permission_mode', 'search', 'profile', 'config', 'add_dirs',
|
|
7
8
|
'monitor',
|
|
@@ -14,8 +15,16 @@ const APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'];
|
|
|
14
15
|
/** Resolve & validate the per-role sandbox mode, throwing on an unknown value. */
|
|
15
16
|
function sandboxMode(role) {
|
|
16
17
|
const s = role.harness_options?.sandbox;
|
|
17
|
-
if (s == null)
|
|
18
|
+
if (s == null) {
|
|
19
|
+
const filesystem = role.permissions?.filesystem;
|
|
20
|
+
if (filesystem === 'read-only')
|
|
21
|
+
return 'read-only';
|
|
22
|
+
if (filesystem === 'unrestricted')
|
|
23
|
+
return 'danger-full-access';
|
|
24
|
+
if (filesystem === 'workspace')
|
|
25
|
+
return 'workspace-write';
|
|
18
26
|
return undefined;
|
|
27
|
+
}
|
|
19
28
|
if (!SANDBOX_MODES.includes(s))
|
|
20
29
|
throw new Error(`invalid harness_options.sandbox "${s}"; allowed: ${SANDBOX_MODES.join(', ')}`);
|
|
21
30
|
return s;
|
|
@@ -24,8 +33,14 @@ function sandboxMode(role) {
|
|
|
24
33
|
function approvalPolicy(role) {
|
|
25
34
|
const o = role.harness_options;
|
|
26
35
|
const a = o?.approval ?? o?.permission_mode;
|
|
27
|
-
if (a == null)
|
|
36
|
+
if (a == null) {
|
|
37
|
+
const approval = role.permissions?.approval;
|
|
38
|
+
if (approval === 'allow')
|
|
39
|
+
return 'never';
|
|
40
|
+
if (approval === 'ask' || approval === 'deny')
|
|
41
|
+
return 'on-request';
|
|
28
42
|
return undefined;
|
|
43
|
+
}
|
|
29
44
|
if (!APPROVAL_POLICIES.includes(a))
|
|
30
45
|
throw new Error(`invalid harness_options.approval "${a}"; allowed: ${APPROVAL_POLICIES.join(', ')}`);
|
|
31
46
|
return a;
|
|
@@ -173,6 +188,29 @@ export function makeCodexAdapter(exec = realExec) {
|
|
|
173
188
|
this.vocabulary.restartPrompt(role.identity, join(stateDir, 'WORKLOG.md'), role)];
|
|
174
189
|
return { argv, env: prep.env };
|
|
175
190
|
},
|
|
191
|
+
buildAcpLaunch(role, prep) {
|
|
192
|
+
const configured = role.session_options?.acp?.command;
|
|
193
|
+
const argv = Array.isArray(configured)
|
|
194
|
+
? [...configured]
|
|
195
|
+
: typeof configured === 'string'
|
|
196
|
+
? ['sh', '-c', configured]
|
|
197
|
+
: bundledAcpAgent('@agentclientprotocol/codex-acp', 'codex-acp', 'codex-acp');
|
|
198
|
+
return { argv, env: prep.env };
|
|
199
|
+
},
|
|
200
|
+
translatePermissions(permissions) {
|
|
201
|
+
return {
|
|
202
|
+
native: {
|
|
203
|
+
approval: permissions.approval === 'allow' ? 'never' : 'on-request',
|
|
204
|
+
sandbox: permissions.filesystem === 'read-only'
|
|
205
|
+
? 'read-only'
|
|
206
|
+
: permissions.filesystem === 'unrestricted'
|
|
207
|
+
? 'danger-full-access'
|
|
208
|
+
: 'workspace-write',
|
|
209
|
+
},
|
|
210
|
+
exact: true,
|
|
211
|
+
warnings: [],
|
|
212
|
+
};
|
|
213
|
+
},
|
|
176
214
|
vocabulary: {
|
|
177
215
|
bindTool: 'choose_identity',
|
|
178
216
|
createTool: 'create_identity',
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ResolvedRole } from '../config.js';
|
|
1
|
+
import type { CommonPermissions, ResolvedRole } from '../config.js';
|
|
2
2
|
export interface PrereqCheck {
|
|
3
3
|
name: string;
|
|
4
4
|
ok: boolean;
|
|
@@ -26,6 +26,15 @@ export interface Launch {
|
|
|
26
26
|
argv: string[];
|
|
27
27
|
env: Record<string, string>;
|
|
28
28
|
}
|
|
29
|
+
export interface AcpLaunch {
|
|
30
|
+
argv: string[];
|
|
31
|
+
env: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
export interface PermissionTranslation {
|
|
34
|
+
native: Record<string, unknown>;
|
|
35
|
+
exact: boolean;
|
|
36
|
+
warnings: string[];
|
|
37
|
+
}
|
|
29
38
|
/** Harness-correct wording/tool names used to generate briefing.md. */
|
|
30
39
|
export interface BriefingVocab {
|
|
31
40
|
bindTool: string;
|
|
@@ -57,6 +66,8 @@ export interface HarnessAdapter {
|
|
|
57
66
|
validateOptions(opts: unknown): ValidationError[];
|
|
58
67
|
prepareSession(role: ResolvedRole, dirs: RoleDirs): Promise<SessionPrep>;
|
|
59
68
|
buildLaunch(role: ResolvedRole, mode: 'fresh' | 'resume', s: SessionState, prep: SessionPrep): Launch;
|
|
69
|
+
buildAcpLaunch?(role: ResolvedRole, prep: SessionPrep): AcpLaunch;
|
|
70
|
+
translatePermissions?(permissions: CommonPermissions): PermissionTranslation;
|
|
60
71
|
vocabulary: BriefingVocab;
|
|
61
72
|
exitPolicy: ExitPolicy;
|
|
62
73
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
export { loadConfig, findRole, ConfigError } from './config.js';
|
|
2
|
-
export type { FleetConfig, ResolvedRole, RoleConfig, OverseeEntry } from './config.js';
|
|
3
|
-
export type { HarnessAdapter, BriefingVocab, ExitPolicy, PrereqReport, PrereqCheck, SessionPrep, SessionState, Launch, RoleDirs, ValidationError, } from './harness/types.js';
|
|
1
|
+
export { loadConfig, findRole, resolvePermissions, ConfigError } from './config.js';
|
|
2
|
+
export type { FleetConfig, ResolvedRole, RoleConfig, OverseeEntry, SessionBackendId, CommonPermissions, SessionOptions, } from './config.js';
|
|
3
|
+
export type { HarnessAdapter, BriefingVocab, ExitPolicy, PrereqReport, PrereqCheck, SessionPrep, SessionState, Launch, AcpLaunch, PermissionTranslation, RoleDirs, ValidationError, } from './harness/types.js';
|
|
4
|
+
export type { SessionHandle, SessionSnapshot, SessionEvent, TurnResult, } from './session/types.js';
|
|
5
|
+
export { AcpSession } from './session/acp.js';
|
|
6
|
+
export { TmuxSession } from './session/tmux.js';
|
|
4
7
|
export { registerAdapter, getAdapter, knownAdapters } from './harness/registry.js';
|
|
5
8
|
export { claudeCodeAdapter, makeClaudeCodeAdapter } from './harness/claude-code.js';
|
|
6
9
|
export { codexAdapter, makeCodexAdapter } from './harness/codex.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { loadConfig, findRole, ConfigError } from './config.js';
|
|
1
|
+
export { loadConfig, findRole, resolvePermissions, ConfigError } from './config.js';
|
|
2
|
+
export { AcpSession } from './session/acp.js';
|
|
3
|
+
export { TmuxSession } from './session/tmux.js';
|
|
2
4
|
export { registerAdapter, getAdapter, knownAdapters } from './harness/registry.js';
|
|
3
5
|
export { claudeCodeAdapter, makeClaudeCodeAdapter } from './harness/claude-code.js';
|
|
4
6
|
export { codexAdapter, makeCodexAdapter } from './harness/codex.js';
|
package/dist/monitor.d.ts
CHANGED
|
@@ -38,6 +38,13 @@ export interface MonitorDeps {
|
|
|
38
38
|
set(fn: () => void, ms: number): ReturnType<typeof setTimeout>;
|
|
39
39
|
clear(t: ReturnType<typeof setTimeout>): void;
|
|
40
40
|
};
|
|
41
|
+
/** Structured prompt delivery used by ACP sessions. Tmux remains the fallback. */
|
|
42
|
+
delivery?: {
|
|
43
|
+
submit(text: string): Promise<{
|
|
44
|
+
accepted: boolean;
|
|
45
|
+
detail?: string;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
41
48
|
}
|
|
42
49
|
/** Best-effort daemon config (issue #17): the fields the MCP client reads. */
|
|
43
50
|
interface DaemonConfig {
|
|
@@ -80,9 +87,19 @@ export declare function filterEvents(events: NotifyEvent[], wakeSources: string[
|
|
|
80
87
|
export declare function formatNotificationLine(events: NotifyEvent[]): string;
|
|
81
88
|
/**
|
|
82
89
|
* Heuristic: does the pane show a modal selection dialog we must not `Enter`
|
|
83
|
-
* into?
|
|
84
|
-
*
|
|
85
|
-
*
|
|
90
|
+
* into? Two independent signals, both requiring the *option* shape, not just a
|
|
91
|
+
* loose numbered line:
|
|
92
|
+
*
|
|
93
|
+
* 1. the `❯` pointer sitting on a numbered option — `❯ 1. Use this MCP server`;
|
|
94
|
+
* 2. a dialog marker ("Do you want …", "Enter to confirm") with ≥2 numbered
|
|
95
|
+
* options within `OPTION_WINDOW` lines — this still catches a dialog captured
|
|
96
|
+
* mid-redraw, before its pointer row is painted.
|
|
97
|
+
*
|
|
98
|
+
* A running turn, a prose list, and a markdown step list are all NOT modal.
|
|
99
|
+
* Erring modal is the safe direction (a wake is retried; an `Enter` into a live
|
|
100
|
+
* permission dialog is not undoable), which is why signal 2 is kept — but a bare
|
|
101
|
+
* marker with no options no longer suffices, because Claude Code closes turns
|
|
102
|
+
* with exactly that prose ("Do you want me to open the PR?").
|
|
86
103
|
*/
|
|
87
104
|
export declare function looksModal(pane: string): boolean;
|
|
88
105
|
/**
|
|
@@ -102,6 +119,8 @@ export declare function looksApiError(pane: string): boolean;
|
|
|
102
119
|
export declare function looksRunning(pane: string): boolean;
|
|
103
120
|
export interface MonitorOpts {
|
|
104
121
|
name: string;
|
|
122
|
+
/** Ours identity whose notification stream is authoritative (may differ from role name). */
|
|
123
|
+
identity?: string;
|
|
105
124
|
agentDir: string;
|
|
106
125
|
cfg: MonitorConfig;
|
|
107
126
|
deps: MonitorDeps;
|
|
@@ -114,12 +133,16 @@ export interface MonitorHandle {
|
|
|
114
133
|
}
|
|
115
134
|
export declare class Monitor {
|
|
116
135
|
private readonly name;
|
|
136
|
+
private readonly identity;
|
|
117
137
|
private readonly cfg;
|
|
118
138
|
private readonly deps;
|
|
119
139
|
private readonly ep;
|
|
120
140
|
private readonly statusPath;
|
|
121
141
|
private readonly cursorPath;
|
|
142
|
+
private readonly statePath;
|
|
122
143
|
private cursor;
|
|
144
|
+
private deliveredCursor;
|
|
145
|
+
private pendingState;
|
|
123
146
|
private fatal;
|
|
124
147
|
private stopped;
|
|
125
148
|
private bootDeadline;
|
|
@@ -127,7 +150,7 @@ export declare class Monitor {
|
|
|
127
150
|
private apiErrorStreak;
|
|
128
151
|
private readonly turnFailThreshold;
|
|
129
152
|
constructor(o: MonitorOpts);
|
|
130
|
-
/**
|
|
153
|
+
/** Resume the last delivered cursor; only a brand-new monitor primes at stream tip. */
|
|
131
154
|
prime(): Promise<void>;
|
|
132
155
|
/** Long-poll → filter → coalesce → inject, until the pane pid dies or stop(). */
|
|
133
156
|
run(pid: number): Promise<void>;
|
|
@@ -154,12 +177,19 @@ export declare class Monitor {
|
|
|
154
177
|
* no-ops (delivery is still verified downstream).
|
|
155
178
|
*/
|
|
156
179
|
private clearComposer;
|
|
157
|
-
/**
|
|
180
|
+
/**
|
|
181
|
+
* Block until the console can accept input; classify offline/stopped/ready, or
|
|
182
|
+
* `modal` when the pane still looks modal after `MODAL_GIVE_UP_MS`. The bound is
|
|
183
|
+
* what keeps a modal from wedging delivery silently: we still never `Enter` into
|
|
184
|
+
* the dialog, but the give-up is reported instead of retried forever.
|
|
185
|
+
*/
|
|
158
186
|
private awaitInjectable;
|
|
159
187
|
private doFetch;
|
|
160
188
|
private advance;
|
|
161
189
|
private persistCursor;
|
|
162
190
|
private readPersistedCursor;
|
|
191
|
+
/** Atomically persist body-free delivery state; restart always resumes from deliveredCursor. */
|
|
192
|
+
private persistState;
|
|
163
193
|
private setStatus;
|
|
164
194
|
}
|
|
165
195
|
export declare function createMonitor(o: MonitorOpts): Monitor;
|