@ours.network/fleet 1.0.5 → 1.1.0-nightly.10

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 (159) hide show
  1. package/README.md +122 -133
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.d.ts +0 -1
  5. package/dist/application/capabilities.js +3 -13
  6. package/dist/application/fleet-query-service.d.ts +0 -3
  7. package/dist/application/fleet-query-service.js +14 -25
  8. package/dist/application/role-creation-service.d.ts +5 -24
  9. package/dist/application/role-creation-service.js +27 -67
  10. package/dist/application/role-removal-service.js +6 -3
  11. package/dist/application/role-repository.d.ts +1 -2
  12. package/dist/application/role-repository.js +7 -7
  13. package/dist/application/session-control.d.ts +0 -12
  14. package/dist/application/session-control.js +0 -42
  15. package/dist/application/session-mutations.d.ts +5 -5
  16. package/dist/application/task-room-service.d.ts +1 -1
  17. package/dist/application/task-room-service.js +154 -16
  18. package/dist/application/types.d.ts +1 -8
  19. package/dist/briefing.js +12 -11
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +143 -63
  22. package/dist/config-yaml.d.ts +1 -1
  23. package/dist/config.d.ts +66 -6
  24. package/dist/config.js +398 -43
  25. package/dist/creation.d.ts +1 -1
  26. package/dist/creation.js +2 -2
  27. package/dist/daemon-recovery.d.ts +93 -0
  28. package/dist/daemon-recovery.js +328 -0
  29. package/dist/docs.d.ts +1 -1
  30. package/dist/docs.js +103 -127
  31. package/dist/doctor.js +18 -11
  32. package/dist/fleet-command-audit.d.ts +160 -0
  33. package/dist/fleet-command-audit.js +433 -0
  34. package/dist/fleet-proxy.d.ts +3 -1
  35. package/dist/fleet-proxy.js +30 -8
  36. package/dist/generated-agent-source.d.ts +9 -0
  37. package/dist/generated-agent-source.js +53 -0
  38. package/dist/harness/acp-session-transport.d.ts +8 -0
  39. package/dist/harness/acp-session-transport.js +3 -0
  40. package/dist/harness/agent-session.d.ts +51 -0
  41. package/dist/harness/agent-session.js +1 -0
  42. package/dist/harness/claude-code-session.d.ts +24 -0
  43. package/dist/harness/claude-code-session.js +36 -0
  44. package/dist/harness/claude-code.d.ts +2 -1
  45. package/dist/harness/claude-code.js +41 -81
  46. package/dist/harness/codex-session.d.ts +24 -0
  47. package/dist/harness/codex-session.js +36 -0
  48. package/dist/harness/codex.d.ts +2 -1
  49. package/dist/harness/codex.js +55 -45
  50. package/dist/harness/registry.js +4 -0
  51. package/dist/harness/types.d.ts +6 -38
  52. package/dist/index.d.ts +4 -6
  53. package/dist/index.js +2 -4
  54. package/dist/init-guidance.d.ts +1 -0
  55. package/dist/init-guidance.js +6 -0
  56. package/dist/isolation/resources.d.ts +3 -7
  57. package/dist/isolation/resources.js +3 -7
  58. package/dist/model-env.d.ts +4 -6
  59. package/dist/model-env.js +5 -8
  60. package/dist/monitor.d.ts +1 -24
  61. package/dist/monitor.js +5 -140
  62. package/dist/ops.js +4 -2
  63. package/dist/owner-channel/channel.d.ts +58 -2
  64. package/dist/owner-channel/channel.js +269 -29
  65. package/dist/owner-channel/commands.d.ts +2 -0
  66. package/dist/owner-channel/commands.js +31 -13
  67. package/dist/owner-channel/state.d.ts +2 -0
  68. package/dist/owner-channel/state.js +8 -2
  69. package/dist/preset-bootstrap.d.ts +10 -0
  70. package/dist/preset-bootstrap.js +85 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.d.ts +3 -2
  84. package/dist/resolved-plan.js +21 -5
  85. package/dist/rooms-tasks/cli.js +225 -40
  86. package/dist/rooms-tasks/config.js +24 -13
  87. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  88. package/dist/rooms-tasks/external-worker.js +14 -9
  89. package/dist/rooms-tasks/provision.js +98 -36
  90. package/dist/rooms-tasks/templates.d.ts +0 -1
  91. package/dist/rooms-tasks/templates.js +9 -76
  92. package/dist/rooms-tasks/types.d.ts +10 -16
  93. package/dist/rooms-tasks/types.js +1 -4
  94. package/dist/runner.d.ts +13 -16
  95. package/dist/runner.js +110 -122
  96. package/dist/sensitive-config.d.ts +2 -0
  97. package/dist/sensitive-config.js +4 -0
  98. package/dist/session/acp.d.ts +7 -2
  99. package/dist/session/acp.js +29 -6
  100. package/dist/session/activity.d.ts +2 -3
  101. package/dist/session/activity.js +2 -3
  102. package/dist/session/arbiter.d.ts +4 -4
  103. package/dist/session/control.d.ts +17 -4
  104. package/dist/session/control.js +35 -0
  105. package/dist/session/types.d.ts +11 -5
  106. package/dist/spawn.d.ts +8 -33
  107. package/dist/spawn.js +53 -213
  108. package/dist/supervisor/none.d.ts +1 -1
  109. package/dist/supervisor/none.js +2 -2
  110. package/dist/temp-lifecycle.d.ts +1 -1
  111. package/dist/tmux.d.ts +1 -14
  112. package/dist/tmux.js +1 -51
  113. package/dist/watchdog/config.d.ts +10 -7
  114. package/dist/watchdog/config.js +30 -18
  115. package/dist/watchdog/run.js +12 -36
  116. package/dist/web/auth.d.ts +1 -1
  117. package/dist/web/fleet-config-service.d.ts +11 -8
  118. package/dist/web/fleet-config-service.js +384 -183
  119. package/dist/web/runtime.js +4 -15
  120. package/dist/web/server.d.ts +1 -3
  121. package/dist/web/server.js +3 -14
  122. package/dist/web/topology-promote.js +26 -19
  123. package/dist/web/topology.js +2 -2
  124. package/dist/web/yaml-document-edit.js +2 -0
  125. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  126. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  127. package/dist/web-app/index.html +2 -2
  128. package/examples/fleet/agents/Alice.yaml +11 -0
  129. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  130. package/examples/fleet/brains/claude-default.yaml +5 -0
  131. package/examples/fleet/roles/coordinator.yaml +5 -0
  132. package/examples/fleet/roles/developer.yaml +3 -0
  133. package/examples/fleet.yaml +74 -0
  134. package/package.json +5 -8
  135. package/presets/fleet/agents/Agent.yaml +3 -0
  136. package/presets/fleet/agents/Architect.yaml +3 -0
  137. package/presets/fleet/agents/Critic.yaml +3 -0
  138. package/presets/fleet/agents/Developer.yaml +3 -0
  139. package/presets/fleet/agents/Secretary.yaml +3 -0
  140. package/presets/fleet/agents/Tester.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/roles/Agent.yaml +6 -0
  143. package/presets/fleet/roles/Architect.yaml +6 -0
  144. package/presets/fleet/roles/Critic.yaml +6 -0
  145. package/presets/fleet/roles/Developer.yaml +6 -0
  146. package/presets/fleet/roles/Secretary.yaml +6 -0
  147. package/presets/fleet/roles/Tester.yaml +6 -0
  148. package/presets/fleet/room_templates/pair.yaml +11 -0
  149. package/presets/fleet/room_templates/single.yaml +8 -0
  150. package/presets/fleet/room_templates/team.yaml +12 -0
  151. package/presets/fleet.yaml +7 -0
  152. package/presets/manifest.json +5 -0
  153. package/dist/session/tmux.d.ts +0 -28
  154. package/dist/session/tmux.js +0 -80
  155. package/dist/web/terminal/bridge.d.ts +0 -27
  156. package/dist/web/terminal/bridge.js +0 -317
  157. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  158. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  159. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
