@ours.network/fleet 1.1.0-nightly.6 → 1.1.0-nightly.8

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 (50) hide show
  1. package/README.md +23 -4
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/fleet-query-service.js +13 -1
  5. package/dist/application/task-room-service.d.ts +1 -1
  6. package/dist/build-info.json +4 -4
  7. package/dist/cli.js +6 -2
  8. package/dist/config-yaml.d.ts +1 -1
  9. package/dist/config.d.ts +1 -1
  10. package/dist/config.js +49 -2
  11. package/dist/daemon-recovery.d.ts +93 -0
  12. package/dist/daemon-recovery.js +328 -0
  13. package/dist/docs.d.ts +1 -1
  14. package/dist/docs.js +26 -3
  15. package/dist/doctor.js +14 -0
  16. package/dist/init-guidance.d.ts +1 -1
  17. package/dist/init-guidance.js +4 -3
  18. package/dist/owner-channel/channel.d.ts +40 -0
  19. package/dist/owner-channel/channel.js +210 -16
  20. package/dist/owner-channel/commands.js +3 -9
  21. package/dist/preset-bootstrap.d.ts +10 -0
  22. package/dist/preset-bootstrap.js +85 -0
  23. package/dist/rooms-tasks/cli.js +4 -4
  24. package/dist/rooms-tasks/config.js +0 -4
  25. package/dist/rooms-tasks/templates.d.ts +0 -1
  26. package/dist/rooms-tasks/templates.js +9 -76
  27. package/dist/rooms-tasks/types.d.ts +2 -1
  28. package/dist/runner.d.ts +8 -1
  29. package/dist/runner.js +74 -3
  30. package/dist/temp-lifecycle.d.ts +1 -1
  31. package/dist/web/fleet-config-service.js +4 -3
  32. package/package.json +2 -1
  33. package/presets/fleet/agents/Agent.yaml +3 -0
  34. package/presets/fleet/agents/Architect.yaml +3 -0
  35. package/presets/fleet/agents/Critic.yaml +3 -0
  36. package/presets/fleet/agents/Developer.yaml +3 -0
  37. package/presets/fleet/agents/Secretary.yaml +3 -0
  38. package/presets/fleet/agents/Tester.yaml +3 -0
  39. package/presets/fleet/brains/claude-default.yaml +5 -0
  40. package/presets/fleet/roles/Agent.yaml +6 -0
  41. package/presets/fleet/roles/Architect.yaml +6 -0
  42. package/presets/fleet/roles/Critic.yaml +6 -0
  43. package/presets/fleet/roles/Developer.yaml +6 -0
  44. package/presets/fleet/roles/Secretary.yaml +6 -0
  45. package/presets/fleet/roles/Tester.yaml +6 -0
  46. package/presets/fleet/room_templates/pair.yaml +11 -0
  47. package/presets/fleet/room_templates/single.yaml +8 -0
  48. package/presets/fleet/room_templates/team.yaml +12 -0
  49. package/presets/fleet.yaml +7 -0
  50. package/presets/manifest.json +5 -0
package/README.md CHANGED
@@ -43,7 +43,7 @@ brain: { inline: { harness: codex, session: acp } }
43
43
  ## How it works
44
44
 
45
45
  ```
46
- ~/fleet.yaml + ~/fleet/{agents,roles,brains}/*.yaml your declaration
46
+ ~/fleet.yaml + ~/fleet/{agents,roles,brains,room_templates}/*.yaml your declaration
47
47
  │ ours-fleet up
48
48
  ▼
49
49
  briefing.md per role ──► agent session adapter ──► ACP client/session ──► ACP agent
@@ -91,7 +91,7 @@ equivalent); logs land in `~/.ours-fleet/logs/`.
91
91
 
92
92
  ```sh
93
93
  npm i -g @ours.network/fleet
94
- ours-fleet init # units/dirs/linger for this user
94
+ ours-fleet init # units/dirs/linger + missing standard presets
95
95
  ours-fleet doctor # verifies everything above, with actionable messages
96
96
  ```
97
97
 
@@ -107,8 +107,8 @@ become that account and repeat.
107
107
  ## Quickstart
108
108
 
109
109
  ```sh
