@ours.network/fleet 0.19.0-nightly.8 → 1.0.0
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 +96 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +145 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +733 -221
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/config.js +8 -5
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +8 -0
- package/dist/rooms-tasks/provision.js +363 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +109 -4
- package/dist/rooms-tasks/types.js +1 -1
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
package/dist/loops/state.js
CHANGED
|
@@ -100,6 +100,9 @@ export class ScheduledLoopStateStore {
|
|
|
100
100
|
if (old?.definitionHash === definition.definitionHash) {
|
|
101
101
|
next[definition.name] = {
|
|
102
102
|
...old, promptHash: definition.promptHash, enabled: definition.enabled,
|
|
103
|
+
// A file written before this field existed restores as undefined; an
|
|
104
|
+
// unreported gap is absent, not lost, so normalize rather than trust.
|
|
105
|
+
missedGap: old.missedGap ?? null,
|
|
103
106
|
};
|
|
104
107
|
}
|
|
105
108
|
else {
|
|
@@ -119,6 +122,7 @@ export class ScheduledLoopStateStore {
|
|
|
119
122
|
activeRunId: old?.activeRunId ?? null,
|
|
120
123
|
counts: old?.counts ?? zeroCounts(), lastError: old?.lastError ?? null,
|
|
121
124
|
operatorDisabled: old?.operatorDisabled ?? false,
|
|
125
|
+
missedGap: old?.missedGap ?? null,
|
|
122
126
|
};
|
|
123
127
|
}
|
|
124
128
|
if (recoverActive && next[definition.name].activeRunId) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ResolvedRole } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* Which environment variable a harness reads to pin the model it RUNS.
|
|
4
|
+
*
|
|
5
|
+
* This is not a convenience: for `claude-code` it is the only channel that
|
|
6
|
+
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
7
|
+
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
8
|
+
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
9
|
+
* `settings.model`, then a resumed session's live model, then its first
|
|
10
|
+
* catalogue entry. A role's declared model was therefore invisible to every
|
|
11
|
+
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
12
|
+
* an explicitly requested one.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MODEL_ENV_BY_HARNESS: Readonly<Record<string, string>>;
|
|
15
|
+
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
16
|
+
export declare function modelEnvVar(harness: string | undefined): string | undefined;
|
|
17
|
+
export interface RoleModelEnvInput {
|
|
18
|
+
harness: string;
|
|
19
|
+
/** Already resolved by `resolveRoleModel` — may come from the fleet default. */
|
|
20
|
+
model: string | undefined;
|
|
21
|
+
/** True when the role (or `--model`) named a model, including `model: null`. */
|
|
22
|
+
modelWasExplicit: boolean;
|
|
23
|
+
defaultsEnv?: Record<string, string>;
|
|
24
|
+
roleEnv?: Record<string, string>;
|
|
25
|
+
authProxyBaseUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RoleModelEnv {
|
|
28
|
+
env: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* The model the harness will actually run. Equal to `env[pin]` for a harness
|
|
31
|
+
* that pins by env, so anything reporting this value reports the runtime.
|
|
32
|
+
*/
|
|
33
|
+
model: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
37
|
+
* never disagree.
|
|
38
|
+
*
|
|
39
|
+
* Precedence, highest first:
|
|
40
|
+
* 1. an explicit `model:` / `--model` on the role
|
|
41
|
+
* 2. the role's own `env:` pin
|
|
42
|
+
* 3. the fleet `defaults.model`
|
|
43
|
+
* 4. the fleet `defaults.env` pin
|
|
44
|
+
*
|
|
45
|
+
* Inheriting the fleet default remains correct when the role names no model
|
|
46
|
+
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
47
|
+
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
48
|
+
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
49
|
+
*
|
|
50
|
+
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
51
|
+
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveRoleModelEnv(input: RoleModelEnvInput, describe?: (message: string) => Error): RoleModelEnv;
|
|
54
|
+
/**
|
|
55
|
+
* The model a role will actually run, read back from the environment it was
|
|
56
|
+
* resolved with. Use this wherever a model is reported to a human.
|
|
57
|
+
*/
|
|
58
|
+
export declare function effectiveRoleModel(role: ResolvedRole): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
61
|
+
* role runs after resolution — model-chain recovery is the live example — must
|
|
62
|
+
* go through this, or it changes only the label.
|
|
63
|
+
*/
|
|
64
|
+
export declare function repinModelEnv(role: ResolvedRole, model: string | undefined): Record<string, string> | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Last line of defence, at the exact point a child's environment is composed:
|
|
67
|
+
* refuse to launch a role whose child would run a model other than the one the
|
|
68
|
+
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
69
|
+
* agreement must fail loudly, not start and be believed.
|
|
70
|
+
*/
|
|
71
|
+
export declare function assertModelPinReachesChild(role: ResolvedRole, childEnv: Record<string, string | undefined>): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which environment variable a harness reads to pin the model it RUNS.
|
|
3
|
+
*
|
|
4
|
+
* This is not a convenience: for `claude-code` it is the only channel that
|
|
5
|
+
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
6
|
+
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
7
|
+
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
8
|
+
* `settings.model`, then a resumed session's live model, then its first
|
|
9
|
+
* catalogue entry. A role's declared model was therefore invisible to every
|
|
10
|
+
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
11
|
+
* an explicitly requested one.
|
|
12
|
+
*/
|
|
13
|
+
export const MODEL_ENV_BY_HARNESS = {
|
|
14
|
+
'claude-code': 'ANTHROPIC_MODEL',
|
|
15
|
+
};
|
|
16
|
+
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
17
|
+
export function modelEnvVar(harness) {
|
|
18
|
+
return harness === undefined ? undefined : MODEL_ENV_BY_HARNESS[harness];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
22
|
+
* never disagree.
|
|
23
|
+
*
|
|
24
|
+
* Precedence, highest first:
|
|
25
|
+
* 1. an explicit `model:` / `--model` on the role
|
|
26
|
+
* 2. the role's own `env:` pin
|
|
27
|
+
* 3. the fleet `defaults.model`
|
|
28
|
+
* 4. the fleet `defaults.env` pin
|
|
29
|
+
*
|
|
30
|
+
* Inheriting the fleet default remains correct when the role names no model
|
|
31
|
+
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
32
|
+
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
33
|
+
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
34
|
+
*
|
|
35
|
+
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
36
|
+
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveRoleModelEnv(input, describe = message => new Error(message)) {
|
|
39
|
+
const env = {
|
|
40
|
+
...(input.defaultsEnv ?? {}),
|
|
41
|
+
...(input.roleEnv ?? {}),
|
|
42
|
+
...(input.authProxyBaseUrl ? { ANTHROPIC_BASE_URL: input.authProxyBaseUrl } : {}),
|
|
43
|
+
};
|
|
44
|
+
const pin = modelEnvVar(input.harness);
|
|
45
|
+
if (!pin)
|
|
46
|
+
return { env, model: input.model };
|
|
47
|
+
const rolePin = input.roleEnv?.[pin];
|
|
48
|
+
if (input.modelWasExplicit) {
|
|
49
|
+
if (rolePin !== undefined && rolePin !== input.model)
|
|
50
|
+
throw describe(`model '${input.model ?? '(none)'}' contradicts env.${pin} '${rolePin}'; `
|
|
51
|
+
+ `remove one — ${pin} is what the harness actually runs`);
|
|
52
|
+
if (input.model === undefined)
|
|
53
|
+
delete env[pin];
|
|
54
|
+
else
|
|
55
|
+
env[pin] = input.model;
|
|
56
|
+
return { env, model: input.model };
|
|
57
|
+
}
|
|
58
|
+
// Not explicit: a role-level pin is the most specific thing said about this
|
|
59
|
+
// role, so it decides — and the reported model follows it.
|
|
60
|
+
if (rolePin !== undefined)
|
|
61
|
+
return { env, model: rolePin };
|
|
62
|
+
if (input.model !== undefined)
|
|
63
|
+
env[pin] = input.model;
|
|
64
|
+
return { env, model: input.model ?? env[pin] };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The model a role will actually run, read back from the environment it was
|
|
68
|
+
* resolved with. Use this wherever a model is reported to a human.
|
|
69
|
+
*/
|
|
70
|
+
export function effectiveRoleModel(role) {
|
|
71
|
+
const pin = modelEnvVar(role.harness);
|
|
72
|
+
return (pin ? role.env?.[pin] : undefined) ?? role.model;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
76
|
+
* role runs after resolution — model-chain recovery is the live example — must
|
|
77
|
+
* go through this, or it changes only the label.
|
|
78
|
+
*/
|
|
79
|
+
export function repinModelEnv(role, model) {
|
|
80
|
+
const pin = modelEnvVar(role.harness);
|
|
81
|
+
if (!pin)
|
|
82
|
+
return role.env;
|
|
83
|
+
const env = { ...(role.env ?? {}) };
|
|
84
|
+
if (model === undefined)
|
|
85
|
+
delete env[pin];
|
|
86
|
+
else
|
|
87
|
+
env[pin] = model;
|
|
88
|
+
return Object.keys(env).length ? env : undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Last line of defence, at the exact point a child's environment is composed:
|
|
92
|
+
* refuse to launch a role whose child would run a model other than the one the
|
|
93
|
+
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
94
|
+
* agreement must fail loudly, not start and be believed.
|
|
95
|
+
*/
|
|
96
|
+
export function assertModelPinReachesChild(role, childEnv) {
|
|
97
|
+
const pin = modelEnvVar(role.harness);
|
|
98
|
+
if (!pin || role.model === undefined)
|
|
99
|
+
return;
|
|
100
|
+
const actual = childEnv[pin];
|
|
101
|
+
if (actual === role.model)
|
|
102
|
+
return;
|
|
103
|
+
throw new Error(`[${role.name}] refusing to launch: role model is '${role.model}' but the child's `
|
|
104
|
+
+ `${pin} is ${actual === undefined ? 'unset' : `'${actual}'`} — the session would run a `
|
|
105
|
+
+ 'different model than the one reported');
|
|
106
|
+
}
|
package/dist/monitor.d.ts
CHANGED
|
@@ -122,7 +122,7 @@ export declare function filterEvents(events: NotifyEvent[], wakeSources: string[
|
|
|
122
122
|
/**
|
|
123
123
|
* Summarize a (coalesced) batch of events into one content-free console line —
|
|
124
124
|
* count + senders + ids, ending in the call to action. Falls back to compact
|
|
125
|
-
* counts when a burst would
|
|
125
|
+
* counts when a burst would exceed the length cap.
|
|
126
126
|
*/
|
|
127
127
|
export declare function formatNotificationLine(events: NotifyEvent[]): string;
|
|
128
128
|
/**
|
|
@@ -146,7 +146,7 @@ export declare function looksModal(pane: string): boolean;
|
|
|
146
146
|
* Heuristic: did the turn shown in this pane TERMINATE in an API-level error?
|
|
147
147
|
* Claude Code renders a failed turn's tail as an `API Error:` line (a Usage-Policy
|
|
148
148
|
* refusal, a 4xx, etc.). We scan a generous tail window so the marker survives a
|
|
149
|
-
* trailing idle composer redrawn beneath it
|
|
149
|
+
* trailing idle composer redrawn beneath it; refine empirically.
|
|
150
150
|
* The N-consecutive threshold in the Monitor debounces the odd false match.
|
|
151
151
|
*/
|
|
152
152
|
export declare function looksApiError(pane: string): boolean;
|
package/dist/monitor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { classifyFailureText } from './model-recovery.js';
|
|
5
|
-
// Code constants
|
|
5
|
+
// Code constants rather than user configuration.
|
|
6
6
|
const DEFAULT_PORT = 3050;
|
|
7
7
|
// The daemon normally holds for 25s, but that value is operator-configurable
|
|
8
8
|
// and synchronous daemon work can delay the response. The former 35s timer
|
|
@@ -191,7 +191,7 @@ const plural = (n, one, many = one + 's') => (n === 1 ? one : many);
|
|
|
191
191
|
/**
|
|
192
192
|
* Summarize a (coalesced) batch of events into one content-free console line —
|
|
193
193
|
* count + senders + ids, ending in the call to action. Falls back to compact
|
|
194
|
-
* counts when a burst would
|
|
194
|
+
* counts when a burst would exceed the length cap.
|
|
195
195
|
*/
|
|
196
196
|
export function formatNotificationLine(events) {
|
|
197
197
|
const of = (t) => events.filter(e => e.event === t);
|
|
@@ -229,7 +229,7 @@ export function formatNotificationLine(events) {
|
|
|
229
229
|
].filter(Boolean).join(', ');
|
|
230
230
|
return `${PREFIX} ${compact} — run get_messages`;
|
|
231
231
|
}
|
|
232
|
-
// ─── Modal-dialog detection
|
|
232
|
+
// ─── Modal-dialog detection, refined empirically ─────────────────────────────
|
|
233
233
|
//
|
|
234
234
|
// `❯` is Claude Code's ordinary composer prompt, so it is on screen in nearly
|
|
235
235
|
// every capture. Testing for it *anywhere* in the pane therefore says nothing;
|
|
@@ -285,7 +285,7 @@ export function looksModal(pane) {
|
|
|
285
285
|
* Heuristic: did the turn shown in this pane TERMINATE in an API-level error?
|
|
286
286
|
* Claude Code renders a failed turn's tail as an `API Error:` line (a Usage-Policy
|
|
287
287
|
* refusal, a 4xx, etc.). We scan a generous tail window so the marker survives a
|
|
288
|
-
* trailing idle composer redrawn beneath it
|
|
288
|
+
* trailing idle composer redrawn beneath it; refine empirically.
|
|
289
289
|
* The N-consecutive threshold in the Monitor debounces the odd false match.
|
|
290
290
|
*/
|
|
291
291
|
export function looksApiError(pane) {
|
package/dist/ops.d.ts
CHANGED
|
@@ -21,12 +21,12 @@ export interface OpsDeps {
|
|
|
21
21
|
* Called the INSTANT a registration is created, before anything else can
|
|
22
22
|
* fail. A creation transaction that learns about registrations only from
|
|
23
23
|
* `up()`'s return value learns nothing when `up()` throws — and the service
|
|
24
|
-
* it just registered is then invisible to rollback
|
|
24
|
+
* it just registered is then invisible to rollback. Optional: plain
|
|
25
25
|
* `ours-fleet up` has no transaction to tell.
|
|
26
26
|
*/
|
|
27
27
|
onInstalled?(outcome: InstallOutcome): void;
|
|
28
28
|
/**
|
|
29
|
-
* Optional: supervises the single watchdog-scheduler process
|
|
29
|
+
* Optional: supervises the single watchdog-scheduler process.
|
|
30
30
|
* Absent for callers that predate watchdogs — `up`/`down` must never throw
|
|
31
31
|
* just because this hook is missing.
|
|
32
32
|
*/
|
package/dist/ops.js
CHANGED
|
@@ -17,7 +17,7 @@ import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './
|
|
|
17
17
|
/** Materialize a role's state dir from config: briefing + markers. Returns the dir. */
|
|
18
18
|
export function applyRole(role, opts = {}) {
|
|
19
19
|
const adapter = getAdapter(role.harness);
|
|
20
|
-
const errs = adapter.validateOptions(role.harness_options);
|
|
20
|
+
const errs = adapter.validateOptions(role.harness_options, role);
|
|
21
21
|
if (errs.length)
|
|
22
22
|
throw new Error(`role '${role.name}': ` + errs.map(e => `${e.path}: ${e.message}`).join('; '));
|
|
23
23
|
const dir = agentDir(role.name, opts.temp === true);
|
|
@@ -62,7 +62,7 @@ export async function up(cfg, names, deps, configPath, identityGuarantee) {
|
|
|
62
62
|
// guessing "stopped" from an unanswered probe silently discards a live
|
|
63
63
|
// conversation.
|
|
64
64
|
// An explicit operator `up` is the sanctioned way to release a held-down
|
|
65
|
-
// role: the still-alive runner polls this file and resumes
|
|
65
|
+
// role: the still-alive runner polls this file and resumes.
|
|
66
66
|
resetRestartLedger(dir);
|
|
67
67
|
const live = await deps.backend.liveness(role.name)
|
|
68
68
|
.catch(e => ({ state: 'unknown', detail: e instanceof Error ? e.message : String(e) }));
|
|
@@ -71,7 +71,7 @@ export async function up(cfg, names, deps, configPath, identityGuarantee) {
|
|
|
71
71
|
else if (live.state === 'unknown')
|
|
72
72
|
deps.log(` ! ${role.name}: liveness unknown, keeping session context — ${live.detail}`);
|
|
73
73
|
// Report what each install actually did, so a creation transaction can undo
|
|
74
|
-
// only the registrations IT made
|
|
74
|
+
// only the registrations IT made. Announced immediately as well as
|
|
75
75
|
// returned: a later role in this same loop can throw, and the registrations
|
|
76
76
|
// already made must still be undoable.
|
|
77
77
|
const outcome = { ...await deps.backend.install(role.name, deps.binPath), role: role.name };
|
|
@@ -94,7 +94,7 @@ function stableStringify(value) {
|
|
|
94
94
|
return v;
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
97
|
+
/** SHA-256 over the resolved enabled-watchdog set, stable regardless of config source order. */
|
|
98
98
|
function watchdogFingerprint(enabled) {
|
|
99
99
|
const sorted = [...enabled].sort((a, b) => a.name.localeCompare(b.name));
|
|
100
100
|
return createHash('sha256').update(stableStringify(sorted)).digest('hex');
|
|
@@ -117,14 +117,14 @@ function writeFingerprint(fingerprint) {
|
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
119
|
* Install/start (or stop) the single supervised watchdog-scheduler process
|
|
120
|
-
*
|
|
120
|
+
* to match the config's enabled watchdogs. Runs on every `up`,
|
|
121
121
|
* including a named `up <Role>` — cheap and idempotent, and the alternative
|
|
122
122
|
* (only reconciling on a whole-fleet `up`) would leave a newly-enabled
|
|
123
123
|
* watchdog unscheduled until the next bare `up`. Never throws: a scheduler
|
|
124
124
|
* hiccup must not fail the role installs that already succeeded.
|
|
125
125
|
*
|
|
126
126
|
* Restarts ONLY when something the scheduler actually needs to pick up
|
|
127
|
-
* changed
|
|
127
|
+
* changed: an unconditional `restart()` on every `up` —
|
|
128
128
|
* including a named `up <SomeUnrelatedRole>` — interrupted whatever the
|
|
129
129
|
* scheduler was mid-run (a 15s stop timeout can kill a long check without a
|
|
130
130
|
* report). Two independent signals decide: `svc.install()`'s own `changed`
|
|
@@ -142,7 +142,7 @@ async function reconcileWatchdogScheduler(cfg, deps, configPath) {
|
|
|
142
142
|
return;
|
|
143
143
|
const enabled = cfg.watchdogs.filter(w => w.enabled);
|
|
144
144
|
try {
|
|
145
|
-
// Check supervised() before any stop
|
|
145
|
+
// Check supervised() before any stop: on an
|
|
146
146
|
// unsupervised platform/config, the scheduler was never installed, so
|
|
147
147
|
// stopping it is not just a no-op — on Linux, stop() throws for a unit
|
|
148
148
|
// that was never loaded. A watchdog-less fleet must not see that
|
|
@@ -162,7 +162,7 @@ async function reconcileWatchdogScheduler(cfg, deps, configPath) {
|
|
|
162
162
|
if (unitChanged || fingerprintChanged) {
|
|
163
163
|
// restart(), not start(): start() is a no-op on an already-active unit,
|
|
164
164
|
// so a config change (new/changed watchdogs) would never reach a
|
|
165
|
-
// scheduler that's already running
|
|
165
|
+
// scheduler that's already running.
|
|
166
166
|
await svc.restart();
|
|
167
167
|
deps.log(`↑ watchdogs scheduler (${enabled.map(w => w.name).join(', ')})`);
|
|
168
168
|
}
|
|
@@ -208,7 +208,7 @@ export async function down(cfg, names, deps) {
|
|
|
208
208
|
// Only a whole-fleet `down` (no names) stops the scheduler — stopping one
|
|
209
209
|
// named role is not a decision to stop watching the others (tolerate
|
|
210
210
|
// absence/errors: a never-installed scheduler must not fail `down`).
|
|
211
|
-
// supervised() gates the call itself
|
|
211
|
+
// supervised() gates the call itself: on an
|
|
212
212
|
// unsupervised platform/config there is nothing installed to stop, and
|
|
213
213
|
// Linux's stop() throws for a unit that was never loaded.
|
|
214
214
|
if (names.length === 0 && deps.watchdogService?.supervised()) {
|
|
@@ -217,6 +217,13 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
217
217
|
* CLI is asked to bounce this very process; neither can happen afterwards.
|
|
218
218
|
*/
|
|
219
219
|
private restartSelf;
|
|
220
|
+
/**
|
|
221
|
+
* The caller may itself be a room member. Persist and acknowledge acceptance
|
|
222
|
+
* before an external worker starts a saga that can retire this process.
|
|
223
|
+
*/
|
|
224
|
+
private closeRoomFromOwner;
|
|
225
|
+
/** Carry a task terminal request through a worker that survives this role. */
|
|
226
|
+
private terminalTaskFromOwner;
|
|
220
227
|
/** Code-point-safe tail of the worklog, or undefined when there is none. */
|
|
221
228
|
private readWorklogTail;
|
|
222
229
|
/** Bound harness-command output to a single outbound message. */
|
|
@@ -250,6 +257,16 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
250
257
|
private warnOwnerOfUnauthorizedSender;
|
|
251
258
|
private effectiveOwners;
|
|
252
259
|
private authorizationIntegrity;
|
|
260
|
+
/**
|
|
261
|
+
* Report what the session actually did with the prompt, not what the config
|
|
262
|
+
* asked for. `interrupt: true` used to be reported as "your request
|
|
263
|
+
* interrupted the previous task" unconditionally; the session now answers
|
|
264
|
+
* whether anything was cancelled, whether the request is queued behind
|
|
265
|
+
* earlier prompts, or whether it is held until the current task reaches a
|
|
266
|
+
* safe stopping point. Backends that report no delivery state keep the old
|
|
267
|
+
* queuedBehind-based wording.
|
|
268
|
+
*/
|
|
269
|
+
private acceptanceNotice;
|
|
253
270
|
private complete;
|
|
254
271
|
private commentsState;
|
|
255
272
|
/** Model-authored commentary only; raw protocol/tool data never reaches here. */
|
|
@@ -6,6 +6,7 @@ import { DEFAULT_OWNER_ATTACHMENT_MIME, canonicalCid, } from '../config.js';
|
|
|
6
6
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
7
7
|
import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, interruptOutcome, } from '../session/types.js';
|
|
8
8
|
import { VERSION } from '../version.js';
|
|
9
|
+
import { renderMarkdownFailure, renderMarkdownResult, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
9
10
|
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
|
|
10
11
|
import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
|
|
11
12
|
import { ownerNotices, } from './notices.js';
|
|
@@ -195,10 +196,13 @@ export class OwnerChannel {
|
|
|
195
196
|
? ' with interruption'
|
|
196
197
|
: event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
|
|
197
198
|
const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
|
|
199
|
+
const permission = event.permissionMode
|
|
200
|
+
? `; permission ${event.permissionMode.fleetMode}, native ${event.permissionMode.nativeMode}`
|
|
201
|
+
: '';
|
|
198
202
|
const inherited = event.inherited.length
|
|
199
203
|
? ` Supervisor inherited omitted defaults: ${event.inherited.join(', ')}.` : '';
|
|
200
204
|
await this.sendProactiveMessage(`🧑💻 ${event.caller} spawned ${event.lifetime} agent ${event.role} `
|
|
201
|
-
+ `(${event.harness}/${event.session}${model}; ${monitor}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
|
|
205
|
+
+ `(${event.harness}/${event.session}${model}; ${monitor}${permission}).${inherited}`, `fleet-spawn\0${event.creationActionId}`, 0);
|
|
202
206
|
});
|
|
203
207
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
204
208
|
return run;
|
|
@@ -789,17 +793,13 @@ export class OwnerChannel {
|
|
|
789
793
|
outbox = this.outboxDir(originWireId);
|
|
790
794
|
await mkdir(outbox, { recursive: true, mode: 0o700 });
|
|
791
795
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
792
|
-
const queued = await this.options.session.queuePrompt(this.ownerAttachmentPrompt(sender, originWireId, requestId,
|
|
796
|
+
const queued = await this.options.session.queuePrompt(this.ownerAttachmentPrompt(sender, originWireId, requestId, admitted, group.caption), {
|
|
793
797
|
interrupt: this.options.config.interrupt,
|
|
794
798
|
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
795
799
|
origin: { kind: 'owner', requestId,
|
|
796
800
|
...(group.caption ? { displayText: String(group.caption.text ?? '') } : {}) },
|
|
797
801
|
});
|
|
798
|
-
const accepted = this.
|
|
799
|
-
? ownerNotices.receivedInterrupting()
|
|
800
|
-
: queued.queuedBehind > 0
|
|
801
|
-
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
802
|
-
: ownerNotices.receivedStarted();
|
|
802
|
+
const accepted = this.acceptanceNotice(queued);
|
|
803
803
|
handledWireIds.forEach(wire => this.inFlight.add(wire));
|
|
804
804
|
const receipt = this.send(sender.id, accepted, originWireId).then(() => undefined).catch(error => {
|
|
805
805
|
this.logError(`attachment request ${requestId.slice(0, 12)} acceptance notice failed`, error);
|
|
@@ -918,7 +918,7 @@ export class OwnerChannel {
|
|
|
918
918
|
let queued;
|
|
919
919
|
const activityCursor = this.latestEventSeq(this.options.session.eventsSince(0));
|
|
920
920
|
try {
|
|
921
|
-
queued = await this.options.session.queuePrompt(this.ownerPrompt(sender, text, wireId
|
|
921
|
+
queued = await this.options.session.queuePrompt(this.ownerPrompt(sender, text, wireId), {
|
|
922
922
|
interrupt: this.options.config.interrupt,
|
|
923
923
|
...(this.options.config.interrupt ? { interruptSource: 'owner' } : {}),
|
|
924
924
|
origin: { kind: 'owner', requestId, displayText: text },
|
|
@@ -940,14 +940,7 @@ export class OwnerChannel {
|
|
|
940
940
|
this.state.remember(wireId);
|
|
941
941
|
return true;
|
|
942
942
|
}
|
|
943
|
-
|
|
944
|
-
// accepted into the ACP queue. Never claim this request is running while
|
|
945
|
-
// the session itself says earlier work remains ahead of it.
|
|
946
|
-
const accepted = queued.queuedBehind > 0
|
|
947
|
-
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
948
|
-
: this.options.config.interrupt
|
|
949
|
-
? ownerNotices.receivedInterrupting()
|
|
950
|
-
: ownerNotices.receivedStarted();
|
|
943
|
+
const accepted = this.acceptanceNotice(queued);
|
|
951
944
|
this.inFlight.add(wireId);
|
|
952
945
|
const receipt = this.send(sender.id, accepted, wireId).then(() => undefined).catch(error => {
|
|
953
946
|
this.logError(`request ${requestId.slice(0, 12)} acceptance notice failed`, error);
|
|
@@ -996,6 +989,8 @@ export class OwnerChannel {
|
|
|
996
989
|
return this.commentsState();
|
|
997
990
|
},
|
|
998
991
|
fleetList: () => this.fleetOps.list(),
|
|
992
|
+
closeRoom: roomId => this.closeRoomFromOwner(sender, roomId, wireId),
|
|
993
|
+
terminalTask: (taskId, kind, outcome) => this.terminalTaskFromOwner(sender, taskId, kind, outcome, wireId),
|
|
999
994
|
recentEvents: limit => this.options.session.eventsSince(0).slice(-limit),
|
|
1000
995
|
readWorklogTail: maxChars => this.readWorklogTail(maxChars),
|
|
1001
996
|
reply: async (replyText) => { await this.send(sender.id, replyText, wireId); },
|
|
@@ -1046,6 +1041,60 @@ export class OwnerChannel {
|
|
|
1046
1041
|
this.options.log(`[${this.options.role}] owner requested ${command}`);
|
|
1047
1042
|
await this.fleetOps.restart(mode);
|
|
1048
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* The caller may itself be a room member. Persist and acknowledge acceptance
|
|
1046
|
+
* before an external worker starts a saga that can retire this process.
|
|
1047
|
+
*/
|
|
1048
|
+
async closeRoomFromOwner(sender, roomId, wireId) {
|
|
1049
|
+
const { acceptManagedRoomClose, recordManagedRoomCloseError } = await import('../rooms-tasks/close.js');
|
|
1050
|
+
await acceptManagedRoomClose(roomId);
|
|
1051
|
+
await this.send(sender.id, renderMarkdownFailure({
|
|
1052
|
+
kind: 'pending', subject: `/room delete ${roomId} ${roomId}`,
|
|
1053
|
+
detail: 'The deletion request was accepted and is still being settled.',
|
|
1054
|
+
action: `Run /room recover ${roomId} if deletion remains pending.`,
|
|
1055
|
+
}), wireId);
|
|
1056
|
+
this.state.remember(wireId);
|
|
1057
|
+
try {
|
|
1058
|
+
await this.fleetOps.closeRoom(roomId);
|
|
1059
|
+
}
|
|
1060
|
+
catch (error) {
|
|
1061
|
+
await recordManagedRoomCloseError(roomId, error instanceof Error ? error.message : String(error), `External delete worker failed to start. Retry /room delete ${roomId} ${roomId}.`);
|
|
1062
|
+
throw error;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/** Carry a task terminal request through a worker that survives this role. */
|
|
1066
|
+
async terminalTaskFromOwner(sender, taskId, kind, outcome, wireId) {
|
|
1067
|
+
const { getTask } = await import('../rooms-tasks/task-state.js');
|
|
1068
|
+
const { acceptTaskTerminalIntent, recordTaskTerminalIntentError, } = await import('../rooms-tasks/terminal.js');
|
|
1069
|
+
const task = getTask(taskId);
|
|
1070
|
+
const accepted = await acceptTaskTerminalIntent({
|
|
1071
|
+
taskId, kind, roomId: task.room_id, outcome,
|
|
1072
|
+
});
|
|
1073
|
+
if (accepted.terminal_intent?.status === 'settled') {
|
|
1074
|
+
await this.send(sender.id, renderMarkdownResult({
|
|
1075
|
+
icon: '📋', title: 'Task terminal action complete',
|
|
1076
|
+
fields: [
|
|
1077
|
+
{ label: 'ID', value: taskId, kind: 'code' },
|
|
1078
|
+
{ label: 'Status', value: taskStatus(accepted.state), kind: 'markdown' },
|
|
1079
|
+
],
|
|
1080
|
+
}), wireId);
|
|
1081
|
+
this.state.remember(wireId);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
await this.send(sender.id, renderMarkdownFailure({
|
|
1085
|
+
kind: 'pending', subject: `/task ${kind === 'done' ? 'done' : 'cancel'} ${taskId}`,
|
|
1086
|
+
detail: 'The terminal request was accepted and is still being settled.',
|
|
1087
|
+
action: `Run /task recover ${taskId} if it remains pending.`,
|
|
1088
|
+
}), wireId);
|
|
1089
|
+
this.state.remember(wireId);
|
|
1090
|
+
try {
|
|
1091
|
+
await this.fleetOps.settleTask(taskId);
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
await recordTaskTerminalIntentError(taskId, error instanceof Error ? error.message : String(error), `External settle worker failed to start. Retry the identical task command or run task recover ${taskId}.`);
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1049
1098
|
/** Code-point-safe tail of the worklog, or undefined when there is none. */
|
|
1050
1099
|
async readWorklogTail(maxChars) {
|
|
1051
1100
|
try {
|
|
@@ -1332,6 +1381,32 @@ export class OwnerChannel {
|
|
|
1332
1381
|
authorizationIntegrity() {
|
|
1333
1382
|
return this.options.config.agent ? { ok: true } : this.authorizations.integrity();
|
|
1334
1383
|
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Report what the session actually did with the prompt, not what the config
|
|
1386
|
+
* asked for. `interrupt: true` used to be reported as "your request
|
|
1387
|
+
* interrupted the previous task" unconditionally; the session now answers
|
|
1388
|
+
* whether anything was cancelled, whether the request is queued behind
|
|
1389
|
+
* earlier prompts, or whether it is held until the current task reaches a
|
|
1390
|
+
* safe stopping point. Backends that report no delivery state keep the old
|
|
1391
|
+
* queuedBehind-based wording.
|
|
1392
|
+
*/
|
|
1393
|
+
acceptanceNotice(queued) {
|
|
1394
|
+
switch (queued.delivery) {
|
|
1395
|
+
case 'interrupted': return ownerNotices.receivedInterrupting();
|
|
1396
|
+
case 'deferred': return ownerNotices.receivedDeferred();
|
|
1397
|
+
case 'queued': return ownerNotices.receivedQueued(Math.max(1, queued.queuedBehind));
|
|
1398
|
+
case 'started': return ownerNotices.receivedStarted();
|
|
1399
|
+
default:
|
|
1400
|
+
// Interrupting the live turn does not remove prompts which were already
|
|
1401
|
+
// accepted into the ACP queue. Never claim this request is running while
|
|
1402
|
+
// the session itself says earlier work remains ahead of it.
|
|
1403
|
+
return queued.queuedBehind > 0
|
|
1404
|
+
? ownerNotices.receivedQueued(queued.queuedBehind)
|
|
1405
|
+
: this.options.config.interrupt
|
|
1406
|
+
? ownerNotices.receivedInterrupting()
|
|
1407
|
+
: ownerNotices.receivedStarted();
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1335
1410
|
async complete(active, outbox, queued, activityCursor) {
|
|
1336
1411
|
const progressMs = this.options.config.progress_interval_ms;
|
|
1337
1412
|
let lastSeq = activityCursor;
|
|
@@ -1532,7 +1607,7 @@ export class OwnerChannel {
|
|
|
1532
1607
|
throw new Error('commentary appears to contain secret, reasoning, or raw tool content');
|
|
1533
1608
|
return message;
|
|
1534
1609
|
}
|
|
1535
|
-
ownerAttachmentPrompt(sender, wireId, requestId,
|
|
1610
|
+
ownerAttachmentPrompt(sender, wireId, requestId, files, caption) {
|
|
1536
1611
|
const lines = [
|
|
1537
1612
|
'[fleet-owner]',
|
|
1538
1613
|
`Authenticated owner ${safeField(sender.name, 160)} (${sender.id}) sent owner-channel attachment request ${wireId}.`,
|
|
@@ -1556,10 +1631,13 @@ export class OwnerChannel {
|
|
|
1556
1631
|
lines.push(`- voice transcript status: ${transcription?.status ?? 'unavailable'}`, `- voice transcript fallback: audio path above; category ${transcription?.errorCategory ?? 'not_provided'}`);
|
|
1557
1632
|
}
|
|
1558
1633
|
}
|
|
1559
|
-
lines.push('Answer in your final assistant response; fleet routes it only to the authenticated sender and correlates it to the originating file wire.',
|
|
1634
|
+
lines.push('Answer in your final assistant response; fleet routes it only to the authenticated sender and correlates it to the originating file wire.',
|
|
1635
|
+
// send_file is the only delivery route an agent is given: a tool call either
|
|
1636
|
+
// delivers or reports an error, where a file written to disk does neither.
|
|
1637
|
+
'To send the owner a file — now or later in this turn — call ours `send_file`:', `contact: ${this.options.config.identity}`, 'and the path of the finished file. Fleet routes it to the authenticated owner.', 'A file written anywhere else is not delivered and nothing will report that it was not.', 'Use descriptive unique filenames. Send nothing the owner did not request or should not receive.');
|
|
1560
1638
|
return lines.join('\n');
|
|
1561
1639
|
}
|
|
1562
|
-
ownerPrompt(sender, text, wireId
|
|
1640
|
+
ownerPrompt(sender, text, wireId) {
|
|
1563
1641
|
return [
|
|
1564
1642
|
'[fleet-owner]',
|
|
1565
1643
|
`Authenticated owner ${sender.name} (${sender.id}) sent owner-channel message ${wireId}.`,
|
|
@@ -1573,10 +1651,13 @@ export class OwnerChannel {
|
|
|
1573
1651
|
] : [
|
|
1574
1652
|
'Managed-agent outbound relay is not configured; do not send intermediate or proactive owner-channel messages.',
|
|
1575
1653
|
]),
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
'
|
|
1579
|
-
|
|
1654
|
+
// send_file is the only delivery route an agent is given: a tool call either
|
|
1655
|
+
// delivers or reports an error, where a file written to disk does neither.
|
|
1656
|
+
'To send the owner a file — now or later in this turn — call ours `send_file`:',
|
|
1657
|
+
`contact: ${this.options.config.identity}`,
|
|
1658
|
+
'and the path of the finished file. Fleet routes it to the authenticated owner.',
|
|
1659
|
+
'A file written anywhere else is not delivered and nothing will report that it was not.',
|
|
1660
|
+
'Use descriptive unique filenames. Send nothing the owner did not request or should not receive.',
|
|
1580
1661
|
'',
|
|
1581
1662
|
text || '(empty message)',
|
|
1582
1663
|
].join('\n');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { InterruptOutcome, SessionEvent, SessionSnapshot } from '../session/types.js';
|
|
2
|
+
import type { TaskOutcome, TaskTerminalIntent } from '../rooms-tasks/types.js';
|
|
2
3
|
import { type OwnerCommentsState } from './notices.js';
|
|
3
4
|
/**
|
|
4
5
|
* Fleet-level effects a deterministic owner command may trigger. Production
|
|
@@ -10,6 +11,10 @@ export interface OwnerFleetOps {
|
|
|
10
11
|
restart(mode: 'keep' | 'fresh'): Promise<void>;
|
|
11
12
|
/** `ours-fleet ls` output. */
|
|
12
13
|
list(): Promise<string>;
|
|
14
|
+
/** Start a room-close worker outside the caller role's supervisor lifecycle. */
|
|
15
|
+
closeRoom(roomId: string): Promise<void>;
|
|
16
|
+
/** Resume a task terminal intent outside the caller role's supervisor lifecycle. */
|
|
17
|
+
settleTask(taskId: string): Promise<void>;
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
15
20
|
* The narrow capability surface a command executor sees. Everything here is
|
|
@@ -40,6 +45,10 @@ export interface OwnerCommandContext {
|
|
|
40
45
|
*/
|
|
41
46
|
setComments(enabled: boolean): OwnerCommentsState;
|
|
42
47
|
fleetList(): Promise<string>;
|
|
48
|
+
/** Persist acceptance, acknowledge it, then launch the external close worker. */
|
|
49
|
+
closeRoom(roomId: string): Promise<void>;
|
|
50
|
+
/** Persist terminal intent, acknowledge it, then launch the external settle worker. */
|
|
51
|
+
terminalTask(taskId: string, kind: TaskTerminalIntent['kind'], outcome?: TaskOutcome): Promise<void>;
|
|
43
52
|
recentEvents(limit: number): SessionEvent[];
|
|
44
53
|
readWorklogTail(maxChars: number): Promise<string | undefined>;
|
|
45
54
|
reply(text: string): Promise<void>;
|