@@ -1,7 +1,7 @@
1
1
  import { Tmux, tmuxArgs } from '../tmux.js';
2
2
  import { realExec, shq } from '../exec.js';
3
3
  /**
4
- * No supervision: sessions are plain tmux, nothing survives a reboot and
4
+ * No service supervision: tmux contains the direct runner process, nothing survives a reboot and
5
5
  * nothing restarts on crash. Used for temp agents and CI tests
6
6
  * (OURS_FLEET_SUPERVISOR=none).
7
7
  */
@@ -9,7 +9,7 @@ export function makeNoneBackend(exec = realExec) {
9
9
  const tmux = new Tmux(exec);
10
10
  return {
11
11
  id: 'none',
12
- async init() { return ['no supervisor: sessions are plain tmux (no reboot survival)']; },
12
+ async init() { return ['no service supervisor: direct runner containment only (no reboot survival)']; },
13
13
  async install(name, binPath) {
14
14
  const existed = await tmux.kill(name); // true when a session was there
15
15
  await tmux.newSession(name, process.cwd(), `${shq(binPath)} _run ${shq(name)}`);
@@ -5,7 +5,7 @@ export declare const TEMP_STOP_REQUEST_FILE = ".temp-stop-request.json";
5
5
  export declare const TEMP_RECLAIM_BATCH = 32;
6
6
  export declare const TEMP_LAUNCH_GRACE_MS = 60000;
7
7
  export type TempSupervisorKind = 'systemd-transient' | 'launchd-transient' | 'detached';
8
- export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'startup-failure' | 'stale-supervisor';
8
+ export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'supervisor-recycle' | 'startup-failure' | 'stale-supervisor';
9
9
  export interface TempSupervisorRecord {
10
10
  version: 1;
11
11
  role: string;
package/dist/tmux.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare const tmuxSocket: (session: string) => string;
20
20
  * the shared default server and re-creates #32.
21
21
  */
22
22
  export declare const tmuxArgs: (session: string, args: string[]) => string[];
23
- /** Thin tmux wrapper; all session handling in the core goes through this. */
23
+ /** Thin tmux wrapper retained only for supervisor=none process containment. */
24
24
  export declare class Tmux {
25
25
  private exec;
26
26
  constructor(exec?: Exec);
@@ -32,17 +32,4 @@ export declare class Tmux {
32
32
  * program inside it exited on its own".
33
33
  */
34
34
  kill(name: string): Promise<boolean>;
35
- capture(name: string, lines?: number): Promise<string>;
36
- /** Bounded ANSI/history seed for the browser terminal projection. */
37
- captureHistory(name: string, lines?: number): Promise<string>;
38
- panePid(name: string): Promise<number | null>;
39
- /**
40
- * List the live sessions among `names`, asking each server in turn.
41
- * There is no fleet-wide `tmux ls` any more: a session per server means the
42
- * caller says which sessions to ask about. A server that is not running
43
- * answers non-zero and contributes nothing.
44
- */
45
- list(names: readonly string[]): Promise<string>;
46
- sendText(name: string, text: string): Promise<void>;
47
- sendKey(name: string, key: string): Promise<void>;
48
35
  }
package/dist/tmux.js CHANGED
@@ -20,7 +20,7 @@ export const tmuxSocket = (session) => `${TMUX_SOCKET_PREFIX}${session}`;
20
20
  * the shared default server and re-creates #32.
21
21
  */
22
22
  export const tmuxArgs = (session, args) => ['-L', tmuxSocket(session), ...args];
23
- /** Thin tmux wrapper; all session handling in the core goes through this. */
23
+ /** Thin tmux wrapper retained only for supervisor=none process containment. */
24
24
  export class Tmux {
25
25
  exec;
26
26
  constructor(exec = realExec) {
@@ -42,54 +42,4 @@ export class Tmux {
42
42
  async kill(name) {
43
43
  return (await this.exec('tmux', tmuxArgs(name, ['kill-session', '-t', name]))).code === 0;
44
44
  }
45
- async capture(name, lines = 40) {
46
- const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p']));
47
- if (r.code !== 0)
48
- throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
49
- const all = r.stdout.replace(/\n+$/, '').split('\n');
50
- return all.slice(-lines).join('\n');
51
- }
52
- /** Bounded ANSI/history seed for the browser terminal projection. */
53
- async captureHistory(name, lines = 5_000) {
54
- const bounded = Math.min(Math.max(Math.trunc(lines), 1), 20_000);
55
- const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p', '-e', '-J', '-S', `-${bounded}`]));
56
- if (r.code !== 0)
57
- throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
58
- return Buffer.from(r.stdout).subarray(0, 4 * 1024 * 1024).toString();
59
- }
60
- async panePid(name) {
61
- const r = await this.exec('tmux', tmuxArgs(name, ['list-panes', '-t', name, '-F', '#{pane_pid}']));
62
- if (r.code !== 0)
63
- return null;
64
- const pid = parseInt(r.stdout.trim().split('\n')[0], 10);
65
- return Number.isFinite(pid) ? pid : null;
66
- }
67
- /**
68
- * List the live sessions among `names`, asking each server in turn.
69
- * There is no fleet-wide `tmux ls` any more: a session per server means the
70
- * caller says which sessions to ask about. A server that is not running
71
- * answers non-zero and contributes nothing.
72
- */
73
- async list(names) {
74
- const lines = [];
75
- for (const name of names) {
76
- const r = await this.exec('tmux', tmuxArgs(name, ['ls']));
77
- if (r.code === 0 && r.stdout.trim())
78
- lines.push(r.stdout.trimEnd());
79
- }
80
- return lines.join('\n');
81
- }
82
- async sendText(name, text) {
83
- let r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, '-l', text]));
84
- if (r.code !== 0)
85
- throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
86
- r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, 'Enter']));
87
- if (r.code !== 0)
88
- throw new Error(`tmux send-keys Enter '${name}' failed: ${r.stderr.trim()}`);
89
- }
90
- async sendKey(name, key) {
91
- const r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, key]));
92
- if (r.code !== 0)
93
- throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
94
- }
95
45
  }