110
- cp -R "$(npm root -g)/@ours.network/fleet/examples/fleet" ~/fleet
111
- cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
110
+ ours-fleet init # safe to repeat: creates missing files, never replaces edits
111
+ ours-fleet config # validates Agents, Roles, Brains, and Room templates
112
112
  $EDITOR ~/fleet/agents/*.yaml # compose Role + Brain and operational settings
113
113
  ours-fleet up # boot the fleet (staggered)
114
114
  ours-fleet ls # running consoles
@@ -442,6 +442,25 @@ installer/setup flows remain responsible for starting it.
442
442
 
443
443
  ### Rooms and tasks
444
444
 
445
+ Init installs editable `single`, `pair`, and `team` definitions under
446
+ `~/fleet/room_templates/`, plus every exact-cased Agent, Role, and Brain they
447
+ reference. Inspect them with `ours-fleet template list` and
448
+ `ours-fleet template show team`. After configuring the authenticated owner below:
449
+
450
+ ```sh
451
+ ours-fleet task create --title "Solo task" --template single
452
+ ours-fleet task create --title "Reviewed change" --template pair
453
+ ours-fleet task create --title "Phased delivery" --template team
454
+ ```
455
+
456
+ `ours-fleet init -c /path/custom.yaml` seeds `/path/custom/` instead. Init reports
457
+ the packaged preset revision and source directory and only seeds missing files.
458
+ It never upgrades an edited preset. To adopt a newer packaged file explicitly,
459
+ copy the reported source file beside the existing target as `.new-default`, review
460
+ `diff -u`, then replace the target yourself. This is the sole adoption operation;
461
+ rerunning init is not an update. Users upgrading from hardcoded templates should
462
+ run init once (with the same `-c` selection they normally use).
463
+
445
464
  Rooms always use `ours-cowork`; there is no room-provider selector. Configure
446
465
  the cowork daemon connection and the room owner directly:
447
466
 
@@ -0,0 +1,17 @@
1
+ import type { ConversationEventV1 } from './session/conversation-types.js';
2
+ import type { AgentSession } from './session/types.js';
3
+ export interface AgentRecoveryEvidence {
4
+ ok: boolean;
5
+ reason: 'RECOVERY_TOOLS_VERIFIED' | 'RECOVERY_PROMPT_MISSING' | 'RECOVERY_TURN_INCOMPLETE' | 'RECOVERY_TURN_FAILED' | 'RECOVERY_CHOOSE_MISSING' | 'RECOVERY_CURRENT_MISSING' | 'RECOVERY_GET_MESSAGES_MISSING' | 'RECOVERY_TOOL_ORDER_INVALID';
6
+ chooseIdentity: boolean;
7
+ currentIdentity: boolean;
8
+ getMessages: boolean;
9
+ turnCompleted: boolean;
10
+ }
11
+ /**
12
+ * Verify one exact recovery turn from its durable conversation ledger. Tool
13
+ * arguments/results are inspected only to derive these booleans and are never
14
+ * returned or persisted by this gate.
15
+ */
16
+ export declare function evaluateAgentRecovery(events: readonly ConversationEventV1[], promptId: string, identity: string): AgentRecoveryEvidence;
17
+ export declare function recoverAgentIdentity(session: AgentSession, identity: string): Promise<AgentRecoveryEvidence>;
@@ -0,0 +1,135 @@
1
+ const TOOL_NAMES = {
2
+ choose: new Set(['choose_identity', 'ours.choose_identity', 'mcp__ours__choose_identity']),
3
+ current: new Set(['current_identity', 'ours.current_identity', 'mcp__ours__current_identity']),
4
+ messages: new Set(['get_messages', 'ours.get_messages', 'mcp__ours__get_messages']),
5
+ };
6
+ function recordFor(records, event) {
7
+ if (event.kind !== 'tool.upsert' || !event.toolCallId)
8
+ return undefined;
9
+ const payload = event.payload;
10
+ const previous = records.get(event.toolCallId) ?? {};
11
+ const next = {
12
+ ...previous,
13
+ ...(payload.title !== undefined ? { title: payload.title } : {}),
14
+ ...(payload.status !== undefined ? { status: payload.status } : {}),
15
+ ...(payload.rawInput !== undefined ? { rawInput: payload.rawInput } : {}),
16
+ ...(payload.status === 'completed' ? { completedSeq: event.seq } : {}),
17
+ };
18
+ records.set(event.toolCallId, next);
19
+ return next;
20
+ }
21
+ function objectInput(input) {
22
+ if (!input || input.truncated || input.redacted || !input.json
23
+ || typeof input.json !== 'object' || Array.isArray(input.json))
24
+ return undefined;
25
+ return input.json;
26
+ }
27
+ function safeChoose(record, identity) {
28
+ if (!record.title || !TOOL_NAMES.choose.has(record.title) || record.status !== 'completed')
29
+ return false;
30
+ const input = objectInput(record.rawInput);
31
+ if (!input)
32
+ return false;
33
+ const keys = Object.keys(input).sort();
34
+ if (keys.some(key => key !== 'force' && key !== 'name'))
35
+ return false;
36
+ return input.name === identity && (input.force === undefined || input.force === false);
37
+ }
38
+ function safeNoOrBoundedInput(record, names, allowed) {
39
+ if (!record.title || !names.has(record.title) || record.status !== 'completed')
40
+ return false;
41
+ if (!record.rawInput)
42
+ return allowed({});
43
+ const input = objectInput(record.rawInput);
44
+ return input !== undefined && allowed(input);
45
+ }
46
+ function safeCurrent(record) {
47
+ return safeNoOrBoundedInput(record, TOOL_NAMES.current, input => Object.keys(input).length === 0);
48
+ }
49
+ function safeMessages(record) {
50
+ return safeNoOrBoundedInput(record, TOOL_NAMES.messages, input => {
51
+ const keys = Object.keys(input);
52
+ if (keys.some(key => key !== 'limit'))
53
+ return false;
54
+ return input.limit === undefined
55
+ || (Number.isSafeInteger(input.limit) && input.limit >= 1 && input.limit <= 200);
56
+ });
57
+ }
58
+ /**
59
+ * Verify one exact recovery turn from its durable conversation ledger. Tool
60
+ * arguments/results are inspected only to derive these booleans and are never
61
+ * returned or persisted by this gate.
62
+ */
63
+ export function evaluateAgentRecovery(events, promptId, identity) {
64
+ const ordered = [...events].sort((a, b) => a.seq - b.seq);
65
+ const admitted = ordered.find(event => event.kind === 'prompt.admitted' && event.promptId === promptId);
66
+ const empty = (reason) => ({
67
+ ok: false, reason, chooseIdentity: false, currentIdentity: false,
68
+ getMessages: false, turnCompleted: false,
69
+ });
70
+ if (!admitted)
71
+ return empty('RECOVERY_PROMPT_MISSING');
72
+ const terminal = ordered.find(event => event.seq > admitted.seq
73
+ && event.kind === 'turn.completed'
74
+ && event.promptId === promptId
75
+ && event.sessionGeneration === admitted.sessionGeneration);
76
+ if (!terminal)
77
+ return empty('RECOVERY_TURN_INCOMPLETE');
78
+ const outcome = terminal.payload.outcome;
79
+ if (outcome !== 'completed')
80
+ return empty('RECOVERY_TURN_FAILED');
81
+ const records = new Map();
82
+ for (const event of ordered) {
83
+ if (event.seq <= admitted.seq || event.seq >= terminal.seq
84
+ || event.promptId !== promptId
85
+ || event.sessionGeneration !== admitted.sessionGeneration)
86
+ continue;
87
+ recordFor(records, event);
88
+ }
89
+ const values = [...records.values()];
90
+ const chooses = values.filter(record => safeChoose(record, identity));
91
+ const currents = values.filter(safeCurrent);
92
+ const messages = values.filter(safeMessages);
93
+ const chooseIdentity = chooses.length > 0;
94
+ const currentIdentity = currents.length > 0;
95
+ const getMessages = messages.length > 0;
96
+ const base = { chooseIdentity, currentIdentity, getMessages, turnCompleted: true };
97
+ if (!chooseIdentity)
98
+ return { ok: false, reason: 'RECOVERY_CHOOSE_MISSING', ...base };
99
+ if (!currentIdentity)
100
+ return { ok: false, reason: 'RECOVERY_CURRENT_MISSING', ...base };
101
+ if (!getMessages)
102
+ return { ok: false, reason: 'RECOVERY_GET_MESSAGES_MISSING', ...base };
103
+ const orderedChain = chooses.some(choose => currents.some(current => messages.some(message => choose.completedSeq !== undefined && current.completedSeq !== undefined
104
+ && message.completedSeq !== undefined
105
+ && choose.completedSeq < current.completedSeq
106
+ && current.completedSeq < message.completedSeq)));
107
+ if (!orderedChain)
108
+ return { ok: false, reason: 'RECOVERY_TOOL_ORDER_INVALID', ...base };
109
+ return { ok: true, reason: 'RECOVERY_TOOLS_VERIFIED', ...base };
110
+ }
111
+ export async function recoverAgentIdentity(session, identity) {
112
+ if (!session.subscribeConversation)
113
+ return {
114
+ ok: false, reason: 'RECOVERY_PROMPT_MISSING', chooseIdentity: false,
115
+ currentIdentity: false, getMessages: false, turnCompleted: false,
116
+ };
117
+ const events = [];
118
+ const unsubscribe = session.subscribeConversation(event => events.push(event));
119
+ try {
120
+ const queued = await session.queuePrompt([
121
+ '[fleet-recovery] The shared ours daemon restarted.',
122
+ `Call ours choose_identity with name ${JSON.stringify(identity)} and force false.`,
123
+ 'Then call current_identity, then get_messages. Complete all three in that order.',
124
+ 'Do not create/delete identities, force-bind, interrupt, or restart any service/session.',
125
+ ].join('\n'), { origin: { kind: 'fleet-monitor' } });
126
+ await queued.completion;
127
+ // Conversation publication is synchronous with terminal settlement in the
128
+ // in-tree ACP store; filtering by exact promptId/sessionGeneration remains
129
+ // the authority even if unrelated events arrived concurrently.
130
+ return evaluateAgentRecovery(events, queued.promptId, identity);
131
+ }
132
+ finally {
133
+ unsubscribe();
134
+ }
135
+ }
@@ -4,6 +4,7 @@ import { classifyActivity } from '../session/activity.js';
4
4
  import { controlRequest } from '../session/control.js';
