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

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 (100) hide show
  1. package/README.md +90 -112
  2. package/dist/application/capabilities.d.ts +0 -1
  3. package/dist/application/capabilities.js +3 -13
  4. package/dist/application/fleet-query-service.d.ts +0 -3
  5. package/dist/application/fleet-query-service.js +1 -24
  6. package/dist/application/role-creation-service.d.ts +5 -5
  7. package/dist/application/role-creation-service.js +14 -9
  8. package/dist/application/role-removal-service.js +6 -3
  9. package/dist/application/role-repository.d.ts +1 -2
  10. package/dist/application/role-repository.js +7 -7
  11. package/dist/application/session-control.d.ts +0 -12
  12. package/dist/application/session-control.js +0 -42
  13. package/dist/application/session-mutations.d.ts +5 -5
  14. package/dist/application/types.d.ts +1 -8
  15. package/dist/briefing.js +3 -6
  16. package/dist/build-info.json +5 -5
  17. package/dist/cli.js +17 -28
  18. package/dist/config.d.ts +29 -6
  19. package/dist/config.js +320 -40
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +2 -2
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +51 -95
  24. package/dist/doctor.js +1 -8
  25. package/dist/fleet-proxy.d.ts +1 -1
  26. package/dist/generated-agent-source.d.ts +9 -0
  27. package/dist/generated-agent-source.js +53 -0
  28. package/dist/harness/acp-session-transport.d.ts +8 -0
  29. package/dist/harness/acp-session-transport.js +3 -0
  30. package/dist/harness/agent-session.d.ts +46 -0
  31. package/dist/harness/agent-session.js +1 -0
  32. package/dist/harness/claude-code-session.d.ts +22 -0
  33. package/dist/harness/claude-code-session.js +32 -0
  34. package/dist/harness/claude-code.d.ts +2 -1
  35. package/dist/harness/claude-code.js +39 -81
  36. package/dist/harness/codex-session.d.ts +22 -0
  37. package/dist/harness/codex-session.js +32 -0
  38. package/dist/harness/codex.d.ts +2 -1
  39. package/dist/harness/codex.js +49 -45
  40. package/dist/harness/registry.js +4 -0
  41. package/dist/harness/types.d.ts +6 -38
  42. package/dist/index.d.ts +3 -5
  43. package/dist/index.js +1 -3
  44. package/dist/init-guidance.d.ts +1 -0
  45. package/dist/init-guidance.js +5 -0
  46. package/dist/isolation/resources.d.ts +3 -7
  47. package/dist/isolation/resources.js +3 -7
  48. package/dist/model-env.d.ts +2 -4
  49. package/dist/model-env.js +4 -7
  50. package/dist/monitor.d.ts +1 -24
  51. package/dist/monitor.js +5 -140
  52. package/dist/ops.js +4 -2
  53. package/dist/owner-channel/channel.d.ts +2 -2
  54. package/dist/resolved-plan.d.ts +3 -2
  55. package/dist/resolved-plan.js +21 -5
  56. package/dist/runner.d.ts +5 -15
  57. package/dist/runner.js +26 -102
  58. package/dist/sensitive-config.d.ts +2 -0
  59. package/dist/sensitive-config.js +4 -0
  60. package/dist/session/acp.d.ts +2 -2
  61. package/dist/session/acp.js +2 -3
  62. package/dist/session/activity.d.ts +2 -3
  63. package/dist/session/activity.js +2 -3
  64. package/dist/session/arbiter.d.ts +4 -4
  65. package/dist/session/control.d.ts +3 -3
  66. package/dist/session/types.d.ts +11 -5
  67. package/dist/spawn.d.ts +4 -4
  68. package/dist/spawn.js +60 -32
  69. package/dist/supervisor/none.d.ts +1 -1
  70. package/dist/supervisor/none.js +2 -2
  71. package/dist/tmux.d.ts +1 -14
  72. package/dist/tmux.js +1 -51
  73. package/dist/watchdog/config.js +6 -4
  74. package/dist/watchdog/run.js +8 -23
  75. package/dist/web/auth.d.ts +1 -1
  76. package/dist/web/fleet-config-service.d.ts +11 -8
  77. package/dist/web/fleet-config-service.js +383 -183
  78. package/dist/web/runtime.js +4 -15
  79. package/dist/web/server.d.ts +1 -3
  80. package/dist/web/server.js +3 -14
  81. package/dist/web/topology-promote.js +18 -18
  82. package/dist/web/topology.js +2 -2
  83. package/dist/web/yaml-document-edit.js +2 -0
  84. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  85. package/dist/web-app/assets/index-BbE9EX6n.js +10 -0
  86. package/dist/web-app/index.html +2 -2
  87. package/examples/fleet/agents/Alice.yaml +11 -0
  88. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  89. package/examples/fleet/brains/claude-default.yaml +5 -0
  90. package/examples/fleet/roles/coordinator.yaml +5 -0
  91. package/examples/fleet/roles/developer.yaml +3 -0
  92. package/examples/fleet.yaml +74 -0
  93. package/package.json +4 -8
  94. package/dist/session/tmux.d.ts +0 -28
  95. package/dist/session/tmux.js +0 -80
  96. package/dist/web/terminal/bridge.d.ts +0 -27
  97. package/dist/web/terminal/bridge.js +0 -317
  98. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  99. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  100. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