@@ -1,19 +1,19 @@
1
- import type { FleetConfig, ResolvedRole, SessionBackendId } from '../config.js';
1
+ import type { AgentDefinition, FleetConfig, ResolvedRole } from '../config.js';
2
+ import type { SessionBackendId } from '../config.js';
2
3
  import type { IsolationConfig } from '../isolation/types.js';
3
4
  export interface WatchdogConfig {
4
5
  coordinator?: string;
5
6
  enabled?: boolean;
6
7
  interval?: string;
7
8
  watch?: string[];
8
- harness?: string;
9
- model?: string | null;
10
- session?: string;
9
+ agent?: {
10
+ ref: string;
11
+ } | AgentDefinition;
11
12
  identity?: string;
12
13
  timeout?: string;
13
14
  keep_reports?: number;
14
15
  alert_cooldown?: string;
15
16
  prompt_file?: string;
16
- isolation?: IsolationConfig;
17
17
  }
18
18
  export interface ResolvedWatchdog {
19
19
  name: string;
@@ -22,15 +22,18 @@ export interface ResolvedWatchdog {
22
22
  intervalMs: number;
23
23
  watch: string[];
24
24
  watchExplicit: boolean;
25
+ agentDefinition: AgentDefinition;
26
+ resolvedAgent: ResolvedRole;
27
+ /** Resolved/read-only runtime facts; never independent authoring fields. */
25
28
  harness: string;
26
29
  session: SessionBackendId;
27
30
  model?: string;
31
+ isolation?: IsolationConfig;
28
32
  identity: string;
29
33
  timeoutMs: number;
30
34
  keepReports: number;
31
35
  alertCooldownMs: number;
32
36
  promptFile?: string;
33
- isolation?: IsolationConfig;
34
37
  sourceFile: string;
35
38
  }
36
39
  export declare const WATCHDOG_DEFAULT_INTERVAL_MS = 600000;
@@ -38,7 +41,7 @@ export declare const WATCHDOG_MIN_INTERVAL_MS = 60000;
38
41
  export declare const WATCHDOG_DEFAULT_TIMEOUT_MS = 300000;
39
42
  export declare const WATCHDOG_DEFAULT_KEEP_REPORTS = 50;
40
43
  export declare const WATCHDOG_DEFAULT_COOLDOWN_MS = 3600000;
41
- export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, defaults: Record<string, unknown>): ResolvedWatchdog[];
44
+ export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, agentDefinitions: Record<string, AgentDefinition>, resolveInline: (name: string, definition: AgentDefinition) => ResolvedRole): ResolvedWatchdog[];
42
45
  /**
43
46
  * Split `restart`'s argument names into watchdogs (a release, handled
44
47
  * directly) and roles (handed to restartRoles). Watchdog names can't collide
@@ -1,11 +1,10 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { isAbsolute } from 'node:path';
3
3
  import { parseDuration } from '../duration.js';
4
- import { ConfigError, ROLE_NAME_RE, resolveRoleModel } from '../config.js';
5
- import { validateIsolationConfig } from '../isolation/policy.js';
4
+ import { ConfigError, ROLE_NAME_RE } from '../config.js';
6
5
  const WATCHDOG_KEYS = [
7
- 'coordinator', 'enabled', 'interval', 'watch', 'harness', 'model', 'session',
8
- 'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file', 'isolation',
6
+ 'coordinator', 'enabled', 'interval', 'watch', 'agent',
7
+ 'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file',
9
8
  ];
10
9
  export const WATCHDOG_DEFAULT_INTERVAL_MS = 600_000;
11
10
  export const WATCHDOG_MIN_INTERVAL_MS = 60_000;
@@ -21,7 +20,7 @@ function deepSub(v, vars) {
21
20
  return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deepSub(x, vars)]));
22
21
  return v;
23
22
  }
24
- export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
23
+ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, agentDefinitions, resolveInline) {
25
24
  const block = baseDoc.watchdogs;
26
25
  if (block === undefined || block === null)
27
26
  return [];
@@ -31,7 +30,7 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
31
30
  const roleIdentities = new Set(roles.map(r => r.identity));
32
31
  // Tracks identity -> owning watchdog name across entries:
33
32
  // two watchdogs declaring the same identity would otherwise share a temp
34
- // dir, tmux session, and run lock with no error until they collide at
33
+ // dir, agent session, and run lock with no error until they collide at
35
34
  // runtime.
36
35
  const watchdogIdentities = new Map();
37
36
  const out = [];
@@ -42,6 +41,9 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
42
41
  if (roleNames.has(name))
43
42
  throw new ConfigError(`${baseFile}: watchdog '${name}' collides with a role name`);
44
43
  const w = deepSub(rawEntry ?? {}, vars);
44
+ const legacy = ['harness', 'model', 'session', 'isolation'].filter(key => Object.hasOwn(w, key));
45
+ if (legacy.length)
46
+ throw new ConfigError(`${where}: E_LEGACY Brain/Agent-owned field(s) ${legacy.join(', ')} are unsupported; select a canonical Agent with agent`);
45
47
  const bad = Object.keys(w).filter(k => !WATCHDOG_KEYS.includes(k));
46
48
  if (bad.length)
47
49
  throw new ConfigError(`${where} has unknown key(s) ${bad.join(', ')}; allowed: ${WATCHDOG_KEYS.join(', ')}`);
@@ -60,21 +62,29 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
60
62
  if (collidingWatchdog !== undefined)
61
63
  throw new ConfigError(`${where}: identity '${identity}' collides with watchdog '${collidingWatchdog}'`);
62
64
  watchdogIdentities.set(identity, name);
63
- const harness = w.harness ?? defaults.harness ?? 'claude-code';
64
- const sessionRaw = w.session ?? defaults.session ?? 'tmux';
65
- if (sessionRaw !== 'tmux' && sessionRaw !== 'acp')
66
- throw new ConfigError(`${where}: session must be 'tmux' or 'acp'`);
65
+ if (!w.agent || typeof w.agent !== 'object' || Array.isArray(w.agent))
66
+ throw new ConfigError(`${where}: agent is required (Agent ref or canonical Agent definition)`);
67
+ let agentDefinition;
68
+ let resolvedAgent;
69
+ if ('ref' in w.agent) {
70
+ const ref = w.agent.ref;
71
+ if (typeof ref !== 'string' || !ref)
72
+ throw new ConfigError(`${where}: agent.ref must be a declared Agent ID`);
73
+ agentDefinition = agentDefinitions[ref];
74
+ resolvedAgent = roles.find(role => role.name === ref);
75
+ if (!agentDefinition || !resolvedAgent)
76
+ throw new ConfigError(`${where}: Agent ref '${ref}' was not found`);
77
+ }
78
+ else {
79
+ agentDefinition = structuredClone(w.agent);
80
+ resolvedAgent = resolveInline(name, agentDefinition);
81
+ }
67
82
  if (w.prompt_file !== undefined) {
68
83
  if (typeof w.prompt_file !== 'string' || !isAbsolute(w.prompt_file))
69
84
  throw new ConfigError(`${where}: prompt_file must be an absolute path`);
70
85
  if (!existsSync(w.prompt_file))
71
86
  throw new ConfigError(`${where}: prompt_file not found: ${w.prompt_file}`);
72
87
  }
73
- if (w.isolation !== undefined) {
74
- const problems = validateIsolationConfig(w.isolation);
75
- if (problems.length)
76
- throw new ConfigError(`${where} ${problems.join('; ')}`);
77
- }
78
88
  const dur = (v, key, fallback, minMs) => {
79
89
  if (v === undefined)
80
90
  return fallback;
@@ -94,13 +104,15 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
94
104
  name, coordinator: w.coordinator.trim(),
95
105
  enabled: w.enabled ?? true,
96
106
  intervalMs: dur(w.interval, 'interval', WATCHDOG_DEFAULT_INTERVAL_MS, WATCHDOG_MIN_INTERVAL_MS),
97
- watch, watchExplicit: w.watch !== undefined, harness, session: sessionRaw,
98
- model: resolveRoleModel(w.model, w.harness, defaults),
107
+ watch, watchExplicit: w.watch !== undefined,
108
+ agentDefinition: structuredClone(agentDefinition), resolvedAgent: structuredClone(resolvedAgent),
109
+ harness: resolvedAgent.harness, session: resolvedAgent.session,
110
+ model: resolvedAgent.model, isolation: resolvedAgent.isolation,
99
111
  identity,
100
112
  timeoutMs: dur(w.timeout, 'timeout', WATCHDOG_DEFAULT_TIMEOUT_MS),
101
113
  keepReports,
102
114
  alertCooldownMs: dur(w.alert_cooldown, 'alert_cooldown', WATCHDOG_DEFAULT_COOLDOWN_MS),
103
- promptFile: w.prompt_file, isolation: w.isolation, sourceFile: baseFile,
115
+ promptFile: w.prompt_file, sourceFile: baseFile,
104
116
  });
105
117
  }
106
118
  return out;
@@ -1,7 +1,6 @@
1
1
  import { closeSync, existsSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
- import { spawn as spawnChild, execFile } from 'node:child_process';
2
+ import { spawn as spawnChild } from 'node:child_process';
3
3
  import { join } from 'node:path';
4
- import { promisify } from 'node:util';
5
4
  import { stringify } from 'yaml';
6
5
  import { errorReport, normalizeWatchdogReport, validateWatchdogReport } from './report.js';
7
6
  import { acquireRunLock, formatRunId, pruneReports, releaseRunLock, writeReport } from './store.js';
@@ -11,12 +10,10 @@ import { applyRole } from '../ops.js';
11
10
  import { daemonIdentityInventoryProvisioner, ensureIdentity, } from '../creation.js';
12
11
  import { runOnce, START_STAGGER_FILE } from '../runner.js';
13
12
  import { agentDir, tmpRoot } from '../paths.js';
14
- import { loadConfig, resolveMonitorConfig, resolveWorklogPolicy, ROLE_NAME_RE, } from '../config.js';
13
+ import { loadConfig, ROLE_NAME_RE, } from '../config.js';
15
14
  import { getAdapter } from '../harness/registry.js';
16
15
  import { redactLogLine } from '../application/log-service.js';
17
16
  import { controlRequest, controlSocketPath } from '../session/control.js';
18
- import { Tmux } from '../tmux.js';
19
- const execFileAsync = promisify(execFile);
20
17
  /** How often the deadline loop polls for a completed report or a dead child. */