5
5
  import { SessionControlError } from '../session/types.js';
6
6
  import { readExitRecord, readRestartLedger } from '../runner.js';
7
+ import { readDaemonRecoveryStatus } from '../daemon-recovery.js';
7
8
  import { roleCapabilities } from './capabilities.js';
8
9
  import { FleetError } from './errors.js';
9
10
  const clean = (value, max = 512) => value.replace(/[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '').trim().slice(0, max);
@@ -46,7 +47,8 @@ function readIsolation(dir) {
46
47
  function sessionOverall(supervisor, session, restart, monitor, isolation, problems) {
47
48
  if (restart.circuit === 'open' || monitor.health === 'failed' || monitor.health === 'degraded'
48
49
  || isolation.degraded
49
- || problems.some(problem => problem.severity === 'error' || problem.source === 'watchdog')
50
+ || problems.some(problem => problem.severity === 'error'
51
+ || problem.source === 'watchdog' || problem.source === 'daemon-recovery')
50
52
  || session.readiness === 'failed')
51
53
  return 'attention';
52
54
  // A reachable agent session is the user's live interaction surface. Its
@@ -103,6 +105,16 @@ export class FleetQueryService {
103
105
  const lastExit = dir ? readExitRecord(join(dir, '.exit-status')) ?? undefined : undefined;
104
106
  const session = await this.session(role, dir, live.state);
105
107
  const problems = [...role.problems];
108
+ const recovery = dir ? readDaemonRecoveryStatus(dir) : undefined;
109
+ if (recovery && recovery.state !== 'recovered') {
110
+ const paths = ['agent', 'owner']
111
+ .filter(name => recovery.paths[name].state !== 'recovered')
112
+ .map(name => `${name}:${recovery.paths[name].state}`);
113
+ problems.push({
114
+ code: 'daemon_recovery', severity: 'warning', source: 'daemon-recovery',
115
+ detail: `${recovery.state}; ${paths.join(', ') || 'no degraded path'}; epoch ${recovery.epoch || 'unavailable'}`,
116
+ });
117
+ }
106
118
  if (live.state === 'running' && session.reachability !== 'online')
107
119
  problems.push({
108
120
  code: 'supervisor_session_disagreement', severity: 'warning',
@@ -179,7 +179,7 @@ export declare class TaskRoomApplicationService {
179
179
  name: string;
180
180
  version: number;
181
181
  description: string;
182
- builtin?: boolean;
182
+ sourceFile?: string;
183
183
  room?: import("../rooms-tasks/types.js").TemplateRoomConfig;
184
184
  contract?: string;
185
185
  members: import("../rooms-tasks/types.js").TemplateMemberSlot[];
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.6",
3
- "buildId": "e174f2cdb42c",
4
- "commit": "53aa98d46005a693784fbc11957d6118048249c8",
2
+ "version": "1.1.0-nightly.8",
3
+ "buildId": "871d3c2bae20",
4
+ "commit": "cfd589d27e88eb95710df1f037a4c13e22eedb79",
5
5
  "dirty": true,
6
- "builtAt": "2026-08-31T06:49:46.839Z",
6
+ "builtAt": "2026-08-31T09:40:52.301Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { createInterface } from 'node:readline';
8
8
  import { Command } from 'commander';
9
9
  import { VERSION } from './version.js';
10
10
  import { INIT_COMPLETION_GUIDANCE } from './init-guidance.js';
11
+ import { bootstrapPresets } from './preset-bootstrap.js';
11
12
  import { analyzeInstalls, buildInfo, buildLabel, discoverInstalls, runningLabel, } from './provenance.js';
12
13
  import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './paths.js';
13
14
  import { findRole, loadConfig, ROLE_NAME_RE } from './config.js';
@@ -1264,12 +1265,15 @@ cOpt(program.command('doctor').description('prerequisite report'))
1264
1265
  if (!rep.ok)
1265
1266
  throw new FleetCliExit(1);
1266
1267
  });
1267
- program.command('init').description('one-time host setup (units, dirs, linger)')
1268
- .action(async () => {
1268
+ cOpt(program.command('init').description('idempotent host setup plus missing packaged presets'))
1269
+ .action(async (opts) => {
1269
1270
  for (const d of [agentsRoot(), tmpRoot(), logsRoot()])
1270
1271
  mkdirSync(d, { recursive: true });
1271
1272
  for (const m of await pickBackend().init(binPath))
1272
1273
  console.log(m);
1274
+ const seeded = bootstrapPresets(opts.configuration);
1275
+ console.log(`Packaged preset revision ${seeded.revision}: ${seeded.sourceRoot}`);
1276
+ console.log(`Created ${seeded.created.length}; preserved ${seeded.preserved.length} existing file(s).`);
1273
1277
  console.log(INIT_COMPLETION_GUIDANCE);
1274
1278
  });
1275
1279
  const webCommand = cOpt(program.command('web').description('start or open the secure localhost fleet web console'))
@@ -1,5 +1,5 @@
1
1
  export type YamlMode = 'compat' | 'strict';
2
- export type ConfigDiagnosticKind = 'anchor' | 'alias' | 'explicit-tag' | 'non-scalar-key' | 'multiple-documents';
2
+ export type ConfigDiagnosticKind = 'anchor' | 'alias' | 'explicit-tag' | 'non-scalar-key' | 'multiple-documents' | 'deprecated-field';
3
3
  export interface ConfigDiagnostic {
4
4
  severity: 'warning';
5
5
  kind: ConfigDiagnosticKind;
package/dist/config.d.ts CHANGED
@@ -220,7 +220,7 @@ export interface FleetConfig {
220
220
  files: string[];
221
221
  configMode?: 'split-v2';
222
222
  sourceDocuments?: Array<{
223
- kind: 'Manifest' | 'Agent' | 'Role' | 'Brain';
223
+ kind: 'Manifest' | 'Agent' | 'Role' | 'Brain' | 'RoomTemplate';
224
224
  id?: string;
225
225
  path: string;
226
226
  }>;
package/dist/config.js CHANGED
@@ -265,6 +265,35 @@ function readKindDirectory(root, kind, required, yamlMode, diagnostics, files) {
265
265
  }
266
266
  return result;
267
267
  }
268
+ function readRoomTemplateDirectory(root, yamlMode, diagnostics, files) {
269
+ if (!existsSync(root))
270
+ return {};
271
+ assertTrustedPath(root, 'directory');
272
+ const result = {};
273
+ const sources = new Map();
274
+ const names = readdirSync(root)
275
+ .filter(name => ['.yaml', '.yml'].includes(extname(name).toLowerCase())).sort();
276
+ for (const filename of names) {
277
+ const file = join(root, filename);
278
+ assertTrustedPath(file, 'file');
279
+ const id = basename(filename, extname(filename));
280
+ const previous = sources.get(id);
281
+ if (previous)
282
+ throw new ConfigError(`E_DUPLICATE_ID: room template '${id}' defined by both ${previous} and ${file}`);
283
+ const parsed = parseFleetDocument(file, readFileSync(file, 'utf8'), yamlMode);
284
+ diagnostics.push(...parsed.diagnostics);
285
+ if (parsed.value.override_builtin === true)
286
+ diagnostics.push({
287
+ severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
288
+ message: `${file}: override_builtin is deprecated and ignored for a file-backed Room template`,
289
+ });
290
+ files.push(file);
291
+ const validated = validateRoomTemplatesConfig({ [id]: parsed.value }, file)[id];
292
+ result[id] = { ...validated, sourceFile: file };
293
+ sources.set(id, file);
294
+ }
295
+ return result;
296
+ }
268
297
  function selection(raw, kind, agentFile, presets, allowed) {
269
298
  if (!isPlainObject(raw))
270
299
  schemaError(agentFile, `/${kind}`, 'must be { ref } or { inline }');
@@ -586,6 +615,9 @@ export function loadConfig(configPath, options = {}) {
586
615
  let ownerInvite;
587
616
  let roomTemplates;
588
617
  let tasks;
618
+ const fileTemplates = readRoomTemplateDirectory(join(splitRootFor(base), 'room_templates'), options.yamlMode ?? 'compat', diagnostics, files);
619
+ if (Object.keys(fileTemplates).length)
620
+ roomTemplates = fileTemplates;
589
621
  for (const { file, doc } of docs) {
590
622
  if (doc.rooms !== undefined) {
591
623
  if (rooms)
@@ -598,7 +630,21 @@ export function loadConfig(configPath, options = {}) {
598
630
  }
599
631
  if (doc.room_templates !== undefined) {
600
632
  const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
601
- roomTemplates = { ...(roomTemplates ?? {}), ...validated };
633
+ const raw = doc.room_templates;
634
+ for (const [name, template] of Object.entries(validated)) {
635
+ const shadowed = roomTemplates?.[name];
636
+ const marker = raw[name]?.override_builtin;
637
+ if (shadowed && (marker !== true || template.version <= shadowed.version))
638
+ throw new ConfigError(`${file}: room_templates.${name} shadows file preset ${shadowed.sourceFile}; `
639
+ + 'set override_builtin: true and use a higher version');
640
+ if (marker === true)
641
+ diagnostics.push({
642
+ severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
643
+ message: `${file}: room_templates.${name}.override_builtin is deprecated; `
644
+ + 'it is retained only as an explicit manifest-over-file migration marker',
645
+ });
646
+ roomTemplates = { ...(roomTemplates ?? {}), [name]: { ...template, sourceFile: file } };
647
+ }
602
648
  }
603
649
  if (doc.tasks !== undefined) {
604
650
  if (tasks)
@@ -616,7 +662,8 @@ export function loadConfig(configPath, options = {}) {
616
662
  return { kind: 'Manifest', path };
617
663
  const parent = basename(dirname(path));
618
664
  const kind = parent === 'agents' ? 'Agent'
619
- : parent === 'roles' ? 'Role' : 'Brain';
665
+ : parent === 'roles' ? 'Role'
666
+ : parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
620
667
  return { kind, id: basename(path, extname(path)), path };
621
668
  }),
622
669
  };
@@ -0,0 +1,93 @@
1
+ import { type FetchLike } from './monitor.js';
2
+ export declare const DAEMON_RECOVERY_MAX_ATTEMPTS = 6;
3
+ export declare const DAEMON_RECOVERY_INITIAL_BACKOFF_MS = 1000;
4
+ export declare const DAEMON_RECOVERY_MAX_BACKOFF_MS = 5000;
5
+ export declare const DAEMON_RECOVERY_DEADLINE_MS = 60000;
6
+ export interface DaemonGeneration {
7
+ bootId: string;
8
+ pid: number;
9
+ startedAt: number;
10
+ stateDir: string;
11
+ }
12
+ export type DaemonGenerationProbe = {
13
+ state: 'ready';
14
+ generation: DaemonGeneration;
15
+ } | {
16
+ state: 'unavailable';
17
+ reason: string;
18
+ };
19
+ export type DaemonGenerationObservation = {
20
+ kind: 'baseline' | 'stable' | 'available';
21
+ generation: DaemonGeneration;
22
+ } | {
23
+ kind: 'changed';
24
+ previous: DaemonGeneration;
25
+ generation: DaemonGeneration;
26
+ } | {
27
+ kind: 'lost' | 'unavailable';
28
+ previous?: DaemonGeneration;
29
+ reason: string;
30
+ };
31
+ interface GenerationProbeDeps {
32
+ readText?(path: string): string;
33
+ canonicalize?(path: string): string;
34
+ }
35
+ /**
36
+ * Corroborate the loopback daemon's unauthenticated `/info`, its credentialed
37
+ * identity-index readiness route, and its local boot-generation record. The
38
+ * index enforces auth when the daemon visibility requires it; open visibility
39
+ * deliberately does not. No source is sufficient
40
+ * alone: `/info` has no boot id, the identity index has no generation, and a
41
+ * stale `ready` file can outlive the process that wrote it.
42
+ */
43
+ export declare function probeDaemonGeneration(fetch: FetchLike, env: NodeJS.ProcessEnv, deps?: GenerationProbeDeps): Promise<DaemonGenerationProbe>;
44
+ export declare class DaemonGenerationObserver {
45
+ private current?;
46
+ private unavailable;
47
+ observe(probe: DaemonGenerationProbe): DaemonGenerationObservation;
48
+ }
49
+ export declare function daemonRecoveryBackoff(attempt: number): number;
50
+ export type RecoveryPath = 'agent' | 'owner';
51
+ export type RecoveryPathResult = {
52
+ ok: true;
53
+ } | {
54
+ ok: false;
55
+ reason: string;
56
+ };
57
+ export interface RoleRecoveryControllerOptions {
58
+ role: string;
59
+ identity: string;
60
+ stateDir: string;
61
+ now(): number;
62
+ sleep(ms: number): Promise<void>;
63
+ recoverAgent(epoch: string): Promise<RecoveryPathResult>;
64
+ recoverOwner(epoch: string): Promise<RecoveryPathResult>;
65
+ log(line: string): void;
66
+ }
67
+ export interface RecoveryStatus {
68
+ version: 1;
69
+ identity: string;
70
+ epoch: string;
71
+ state: 'recovering' | 'recovered' | 'degraded' | 'cancelled';
72
+ paths: Record<RecoveryPath, {
73
+ state: 'pending' | 'recovered' | 'degraded';
74
+ attempts: number;
75
+ reason?: string;
76
+ }>;
77
+ updatedAt: string;
78
+ }
79
+ export declare function readDaemonRecoveryStatus(dir: string): RecoveryStatus | undefined;
80
+ /** Per-role, per-generation bounded recovery with path-level fault isolation. */
81
+ export declare class RoleRecoveryController {
82
+ private readonly options;
83
+ private token;
84
+ private activeEpoch?;
85
+ private active?;
86
+ private status?;
87
+ constructor(options: RoleRecoveryControllerOptions);
88
+ recover(generation: DaemonGeneration): Promise<RecoveryStatus>;
89
+ cancel(): void;
90
+ noteLoss(reason: string): void;
91
+ private write;
92
+ }
93
+ export {};