@@ -0,0 +1 @@
1
+ export declare const INIT_COMPLETION_GUIDANCE = "\nNext:\n cp \"$(npm root -g)/@ours.network/fleet/examples/fleet.yaml\" ~/fleet.yaml\n cp -R \"$(npm root -g)/@ours.network/fleet/examples/fleet\" ~/fleet\n edit ~/fleet.yaml and ~/fleet/{agents,roles,brains}/*.yaml, then: ours-fleet up";
@@ -0,0 +1,5 @@
1
+ export const INIT_COMPLETION_GUIDANCE = `
2
+ Next:
3
+ cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
4
+ cp -R "$(npm root -g)/@ours.network/fleet/examples/fleet" ~/fleet
5
+ edit ~/fleet.yaml and ~/fleet/{agents,roles,brains}/*.yaml, then: ours-fleet up`;
@@ -4,13 +4,9 @@ export interface ResourceArgs {
4
4
  warnings: string[];
5
5
  }
6
6
  /**
7
- * Build the `systemd-run --user --scope -p … --` prefix that caps the pane's
8
- * cgroup-v2 scope. Composed OUTSIDE the sandbox wrap: a tmux pane is
9
- * a child of a tmux SERVER rather than of the role's own runner process, so the
10
- * only reliable per-agent limit is a transient scope at the pane itself. (Since
11
- * #32 that server is per role rather than fleet-wide, which is what keeps one
12
- * role's `stop` off every other role's pane — the limit still belongs on the
13
- * pane.)
7
+ * Build the `systemd-run --user --scope -p … --` prefix that caps the agent
8
+ * process's cgroup-v2 scope. It composes outside the sandbox wrapper so both
9
+ * the adapter and its sandboxed child remain inside the transient scope.
14
10
  *
15
11
  * mem/pids are always enforced (their controllers are delegated to `--user` by
16
12
  * default). cpu degrades to a warning when the cpu controller is not delegated.
@@ -1,12 +1,8 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  /**
3
- * Build the `systemd-run --user --scope -p … --` prefix that caps the pane's
4
- * cgroup-v2 scope. Composed OUTSIDE the sandbox wrap: a tmux pane is
5
- * a child of a tmux SERVER rather than of the role's own runner process, so the
6
- * only reliable per-agent limit is a transient scope at the pane itself. (Since
7
- * #32 that server is per role rather than fleet-wide, which is what keeps one
8
- * role's `stop` off every other role's pane — the limit still belongs on the
9
- * pane.)
3
+ * Build the `systemd-run --user --scope -p … --` prefix that caps the agent
4
+ * process's cgroup-v2 scope. It composes outside the sandbox wrapper so both
5
+ * the adapter and its sandboxed child remain inside the transient scope.
10
6
  *
11
7
  * mem/pids are always enforced (their controllers are delegated to `--user` by
12
8
  * default). cpu degrades to a warning when the cpu controller is not delegated.
@@ -3,15 +3,13 @@ import type { ResolvedRole } from './config.js';
3
3
  * Which environment variable a harness reads to pin the model it RUNS.
4
4
  *
5
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
6
+ * reaches the Claude Code ACP adapter. The adapter resolves its model in this
7
+ * order — ANTHROPIC_MODEL, then
9
8
  * `settings.model`, then a resumed session's live model, then its first
10
9
  * catalogue entry. A role's declared model was therefore invisible to every
11
10
  * ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
12
11
  * an explicitly requested one.
13
12
  */