21
18
  const POLL_MS = 1000;
22
19
  /** A `report.json` this old, with no further writes, is treated as finished. */
@@ -56,7 +53,6 @@ async function killIfNeeded(reason, child, roleName, sleep) {
56
53
  if (reason === 'stable')
57
54
  await sleep(HARVEST_GRACE_MS);
58
55
  child.kill();
59
- await killTmuxSession(roleName);
60
56
  }
61
57
  }
62
58
  function defaultLaunchChild(binPath, roleName, runDir) {
@@ -77,13 +73,6 @@ function defaultLaunchChild(binPath, roleName, runDir) {
77
73
  }),
78
74
  };
79
75
  }
80
- /** Best-effort: the tmux session outlives the supervisor child (`runOnce` created it). */
81
- async function killTmuxSession(roleName) {
82
- try {
83
- await execFileAsync('tmux', ['kill-session', '-t', roleName]);
84
- }
85
- catch { /* best effort */ }
86
- }
87
76
  /** Last 4096 chars of run.log, redacted — attached to error reports as diagnostic tail. */
88
77
  function readTail(runDir) {
89
78
  try {
@@ -95,19 +84,10 @@ function readTail(runDir) {
95
84
  }
96
85
  }
97
86
  /** The temp role every watchdog-family run launches under. Isolation is opt-in. */
98
- function buildWatchdogRole(wd, cfg) {
87
+ function buildWatchdogRole(wd, _cfg) {
99
88
  return {
100
- name: wd.identity, sourceFile: '(watchdog)',
101
- harness: wd.harness, session: wd.session,
102
- identity: wd.identity, model: wd.model,
103
- // Watchdogs are observe-only by contract, but their sanctioned status commands
104
- // must reach host control sockets. Keep approvals/unattended escalation denied
105
- // while disabling the harness's native filesystem/network sandbox.
106
- permissions: { approval: 'deny', filesystem: 'unrestricted', unattended: 'deny' },
107
- permissionsDeclared: true,
108
- monitor: resolveMonitorConfig(cfg.defaults.monitor, undefined),
109
- worklog: resolveWorklogPolicy(cfg.defaults.worklog, undefined),
110
- isolation: wd.isolation,
89
+ ...structuredClone(wd.resolvedAgent),
90
+ name: wd.identity, sourceFile: '(watchdog)', identity: wd.identity,
111
91
  };
112
92
  }
113
93
  /** Discover temporary sessions that are live now, not merely stale dirs on disk. */
@@ -121,19 +101,15 @@ async function discoverLiveTemporaryRoles() {
121
101
  catch {
122
102
  return [];
123
103
  }
124
- const tmux = new Tmux();
125
104
  const live = await Promise.all(names.map(async (name) => {
126
105
  const dir = agentDir(name, true);
127
- const [acpAlive, tmuxAlive] = await Promise.all([
128
- existsSync(controlSocketPath(dir))
129
- ? controlRequest(dir, { command: 'status' }, 2_000)
130
- .then(response => response.ok
131
- && response.result?.alive === true)
132
- .catch(() => false)
133
- : Promise.resolve(false),
134
- tmux.has(name).catch(() => false),
135
- ]);
136
- return acpAlive || tmuxAlive ? name : undefined;
106
+ const alive = existsSync(controlSocketPath(dir))
107
+ ? await controlRequest(dir, { command: 'status' }, 2_000)
108
+ .then(response => response.ok
109
+ && response.result?.alive === true)
110
+ .catch(() => false)
111
+ : false;
112
+ return alive ? name : undefined;
137
113
  }));
138
114
  return live.filter((name) => name !== undefined);
139
115
  }
@@ -16,7 +16,7 @@ export interface AuthResult {
16
16
  interface Ticket {
17
17
  value: string;
18
18
  sessionId: string;
19
- purpose: 'events' | 'terminal' | 'conversation';
19
+ purpose: 'events' | 'conversation';
20
20
  roleId?: string;
21
21
  expiresAt: number;
22
22
  }
@@ -1,10 +1,13 @@
1
1
  import type { PrereqReport } from '../harness/types.js';
2
2
  export declare const REDACTED_ENV_VALUE = "__OURS_FLEET_SECRET_REDACTED__";
3
- export type EditableFleetModel = Record<string, unknown>;
3
+ export interface EditableFleetModel {
4
+ manifest: Record<string, unknown>;
5
+ agents: Record<string, Record<string, unknown>>;
6
+ }
4
7
  export interface ConfigReadResult {
5
8
  path: string;
6
- exists: boolean;
7
- firstRun: boolean;
9
+ exists: true;
10
+ firstRun: false;
8
11
  revision: string;
9
12
  model: EditableFleetModel;
10
13
  redactions: string[];
@@ -33,15 +36,15 @@ export interface ConfigWriteResult extends ConfigPreviewResult {
33
36
  export interface FleetConfigServiceOptions {
34
37
  configPath?: string;
35
38
  preflight?(configPath: string): Promise<PrereqReport>;
39
+ /** Fault-injection seam; called under lock immediately before each mutation. */
40
+ beforeMutation?(relativePath: string, index: number): void;
36
41
  }
37
42
  export declare class FleetConfigService {
38
43
  readonly path: string;
39
44
  private readonly preflight;
45
+ private readonly beforeMutation?;
40
46
  constructor(options?: FleetConfigServiceOptions);
41
47
  read(): ConfigReadResult;
42
- preview(baseRevision: string, model: unknown): Promise<ConfigPreviewResult>;
43
- write(baseRevision: string, model: unknown): Promise<ConfigWriteResult>;
44
- private readSource;
45
- private assertRevision;
46
- private validateCandidate;
48
+ preview(baseRevision: string, input: unknown): Promise<ConfigPreviewResult>;
49
+ write(baseRevision: string, input: unknown): Promise<ConfigWriteResult>;
47
50
  }