@ours.network/fleet 0.9.4 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +148 -30
  2. package/dist/atomic-file.d.ts +30 -0
  3. package/dist/atomic-file.js +86 -0
  4. package/dist/briefing.d.ts +6 -0
  5. package/dist/briefing.js +41 -11
  6. package/dist/cli.js +238 -26
  7. package/dist/config.d.ts +39 -1
  8. package/dist/config.js +126 -3
  9. package/dist/creation.d.ts +179 -0
  10. package/dist/creation.js +254 -0
  11. package/dist/docs.d.ts +34 -0
  12. package/dist/docs.js +309 -0
  13. package/dist/doctor.js +123 -21
  14. package/dist/harness/acp-agent.d.ts +11 -0
  15. package/dist/harness/acp-agent.js +27 -0
  16. package/dist/harness/claude-code.d.ts +39 -3
  17. package/dist/harness/claude-code.js +145 -13
  18. package/dist/harness/codex.d.ts +7 -1
  19. package/dist/harness/codex.js +89 -4
  20. package/dist/harness/registry.d.ts +2 -0
  21. package/dist/harness/registry.js +19 -0
  22. package/dist/harness/types.d.ts +59 -1
  23. package/dist/index.d.ts +6 -3
  24. package/dist/index.js +3 -1
  25. package/dist/isolation/bubblewrap.js +7 -1
  26. package/dist/isolation/policy.d.ts +34 -5
  27. package/dist/isolation/policy.js +114 -7
  28. package/dist/isolation/resources.d.ts +6 -3
  29. package/dist/isolation/resources.js +6 -3
  30. package/dist/isolation/types.d.ts +19 -1
  31. package/dist/monitor.d.ts +44 -2
  32. package/dist/monitor.js +177 -42
  33. package/dist/ops.d.ts +15 -2
  34. package/dist/ops.js +32 -9
  35. package/dist/permissions.d.ts +70 -0
  36. package/dist/permissions.js +97 -0
  37. package/dist/runner.d.ts +65 -2
  38. package/dist/runner.js +307 -32
  39. package/dist/session/acp.d.ts +70 -0
  40. package/dist/session/acp.js +364 -0
  41. package/dist/session/control.d.ts +89 -0
  42. package/dist/session/control.js +322 -0
  43. package/dist/session/events.d.ts +14 -0
  44. package/dist/session/events.js +67 -0
  45. package/dist/session/tmux.d.ts +27 -0
  46. package/dist/session/tmux.js +76 -0
  47. package/dist/session/types.d.ts +138 -0
  48. package/dist/session/types.js +42 -0
  49. package/dist/spawn.d.ts +32 -2
  50. package/dist/spawn.js +177 -16
  51. package/dist/supervisor/launchd.d.ts +50 -0
  52. package/dist/supervisor/launchd.js +121 -4
  53. package/dist/supervisor/none.js +22 -4
  54. package/dist/supervisor/systemd.d.ts +8 -1
  55. package/dist/supervisor/systemd.js +94 -4
  56. package/dist/supervisor/types.d.ts +36 -3
  57. package/dist/tmux.d.ts +34 -2
  58. package/dist/tmux.js +48 -11
  59. package/package.json +7 -2
