@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/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 tmux = await exec('tmux', ['-V']);
47
- checks.push({
48
- name: 'tmux', ok: tmux.code === 0,
49
- detail: tmux.code === 0 ? tmux.stdout.trim() : 'not found — apt install tmux / brew install tmux',
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',
@@ -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',
@@ -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? Markers are the deployed Claude Code trust/permission dialogs a `❯`
84
- * pointer beside numbered options, or a "Do you want …" prompt (design §3.2,
85
- * open question (a): refine empirically). A running turn is NOT modal.
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
- /** Prime at the stream tip (or resume a persisted cursor if the daemon is down). */
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
- /** Block until the console can accept input; classify offline/stopped/ready. */
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;