14
- export declare const MODEL_ENV_BY_HARNESS: Readonly<Record<string, string>>;
15
13
  /** The model-pin variable for a harness, or undefined if it pins no model by env. */
16
14
  export declare function modelEnvVar(harness: string | undefined): string | undefined;
17
15
  export interface RoleModelEnvInput {
package/dist/model-env.js CHANGED
@@ -1,21 +1,18 @@
1
+ import { getAdapter } from './harness/registry.js';
1
2
  /**
2
3
  * Which environment variable a harness reads to pin the model it RUNS.
3
4
  *
4
5
  * 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
6
+ * reaches the Claude Code ACP adapter. The adapter resolves its model in this
7
+ * order — ANTHROPIC_MODEL, then
8
8
  * `settings.model`, then a resumed session's live model, then its first
9
9
  * catalogue entry. A role's declared model was therefore invisible to every
10
10
  * ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
11
11
  * an explicitly requested one.
12
12
  */
13
- export const MODEL_ENV_BY_HARNESS = {
14
- 'claude-code': 'ANTHROPIC_MODEL',
15
- };
16
13
  /** The model-pin variable for a harness, or undefined if it pins no model by env. */
17
14
  export function modelEnvVar(harness) {
18
- return harness === undefined ? undefined : MODEL_ENV_BY_HARNESS[harness];
15
+ return harness === undefined ? undefined : getAdapter(harness).agentSession.modelEnvironmentVariable();
19
16
  }
20
17
  /**
21
18
  * Resolve a role's environment and its runtime model TOGETHER, so the two can
package/dist/monitor.d.ts CHANGED
@@ -26,15 +26,8 @@ export type FetchLike = (url: string, init?: {
26
26
  headers?: Record<string, string>;
27
27
  signal?: AbortSignal;
28
28
  }) => Promise<FetchResponse>;
29
- export interface MonitorTmux {
30
- has(name: string): Promise<boolean>;
31
- capture(name: string, lines?: number): Promise<string>;
32
- sendText(name: string, text: string): Promise<void>;
33
- sendKey(name: string, key: string): Promise<void>;
34
- }
35
29
  export interface MonitorDeps {
36
30
  fetch: FetchLike;
37
- tmux: MonitorTmux;
38
31
  isAlive(pid: number): boolean;
39
32
  sleep(ms: number): Promise<void>;
40
33
  now(): number;
@@ -45,7 +38,7 @@ export interface MonitorDeps {
45
38
  clear(t: ReturnType<typeof setTimeout>): void;
46
39
  };
47
40
  /**
48
- * Structured prompt delivery used by ACP sessions. Tmux remains the fallback.
41
+ * Structured prompt delivery used by agent sessions.
49
42
  * `succeeded` is the turn's TERMINAL result, not merely that the session took
50
43
  * the prompt: a refused or cancelled wake was seen and not acted on, and must
51
44
  * not commit the cursor.
@@ -208,14 +201,6 @@ export declare class Monitor {
208
201
  /** Gather stragglers arriving within batch_ms so a burst lands as one line. */
209
202
  private coalesce;
210
203
  private deliver;
211
- /**
212
- * Watch the pane until the just-triggered turn settles, then fold its outcome
213
- * into the API-error streak and republish `.monitor-status`. A completed turn
214
- * (or an inconclusive give-up) resets/keeps the streak; an `API Error:` tail
215
- * grows it. Once the streak reaches the threshold the status degrades; a later
216
- * completed turn flips it back to armed. Detection only — no remediation (#19).
217
- */
218
- private observeTurnOutcome;
219
204
  /** Update the consecutive-API-error streak and derive `.monitor-status` from it. */
220
205
  private recordTurn;
221
206
  /**
@@ -226,14 +211,6 @@ export declare class Monitor {
226
211
  * the composer is cleared by hand. Best-effort: a dead pane just makes the keys
227
212
  * no-ops (delivery is still verified downstream).
228
213
  */
229
- private clearComposer;
230
- /**
231
- * Block until the console can accept input; classify offline/stopped/ready, or
232
- * `modal` when the pane still looks modal after `MODAL_GIVE_UP_MS`. The bound is
233
- * what keeps a modal from wedging delivery silently: we still never `Enter` into
234
- * the dialog, but the give-up is reported instead of retried forever.
235
- */
236
- private awaitInjectable;
237
214
  private doFetch;
238
215
  private advance;
239
216
  private persistCursor;
package/dist/monitor.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
- import { classifyFailureText } from './model-recovery.js';
5
4
  // Code constants rather than user configuration.
6
5
  const DEFAULT_PORT = 3050;
7
6
  // The daemon normally holds for 25s, but that value is operator-configurable
@@ -522,7 +521,11 @@ export class Monitor {
522
521
  }
523
522
  async deliver(pid, batch) {
524
523
  const line = formatNotificationLine(batch);
525
- if (this.deps.delivery) {
524
+ if (!this.deps.delivery) {
525
+ this.degrade('delivery', 'structured agent-session delivery is unavailable');
526
+ return false;
527
+ }
528
+ {
526
529
  const result = await this.deps.delivery.submit(line, { interrupt: this.cfg.interrupt });
527
530
  if (!result.succeeded) {
528
531
  // Name the reason: "refused" and "cancelled" are the agent's answer,
@@ -540,101 +543,6 @@ export class Monitor {
540
543
  this.recordTurn('completed');
541
544
  return true;
542
545
  }
543
- // Tmux exposes no authenticated tool lifecycle. `after_tool` therefore
544
- // degrades to the existing non-cancelling injection path; never guess a
545
- // boundary from pane text and never send C-c for this mode.
546
- if (this.cfg.interrupt === true)
547
- await this.deps.tmux.sendKey(this.name, 'C-c');
548
- if (this.cfg.interrupt === 'after_tool')
549
- this.degrade('safe-boundary', 'after_tool unsupported by tmux; using non-cancelling delivery');
550
- const state = await this.awaitInjectable(pid);
551
- if (state !== 'ready') {
552
- if (state === 'offline')
553
- this.degrade('offline', 'offline during delivery');
554
- else if (state === 'modal')
555
- this.degrade('modal', `modal wedge — pane held a dialog for ` +
556
- `${MODAL_GIVE_UP_MS / 1000}s, wake not injected`);
557
- return false;
558
- }
559
- await this.clearComposer(); // start from an empty composer
560
- await this.deps.tmux.sendText(this.name, line); // send-keys -l + Enter
561
- let delivered = false;
562
- // Verify submission for THIS line even if stop() arrives mid-flight: the text
563
- // is already in the composer and we want it submitted (at-least-once). A truly
564
- // dead pane makes safeCapture return '' ⇒ not-in-composer ⇒ breaks, no wasted Enter.
565
- for (let i = 0; i < MAX_ENTER_RETRIES;) {
566
- await this.deps.sleep(POST_VERIFY_MS);
567
- const capture = await safeCapture(this.deps.tmux, this.name);
568
- if (!capture.ok) {
569
- this.degrade('delivery', 'capture failed during injection verification');
570
- return false;
571
- }
572
- // A dialog can appear after the initial send. Never let a verification
573
- // retry confirm it. Wait under the same bounded modal policy as initial
574
- // injection, then re-capture immediately before considering Enter.
575
- if (looksModal(capture.pane)) {
576
- const state = await this.awaitInjectable(pid);
577
- if (state === 'ready')
578
- continue;
579
- if (state === 'offline')
580
- this.degrade('offline', 'offline during injection verification');
581
- else if (state === 'modal')
582
- this.degrade('modal', `modal wedge during injection verification — ` +
583
- `no Enter sent for ${MODAL_GIVE_UP_MS / 1000}s`);
584
- return false;
585
- }
586
- if (!stillInComposer(capture.pane, line)) {
587
- delivered = true;
588
- break;
589
- }
590
- await this.deps.tmux.sendKey(this.name, 'Enter');
591
- i++;
592
- }
593
- if (!delivered) {
594
- this.degrade('delivery', 'injection unverified');
595
- return false;
596
- }
597
- this.recover('delivery', 'modal');
598
- // The wake landed and a turn started; observe how that turn terminates so a
599
- // refusal-wedge (every turn dies with `API Error:` while delivery stays green)
600
- // becomes visible in `.monitor-status` instead of masquerading as armed (#19).
601
- const recoveryTriggered = await this.observeTurnOutcome(pid);
602
- return !recoveryTriggered;
603
- }
604
- /**
605
- * Watch the pane until the just-triggered turn settles, then fold its outcome
606
- * into the API-error streak and republish `.monitor-status`. A completed turn
607
- * (or an inconclusive give-up) resets/keeps the streak; an `API Error:` tail
608
- * grows it. Once the streak reaches the threshold the status degrades; a later
609
- * completed turn flips it back to armed. Detection only — no remediation (#19).
610
- */
611
- async observeTurnOutcome(pid) {
612
- for (let i = 0; i < TURN_OBSERVE_POLLS; i++) {
613
- if (this.stopped)
614
- return false; // shutting down — leave status
615
- if (!this.deps.isAlive(pid) || !(await this.deps.tmux.has(this.name)))
616
- return false; // loop marks offline
617
- const capture = await safeCapture(this.deps.tmux, this.name);
618
- if (!capture.ok) {
619
- this.degrade('delivery', 'capture failed during turn observation');
620
- return false;
621
- }
622
- if (looksApiError(capture.pane)) {
623
- const evidence = classifyFailureText(capture.pane);
624
- const recoveryTriggered = evidence
625
- ? this.deps.onFailureEvidence?.(evidence) === true
626
- : false;
627
- this.recordTurn('api-error');
628
- return recoveryTriggered;
629
- }
630
- if (!looksRunning(capture.pane)) {
631
- this.recordTurn('completed');
632
- return false;
633
- }
634
- await this.deps.sleep(TURN_OBSERVE_INTERVAL_MS);
635
- }
636
- this.recordTurn('inconclusive'); // still running at give-up: hold the streak, don't re-arm
637
- return false;
638
546
  }
639
547
  /** Update the consecutive-API-error streak and derive `.monitor-status` from it. */
640
548
  recordTurn(outcome) {
@@ -658,41 +566,6 @@ export class Monitor {
658
566
  * the composer is cleared by hand. Best-effort: a dead pane just makes the keys
659
567
  * no-ops (delivery is still verified downstream).
660
568
  */
661
- async clearComposer() {
662
- for (const key of COMPOSER_CLEAR_KEYS)
663
- await this.deps.tmux.sendKey(this.name, key);
664
- }
665
- /**
666
- * Block until the console can accept input; classify offline/stopped/ready, or
667
- * `modal` when the pane still looks modal after `MODAL_GIVE_UP_MS`. The bound is
668
- * what keeps a modal from wedging delivery silently: we still never `Enter` into
669
- * the dialog, but the give-up is reported instead of retried forever.
670
- */
671
- async awaitInjectable(pid) {
672
- let modalWaits = 0;
673
- for (;;) {
674
- if (this.stopped)
675
- return 'stopped';
676
- if (!this.deps.isAlive(pid) || !(await this.deps.tmux.has(this.name)))
677
- return 'offline';
678
- const now = this.deps.now();
679
- if (now < this.bootDeadline) {
680
- await this.deps.sleep(this.bootDeadline - now);
681
- continue;
682
- }
683
- const capture = await safeCapture(this.deps.tmux, this.name);
684
- if (!capture.ok) {
685
- this.degrade('delivery', 'capture failed while checking session readiness');
686
- await this.deps.sleep(MODAL_RETRY_MS);
687
- continue;
688
- }
689
- if (!looksModal(capture.pane))
690
- return 'ready';
691
- if (modalWaits++ >= MAX_MODAL_WAITS)
692
- return 'modal';
693
- await this.deps.sleep(MODAL_RETRY_MS);
694
- }
695
- }
696
569
  async doFetch(since, timeoutMs, timeoutKind) {
697
570
  const ctrl = new AbortController();
698
571
  this.currentAbort = ctrl;
@@ -820,12 +693,4 @@ export class Monitor {
820
693
  export function createMonitor(o) {
821
694
  return new Monitor(o);
822
695
  }
823
- async function safeCapture(tmux, name) {
824
- try {
825
- return { ok: true, pane: await tmux.capture(name) };
826
- }
827
- catch {
828
- return { ok: false, pane: '' };
829
- }
830
- }
831
696
  const msg = (e) => e?.message ?? String(e);
package/dist/ops.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, unlinkSync, } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { randomUUID, createHash } from 'node:crypto';
4
- import { agentDir, fleetDDir, watchdogsRoot } from './paths.js';
4
+ import { agentDir, watchdogsRoot } from './paths.js';
5
5
  import { findRole } from './config.js';
6
6
  import { getAdapter } from './harness/registry.js';
7
7
  import { generateBriefing } from './briefing.js';
@@ -9,6 +9,7 @@ import { resetRestartLedger } from './runner.js';
9
9
  import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, } from './temp-lifecycle.js';
10
10
  import { realExec } from './exec.js';
11
11
  import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './creation.js';
12
+ import { provesGeneratedAgentSource } from './generated-agent-source.js';
12
13
  // Launch staggering now lives at the harness-launch point (the runner's start
13
14
  // gate, driven by `start_stagger_ms`), so it covers systemd host-boot too — not
14
15
  // just the `up`/`restart` command loop below. The old in-loop FLEET_START_STAGGER
@@ -264,9 +265,10 @@ export async function rmRole(cfg, name, deps) {
264
265
  return;
265
266
  }
266
267
  const role = findRole(cfg, name);
268
+ const generatedSource = provesGeneratedAgentSource(agentDir(name), cfg.files[0], role.sourceFile);
267
269
  await deps.backend.uninstall(name);
268
270
  rmSync(agentDir(name), { recursive: true, force: true });
269
- if (role.sourceFile.startsWith(fleetDDir() + '/')) {
271
+ if (generatedSource && existsSync(role.sourceFile)) {
270
272
  unlinkSync(role.sourceFile);
271
273
  deps.log(`removed ${role.sourceFile}`);
272
274
  }
@@ -1,5 +1,5 @@
1
1
  import { type OwnerChannelConfig } from '../config.js';
2
- import { type SessionHandle } from '../session/types.js';
2
+ import { type AgentSession } from '../session/types.js';
3
3
  import { type OwnerFleetOps } from './commands.js';
4
4
  import type { ManagedFleetSpawnResult } from '../fleet-proxy.js';
5
5
  import { type OursOps } from './ours-client.js';
@@ -12,7 +12,7 @@ export interface OwnerChannelOptions {
12
12
  /** Harness id of the role (e.g. 'claude-code', 'codex'); gates which slash commands may be forwarded. */
13
13
  harness: string;
14
14
  config: OwnerChannelConfig;
15
- session: SessionHandle;
15
+ session: AgentSession;
16
16
  stateDir: string;
17
17
  env?: Record<string, string>;
18
18
  log(line: string): void;
@@ -1,5 +1,6 @@
1
1
  import type { FleetConfig, ResolvedRole } from './config.js';
2
- export declare const RESOLVED_PLAN_SCHEMA_VERSION = 1;
2
+ export declare const RESOLVED_PLAN_SCHEMA_VERSION = 2;
3
+ export declare const redactSensitive: (value: unknown, key?: string) => unknown;
3
4
  /** Stable, secret-safe machine contract consumed by config JSON and spawn dry-run. */
4
5
  export declare function resolvedPlan(cfg: FleetConfig): Record<string, unknown>;
5
- export declare function resolvedRolePlan(role: ResolvedRole): Record<string, unknown>;
6
+ export declare function resolvedRolePlan(role: ResolvedRole, manifestDir?: string): Record<string, unknown>;
@@ -1,8 +1,10 @@
1
1
  import { analyzeRolePermissions } from './permissions.js';
2
- export const RESOLVED_PLAN_SCHEMA_VERSION = 1;
2
+ import { dirname, relative } from 'node:path';
3
+ import { isSensitiveConfigKey } from './sensitive-config.js';
4
+ export const RESOLVED_PLAN_SCHEMA_VERSION = 2;
3
5
  const sortedObject = (value) => Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
4
- const redactSensitive = (value, key = '') => {
5
- if (/(?:secret|token|password|authorization|api[_-]?key)/i.test(key))
6
+ export const redactSensitive = (value, key = '') => {
7
+ if (isSensitiveConfigKey(key))
6
8
  return '<redacted>';
7
9
  if (Array.isArray(value))
8
10
  return value.map(item => redactSensitive(item));
@@ -14,12 +16,17 @@ const redactSensitive = (value, key = '') => {
14
16
  };
15
17
  /** Stable, secret-safe machine contract consumed by config JSON and spawn dry-run. */
16
18
  export function resolvedPlan(cfg) {
19
+ const manifestDir = dirname(cfg.files[0] ?? '.');
17
20
  return {
18
21
  schemaVersion: RESOLVED_PLAN_SCHEMA_VERSION,
22
+ configMode: cfg.configMode ?? 'split-v2',
19
23
  sourceFiles: [...cfg.files],
24
+ sourceDocuments: (cfg.sourceDocuments ?? []).map(document => ({
25
+ ...document, path: relative(manifestDir, document.path) || document.path,
26
+ })),
20
27
  startStaggerMs: cfg.startStaggerMs,
21
28
  diagnostics: cfg.diagnostics.map(diagnostic => ({ ...diagnostic })),
22
- roles: cfg.roles.map(resolvedRolePlan),
29
+ roles: cfg.roles.map(role => resolvedRolePlan(role, manifestDir)),
23
30
  loops: cfg.loops.map(loop => sortedObject({
24
31
  name: loop.name, selectors: [...loop.selectors], roles: [...loop.roleNames],
25
32
  intervalMs: loop.intervalMs, initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
@@ -36,16 +43,25 @@ export function resolvedPlan(cfg) {
36
43
  })),
37
44
  };
38
45
  }
39
- export function resolvedRolePlan(role) {
46
+ export function resolvedRolePlan(role, manifestDir = dirname(role.sourceFile)) {
40
47
  const analysis = analyzeRolePermissions(role);
41
48
  return {
42
49
  name: role.name,
43
50
  sourceFile: role.sourceFile,
51
+ references: Object.values(role.provenance ?? {}).flatMap(item => item.viaReference ? [item.viaReference] : [])
52
+ .filter((item, index, all) => all.findIndex(other => JSON.stringify(other) === JSON.stringify(item)) === index)
53
+ .sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)
54
+ || a.sourcePointer.localeCompare(b.sourcePointer)),
55
+ provenance: sortedObject(Object.fromEntries(Object.entries(role.provenance ?? {}).map(([field, item]) => [
56
+ field, { ...item, sourceFile: item.sourceKind === 'built-in'
57
+ ? item.sourceFile : relative(manifestDir, item.sourceFile) || item.sourceFile },
58
+ ]))),
44
59
  identity: role.identity,
45
60
  harness: role.harness,
46
61
  session: role.session,
47
62
  sessionOptions: role.session_options ?? null,
48
63
  model: role.model ?? null,
64
+ effort: role.effort ?? null,
49
65
  modelChain: role.model_chain ?? null,
50
66
  cwd: role.cwd ?? null,
51
67
  coordinator: role.coordinator ?? null,
package/dist/runner.d.ts CHANGED
@@ -1,15 +1,12 @@
1
1
  import { type ResolvedRole } from './config.js';
2
- import type { Launch } from './harness/types.js';
3
- import { Tmux } from './tmux.js';
4
2
  import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
5
3
  import { type Exec } from './exec.js';
6
- import { type AcpSessionOptions } from './session/acp.js';
7
4
  import { RoleControlServer } from './session/control.js';
8
- import type { ExitRecord, SessionHandle, TurnResult } from './session/types.js';
5
+ import type { AgentSession, ExitRecord, TurnResult } from './session/types.js';
6
+ import type { AgentSessionAdapter, AgentSessionStartOptions } from './harness/agent-session.js';
9
7
  import { type OwnerChannelHandle, type OwnerChannelOptions } from './owner-channel/channel.js';
10
8
  import { type OwnerBinderLease } from './owner-channel/binder.js';
11
9
  export interface RunnerDeps {
12
- tmux: Tmux;
13
10
  exec: Exec;
14
11
  cpuDelegated(): boolean;
15
12
  isAlive(pid: number): boolean;
@@ -23,9 +20,10 @@ export interface RunnerDeps {
23
20
  /** Construct trusted owner ingress (injectable for lifecycle tests). */
24
21
  createOwnerChannel(opts: OwnerChannelOptions): OwnerChannelHandle;
25
22
  /** Start the ACP transport (injectable for deterministic runner lifecycle tests). */
26
- startAcpSession(opts: AcpSessionOptions): Promise<SessionHandle>;
23
+ /** Neutral construction seam for deterministic runner lifecycle tests. */
24
+ startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
27
25
  /** Construct the authenticated role control route (injectable where sockets are unavailable). */
28
- createControlServer(stateDir: string, session: SessionHandle, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
26
+ createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
29
27
  /** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
30
28
  acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise<OwnerBinderLease>;
31
29
  /** Ask the still-authenticated predecessor to emit the fixed recovery notice. */
@@ -51,14 +49,6 @@ export declare function harnessChildEnv(role: ResolvedRole, launchEnv: Record<st
51
49
  * responsible for.
52
50
  */
53
51
  export declare function recordMonitorOwner(dir: string, owner: 'fleet' | 'native'): boolean;
54
- /**
55
- * Compose the tmux pane shell command: env prefix + argv + exit-status capture.
56
- * `paneArgv` defaults to `launch.argv`; when isolation is active the caller passes
57
- * the sandbox-wrapped argv (e.g. `bwrap … -- claude …`). The `env` prefix and the
58
- * `echo $? > exitfile` capture stay host-side, outside the sandbox, so the runner
59
- * still sees the real exit code.
60
- */
61
- export declare function buildPaneCommand(launch: Launch, roleEnv: Record<string, string> | undefined, exitStatusPath: string, paneArgv?: string[]): string;
62
52
  /**
63
53
  * Read the pane's `.exit-status`. Three shapes are accepted: the structured
64
54
  * record written above, a bare number left by a pre-upgrade pane (so an