@@ -0,0 +1,70 @@
1
+ import type { CommonPermissions } from '../config.js';
2
+ import type { ExitRecord, QueuedPrompt, SessionEvent, SessionHandle, SessionSnapshot, TurnOutcome, TurnResult } from './types.js';
3
+ export interface AcpSessionOptions {
4
+ name: string;
5
+ argv: string[];
6
+ cwd: string;
7
+ env: Record<string, string>;
8
+ stateDir: string;
9
+ mode: 'fresh' | 'resume';
10
+ permissions: CommonPermissions;
11
+ log(line: string): void;
12
+ }
13
+ /**
14
+ * Classify an ACP `stopReason` into a terminal outcome. A refusal and a
15
+ * cancellation are the two ways a delivered prompt ends without being carried
16
+ * out; every other stop reason ran the turn to an end the agent chose.
17
+ */
18
+ export declare function classifyStopReason(stopReason: string | undefined): TurnOutcome;
19
+ /**
20
+ * Persistent ACP v1 client. It is the sole owner of the agent's stdio; all
21
+ * human/automation attachment happens through the fleet role-control protocol.
22
+ */
23
+ export declare class AcpSession implements SessionHandle {
24
+ private readonly options;
25
+ readonly backend: "acp";
26
+ readonly pid: number;
27
+ private readonly child;
28
+ private readonly events;
29
+ private readonly sessionFile;
30
+ private readonly pendingPermissions;
31
+ private connection;
32
+ private sessionId?;
33
+ private readiness;
34
+ private lastError?;
35
+ private promptTail;
36
+ private queueDepth;
37
+ private exit;
38
+ private capabilities?;
39
+ private controllerCount;
40
+ private constructor();
41
+ static start(options: AcpSessionOptions): Promise<AcpSession>;
42
+ isAlive(): boolean;
43
+ snapshot(): SessionSnapshot;
44
+ /**
45
+ * Accept responsibility for a prompt, then return. The turn itself may run
46
+ * for minutes behind other queued turns; making an interactive caller wait
47
+ * for it is what turned a busy agent into a timeout and then into "dead".
48
+ */
49
+ queuePrompt(text: string): Promise<QueuedPrompt>;
50
+ submitPrompt(text: string): Promise<TurnResult>;
51
+ interrupt(): Promise<void>;
52
+ respondPermission(permissionId: string, optionId: string): boolean;
53
+ eventsSince(seq: number): SessionEvent[];
54
+ subscribe(listener: (event: SessionEvent) => void): () => void;
55
+ setControllerAttached(attached: boolean): void;
56
+ exitResult(): ExitRecord | null;
57
+ close(): Promise<void>;
58
+ private initialize;
59
+ private runPrompt;
60
+ private requestPermission;
61
+ /**
62
+ * Resolve a permission request from policy alone and leave a record of it.
63
+ * Nothing else in the system can observe an automatic decision, so an
64
+ * unrecorded one is indistinguishable from a request that was never made.
65
+ */
66
+ private settleAutomatically;
67
+ private withinAutomaticBoundary;
68
+ private recordUpdate;
69
+ private fail;
70
+ }
@@ -0,0 +1,364 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { isAbsolute, join, relative, resolve } from 'node:path';
5
+ import { Readable, Writable } from 'node:stream';
6
+ import * as acp from '@agentclientprotocol/sdk';
7
+ import { SessionEvents } from './events.js';
8
+ import { SessionControlError, classifyChildExit, turnResult } from './types.js';
9
+ /**
10
+ * Classify an ACP `stopReason` into a terminal outcome. A refusal and a
11
+ * cancellation are the two ways a delivered prompt ends without being carried
12
+ * out; every other stop reason ran the turn to an end the agent chose.
13
+ */
14
+ export function classifyStopReason(stopReason) {
15
+ switch (stopReason) {
16
+ case 'refusal': return 'refused';
17
+ case 'cancelled': return 'cancelled';
18
+ default: return 'completed';
19
+ }
20
+ }
21
+ /**
22
+ * Persistent ACP v1 client. It is the sole owner of the agent's stdio; all
23
+ * human/automation attachment happens through the fleet role-control protocol.
24
+ */
25
+ export class AcpSession {
26
+ options;
27
+ backend = 'acp';
28
+ pid;
29
+ child;
30
+ events;
31
+ sessionFile;
32
+ pendingPermissions = new Map();
33
+ connection;
34
+ sessionId;
35
+ readiness = 'starting';
36
+ lastError;
37
+ promptTail = Promise.resolve();
38
+ queueDepth = 0;
39
+ exit = null;
40
+ capabilities;
41
+ controllerCount = 0;
42
+ constructor(options, child, connection) {
43
+ this.options = options;
44
+ this.child = child;
45
+ this.connection = connection;
46
+ this.pid = child.pid ?? -1;
47
+ this.events = new SessionEvents(join(options.stateDir, '.session-events.jsonl'));
48
+ this.sessionFile = join(options.stateDir, '.acp-session-id');
49
+ child.stderr.on('data', chunk => options.log(`[${options.name}] acp: ${String(chunk).trimEnd()}`));
50
+ child.once('exit', (code, signal) => {
51
+ // Record the child's real exit code/signal. The tmux path can only see a
52
+ // shell's `$?`; here the truth is available, so keep it.
53
+ this.exit = classifyChildExit(code, signal);
54
+ if (this.readiness !== 'failed') {
55
+ this.readiness = 'failed';
56
+ this.lastError = `ACP agent ${this.exit.detail}`;
57
+ }
58
+ this.events.emit('state', { status: 'failed', text: this.lastError });
59
+ });
60
+ }
61
+ static async start(options) {
62
+ if (!options.argv.length)
63
+ throw new Error('ACP agent command is empty');
64
+ const child = spawn(options.argv[0], options.argv.slice(1), {
65
+ cwd: options.cwd,
66
+ env: { ...process.env, ...options.env },
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ });
69
+ await new Promise((resolve, reject) => {
70
+ child.once('spawn', resolve);
71
+ child.once('error', reject);
72
+ });
73
+ let instance;
74
+ const app = acp.client({ name: 'ours-fleet' })
75
+ .onNotification(acp.methods.client.session.update, ({ params }) => {
76
+ instance?.recordUpdate(params.update);
77
+ })
78
+ .onRequest(acp.methods.client.session.requestPermission, ({ params }) => {
79
+ if (!instance)
80
+ return { outcome: { outcome: 'cancelled' } };
81
+ return instance.requestPermission(params);
82
+ });
83
+ const stream = acp.ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
84
+ const connection = app.connect(stream);
85
+ instance = new AcpSession(options, child, connection);
86
+ try {
87
+ await instance.initialize();
88
+ return instance;
89
+ }
90
+ catch (error) {
91
+ instance.fail(error);
92
+ await instance.close();
93
+ throw error;
94
+ }
95
+ }
96
+ isAlive() {
97
+ return this.child.exitCode === null && !this.child.killed;
98
+ }
99
+ snapshot() {
100
+ return {
101
+ backend: 'acp',
102
+ alive: this.isAlive(),
103
+ readiness: this.readiness,
104
+ sessionId: this.sessionId,
105
+ lastError: this.lastError,
106
+ pendingPermissionId: this.pendingPermissions.keys().next().value,
107
+ };
108
+ }
109
+ /**
110
+ * Accept responsibility for a prompt, then return. The turn itself may run
111
+ * for minutes behind other queued turns; making an interactive caller wait
112
+ * for it is what turned a busy agent into a timeout and then into "dead".
113
+ */
114
+ async queuePrompt(text) {
115
+ if (!this.sessionId || !this.isAlive())
116
+ throw new SessionControlError('offline', this.lastError ?? 'ACP session is offline');
117
+ const promptId = randomUUID();
118
+ const queuedBehind = this.queueDepth++;
119
+ const run = this.promptTail.then(() => this.runPrompt(text, promptId));
120
+ this.promptTail = run.then(() => undefined, () => undefined);
121
+ const completion = run.then(result => { this.queueDepth = Math.max(0, this.queueDepth - 1); return result; }, error => {
122
+ this.queueDepth = Math.max(0, this.queueDepth - 1);
123
+ return turnResult(false, 'failed', error?.message ?? String(error));
124
+ });
125
+ return { promptId, queuedBehind, completion };
126
+ }
127
+ async submitPrompt(text) {
128
+ try {
129
+ return await (await this.queuePrompt(text)).completion;
130
+ }
131
+ catch (error) {
132
+ if (error instanceof SessionControlError)
133
+ return turnResult(false, 'failed', error.message);
134
+ throw error;
135
+ }
136
+ }
137
+ async interrupt() {
138
+ if (!this.sessionId)
139
+ return;
140
+ await this.connection.agent.notify(acp.methods.agent.session.cancel, { sessionId: this.sessionId });
141
+ }
142
+ respondPermission(permissionId, optionId) {
143
+ const pending = this.pendingPermissions.get(permissionId);
144
+ const chosen = pending?.options.find(option => option.optionId === optionId);
145
+ if (!pending || !chosen)
146
+ return false;
147
+ this.pendingPermissions.delete(permissionId);
148
+ pending.resolve({ outcome: { outcome: 'selected', optionId } });
149
+ this.events.emit('permission', {
150
+ permissionId,
151
+ status: 'completed',
152
+ decision: chosen.kind.startsWith('reject') ? 'denied' : 'allowed',
153
+ decisionSource: 'manual',
154
+ reason: `answered from an attached controller (${chosen.kind})`,
155
+ optionId,
156
+ });
157
+ this.readiness = 'running';
158
+ return true;
159
+ }
160
+ eventsSince(seq) {
161
+ return this.events.since(seq);
162
+ }
163
+ subscribe(listener) {
164
+ return this.events.subscribe(listener);
165
+ }
166
+ setControllerAttached(attached) {
167
+ this.controllerCount = Math.max(0, this.controllerCount + (attached ? 1 : -1));
168
+ }
169
+ exitResult() {
170
+ return this.exit;
171
+ }
172
+ async close() {
173
+ for (const pending of this.pendingPermissions.values())
174
+ pending.resolve({ outcome: { outcome: 'cancelled' } });
175
+ this.pendingPermissions.clear();
176
+ if (this.sessionId && this.capabilities?.sessionCapabilities?.close != null) {
177
+ await this.connection.agent.request(acp.methods.agent.session.close, { sessionId: this.sessionId }).catch(() => undefined);
178
+ }
179
+ this.connection.close();
180
+ if (this.isAlive())
181
+ this.child.kill('SIGTERM');
182
+ }
183
+ async initialize() {
184
+ const initialized = await this.connection.agent.request(acp.methods.agent.initialize, {
185
+ protocolVersion: acp.PROTOCOL_VERSION,
186
+ clientCapabilities: {},
187
+ clientInfo: { name: 'ours-fleet', version: '1' },
188
+ });
189
+ if (initialized.protocolVersion !== acp.PROTOCOL_VERSION)
190
+ throw new Error(`ACP protocol mismatch: agent selected ${initialized.protocolVersion}, client supports ${acp.PROTOCOL_VERSION}`);
191
+ this.capabilities = initialized.agentCapabilities;
192
+ const persisted = this.options.mode === 'resume' && existsSync(this.sessionFile)
193
+ ? readFileSync(this.sessionFile, 'utf8').trim()
194
+ : '';
195
+ if (persisted && this.capabilities?.sessionCapabilities?.resume != null) {
196
+ await this.connection.agent.request(acp.methods.agent.session.resume, {
197
+ sessionId: persisted,
198
+ cwd: this.options.cwd,
199
+ mcpServers: [],
200
+ });
201
+ this.sessionId = persisted;
202
+ }
203
+ else if (persisted && this.capabilities?.loadSession) {
204
+ await this.connection.agent.request(acp.methods.agent.session.load, {
205
+ sessionId: persisted,
206
+ cwd: this.options.cwd,
207
+ mcpServers: [],
208
+ });
209
+ this.sessionId = persisted;
210
+ }
211
+ else {
212
+ const created = await this.connection.agent.request(acp.methods.agent.session.new, {
213
+ cwd: this.options.cwd,
214
+ mcpServers: [],
215
+ });
216
+ this.sessionId = created.sessionId;
217
+ }
218
+ writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
219
+ this.readiness = 'idle';
220
+ this.events.emit('state', { status: 'idle', text: `ACP session ${this.sessionId}` });
221
+ }
222
+ async runPrompt(text, turnId = randomUUID()) {
223
+ if (!this.sessionId || !this.isAlive())
224
+ return turnResult(false, 'failed', this.lastError ?? 'ACP session is offline');
225
+ this.readiness = 'running';
226
+ this.events.emit('state', { turnId, status: 'running' });
227
+ try {
228
+ const response = await this.connection.agent.request(acp.methods.agent.session.prompt, {
229
+ sessionId: this.sessionId,
230
+ prompt: [{ type: 'text', text }],
231
+ });
232
+ this.readiness = 'idle';
233
+ this.events.emit('turn_stop', { turnId, stopReason: response.stopReason });
234
+ this.events.emit('state', { status: 'idle' });
235
+ // The prompt was accepted either way — the agent answered. Whether the
236
+ // turn SUCCEEDED is a separate question, and only `stopReason` answers it.
237
+ return turnResult(true, classifyStopReason(response.stopReason), response.stopReason);
238
+ }
239
+ catch (error) {
240
+ this.lastError = error?.message ?? String(error);
241
+ this.readiness = this.isAlive() ? 'idle' : 'failed';
242
+ this.events.emit('error', { turnId, text: this.lastError });
243
+ if (this.isAlive())
244
+ this.events.emit('state', { status: 'idle' });
245
+ return turnResult(false, 'failed', this.lastError);
246
+ }
247
+ }
248
+ requestPermission(params) {
249
+ // `kinds` is a PRIORITY order. Scanning the agent's option array instead
250
+ // (`options.find(o => kinds.includes(o.kind))`) hands the choice to whatever
251
+ // order the agent happened to list, which is exactly how an automatic denial
252
+ // could land on `reject_always`.
253
+ const choose = (kinds) => {
254
+ for (const kind of kinds) {
255
+ const option = params.options.find(o => o.kind === kind);
256
+ if (option)
257
+ return option;
258
+ }
259
+ return undefined;
260
+ };
261
+ if (this.options.permissions.approval === 'allow' && this.withinAutomaticBoundary(params)) {
262
+ const option = choose(['allow_always', 'allow_once']);
263
+ return Promise.resolve(this.settleAutomatically(params, option, 'allowed', 'permissions.approval=allow', `the request is inside the ${this.options.permissions.filesystem} boundary`));
264
+ }
265
+ const unattended = this.controllerCount === 0 && this.options.permissions.unattended === 'deny';
266
+ if (this.options.permissions.approval === 'deny' || unattended) {
267
+ // reject_once FIRST: `reject_always` teaches the agent a standing rule from
268
+ // a decision no human made, so one unattended denial would silently disable
269
+ // the tool for the rest of the session.
270
+ const option = choose(['reject_once', 'reject_always']);
271
+ return Promise.resolve(this.settleAutomatically(params, option, 'denied', unattended ? 'permissions.unattended=deny' : 'permissions.approval=deny', unattended
272
+ ? 'no controller is attached, so the request cannot be shown to anyone'
273
+ : 'the role denies every permission request by policy'));
274
+ }
275
+ const permissionId = randomUUID();
276
+ this.readiness = 'awaiting_permission';
277
+ this.events.emit('permission', {
278
+ permissionId,
279
+ toolCallId: params.toolCall.toolCallId,
280
+ title: params.toolCall.title ?? 'Permission requested',
281
+ status: 'pending',
282
+ options: params.options.map(option => ({
283
+ optionId: option.optionId, name: option.name, kind: option.kind,
284
+ })),
285
+ });
286
+ return new Promise(resolve => {
287
+ this.pendingPermissions.set(permissionId, { options: params.options, resolve });
288
+ });
289
+ }
290
+ /**
291
+ * Resolve a permission request from policy alone and leave a record of it.
292
+ * Nothing else in the system can observe an automatic decision, so an
293
+ * unrecorded one is indistinguishable from a request that was never made.
294
+ */
295
+ settleAutomatically(params, option, decision, policy, reason) {
296
+ const settled = option ? decision : 'cancelled';
297
+ this.events.emit('permission', {
298
+ permissionId: randomUUID(),
299
+ toolCallId: params.toolCall.toolCallId,
300
+ title: params.toolCall.title ?? 'Permission requested',
301
+ status: 'completed',
302
+ decision: settled,
303
+ decisionSource: 'automatic',
304
+ policy,
305
+ reason: option ? reason : `${reason}, but the agent offered no matching option`,
306
+ optionId: option?.optionId,
307
+ options: params.options.map(o => ({ optionId: o.optionId, name: o.name, kind: o.kind })),
308
+ });
309
+ return option
310
+ ? { outcome: { outcome: 'selected', optionId: option.optionId } }
311
+ : { outcome: { outcome: 'cancelled' } };
312
+ }
313
+ withinAutomaticBoundary(params) {
314
+ const filesystem = this.options.permissions.filesystem;
315
+ if (filesystem === 'unrestricted')
316
+ return true;
317
+ if (filesystem === 'read-only' && params.toolCall.kind !== 'read')
318
+ return false;
319
+ const locations = params.toolCall.locations ?? [];
320
+ if (locations.length === 0)
321
+ return false;
322
+ const cwd = resolve(this.options.cwd);
323
+ return locations.every(location => {
324
+ const path = resolve(location.path);
325
+ const rel = relative(cwd, path);
326
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
327
+ });
328
+ }
329
+ recordUpdate(update) {
330
+ switch (update.sessionUpdate) {
331
+ case 'agent_message_chunk':
332
+ this.events.emit('agent_text', {
333
+ text: update.content.type === 'text' ? update.content.text : `[${update.content.type}]`,
334
+ });
335
+ break;
336
+ case 'agent_thought_chunk':
337
+ this.events.emit('thought', {
338
+ text: update.content.type === 'text' ? update.content.text : `[${update.content.type}]`,
339
+ });
340
+ break;
341
+ case 'tool_call':
342
+ this.events.emit('tool_call', {
343
+ toolCallId: update.toolCallId,
344
+ title: update.title,
345
+ status: update.status,
346
+ });
347
+ break;
348
+ case 'tool_call_update':
349
+ this.events.emit('tool_update', {
350
+ toolCallId: update.toolCallId,
351
+ title: update.title ?? undefined,
352
+ status: update.status ?? undefined,
353
+ });
354
+ break;
355
+ default:
356
+ break;
357
+ }
358
+ }
359
+ fail(error) {
360
+ this.lastError = error?.message ?? String(error);
361
+ this.readiness = 'failed';
362
+ this.events.emit('error', { text: this.lastError });
363
+ }
364
+ }
@@ -0,0 +1,89 @@
1
+ import { type Socket } from 'node:net';
2
+ import type { ControlFailureKind, SessionHandle } from './types.js';
3
+ export interface ControlRequest {
4
+ version: 1;
5
+ id: string;
6
+ token: string;
7
+ command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow';
8
+ text?: string;
9
+ permissionId?: string;
10
+ optionId?: string;
11
+ since?: number;
12
+ }
13
+ export interface ControlResponse {
14
+ version: 1;
15
+ id: string;
16
+ ok: boolean;
17
+ result?: unknown;
18
+ error?: string;
19
+ /** Why it failed, so the caller does not have to guess from the text. */
20
+ kind?: ControlFailureKind;
21
+ }
22
+ /**
23
+ * One line saying what a control failure does — and does not — prove about the
24
+ * agent. Only `offline` is evidence that it is gone; every other kind used to
25
+ * be rendered as "is not running", which is how a busy agent got restarted.
26
+ */
27
+ export declare function livenessNote(kind: ControlFailureKind, name: string): string;
28
+ /**
29
+ * The result taxonomy an overseer judges a role by (7.2).
30
+ *
31
+ * One console command is not a liveness verdict. `peek` and `send` can fail for
32
+ * five distinct reasons and succeed for one, and only ONE of the six says the
33
+ * agent is gone — collapsing them into "not running" is how busy agents got
34
+ * restarted. This is the single definition of that vocabulary: the generated
35
+ * briefing renders it, and the shipped oversee-agents skills quote it. The
36
+ * per-result wording comes from `livenessNote` rather than being restated, so
37
+ * the words an overseer reads in its instructions are the words the CLI prints.
38
+ */
39
+ export interface OversightResult {
40
+ /** What the command reported: the one success, or the failure kind. */
41
+ result: 'queued' | ControlFailureKind;
42
+ /** What it proves about the agent. */
43
+ meaning: string;
44
+ /** What the overseer does next. */
45
+ action: string;
46
+ /**
47
+ * Whether this result ALONE justifies restarting the role. True for exactly
48
+ * one result. Every other one requires corroboration before touching a role
49
+ * that may simply be working.
50
+ */
51
+ restartJustified: boolean;
52
+ }
53
+ /** The taxonomy, in the order generated guidance presents it. */
54
+ export declare function oversightTaxonomy(name?: string): OversightResult[];
55
+ /**
56
+ * The taxonomy as guidance lines, for a briefing or any generated document.
57
+ * The shipped oversee-agents skills carry these same lines, and a test holds
58
+ * them to it — so an overseer reading its briefing and an overseer reading the
59
+ * skill cannot be given different rules.
60
+ */
61
+ export declare function oversightTaxonomyLines(name?: string): string[];
62
+ export declare const controlSocketPath: (stateDir: string) => string;
63
+ export declare const controlTokenPath: (stateDir: string) => string;
64
+ /** Private, versioned JSONL control plane for CLI and future console frontends. */
65
+ export declare class RoleControlServer {
66
+ private readonly session;
67
+ private readonly log;
68
+ private readonly server;
69
+ private readonly socketPath;
70
+ private readonly token;
71
+ private readonly sockets;
72
+ constructor(stateDir: string, session: SessionHandle, log: (line: string) => void);
73
+ start(): Promise<void>;
74
+ close(): Promise<void>;
75
+ private accept;
76
+ private handle;
77
+ private write;
78
+ }
79
+ /**
80
+ * Send one control request. Every failure mode is classified: a missing token
81
+ * or socket is `control-unavailable`, a silent server is `timeout`, and a
82
+ * response that is not parseable JSON is `backend`. The caller never has to
83
+ * infer liveness from an exception message.
84
+ */
85
+ export declare function controlRequest(stateDir: string, request: Omit<ControlRequest, 'version' | 'id' | 'token'>, timeoutMs?: number): Promise<ControlResponse>;
86
+ export declare function followControl(stateDir: string, onMessage: (message: Record<string, unknown>) => void): Promise<{
87
+ socket: Socket;
88
+ send(request: Omit<ControlRequest, 'version' | 'id' | 'token'>): void;
89
+ }>;