@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
@@ -1,54 +1,4 @@
1
1
  import { createHash } from 'node:crypto';
2
- const TEAM = {
3
- name: 'team',
4
- version: 1,
5
- builtin: true,
6
- description: 'Phased task pipeline: Architect specifies, Developer implements, Tester verifies',
7
- room: { quiet_membership: false, anonymous: false },
8
- contract: [
9
- 'Architect produces a spec and posts it to the room. Work pauses for owner approval.',
10
- 'Developer implements per approved spec.',
11
- 'Tester verifies spec conformance and test coverage.',
12
- 'Completion requires tester sign-off.',
13
- ].join('\n'),
14
- members: [
15
- { slot: 'architect', role: 'Architect', count: 1, agent: { ref: 'Architect' } },
16
- { slot: 'developer', role: 'Developer', count: 1, agent: { ref: 'Developer' } },
17
- { slot: 'tester', role: 'Tester', count: 1, agent: { ref: 'Tester' } },
18
- ],
19
- };
20
- const PAIR = {
21
- name: 'pair',
22
- version: 1,
23
- builtin: true,
24
- description: 'Deliberation pair: Secretary writes code, Critic reviews — every decision deliberated together',
25
- room: { quiet_membership: false, anonymous: false },
26
- contract: [
27
- 'Secretary and Critic deliberate every decision together before acting.',
28
- 'Secretary writes code; Critic reviews and challenges.',
29
- 'No material change lands without both agreeing.',
30
- 'Completion requires joint sign-off.',
31
- ].join('\n'),
32
- members: [
33
- { slot: 'secretary', role: 'Secretary', count: 1, agent: { ref: 'Secretary' } },
34
- { slot: 'critic', role: 'Critic', count: 1, agent: { ref: 'Critic' } },
35
- ],
36
- };
37
- const SINGLE = {
38
- name: 'single',
39
- version: 1,
40
- builtin: true,
41
- description: 'Solo agent: one general-purpose agent works the task with owner oversight',
42
- room: { quiet_membership: false, anonymous: false },
43
- contract: [
44
- 'Agent works the task autonomously, consulting the owner when blocked.',
45
- 'Owner reviews and approves completion.',
46
- ].join('\n'),
47
- members: [
48
- { slot: 'agent', role: 'Agent', count: 1, agent: { ref: 'Agent' } },
49
- ],
50
- };
51
- export const BUILTIN_TEMPLATES = [TEAM, PAIR, SINGLE];
52
2
  export function hashTemplate(t) {
53
3
  const canonical = JSON.stringify({
54
4
  name: t.name, version: t.version, description: t.description,
@@ -57,38 +7,21 @@ export function hashTemplate(t) {
57
7
  return createHash('sha256').update(canonical).digest('hex');
58
8
  }
59
9
  export function snapshotTemplate(t) {
60
- return { ...t, content_hash: hashTemplate(t) };
10
+ const { sourceFile: _, ...semantic } = t;
11
+ return { ...semantic, content_hash: hashTemplate(t) };
61
12
  }
62
13
  export function resolveTemplate(name, customTemplates) {
63
14
  const atIdx = name.lastIndexOf('@');
64
15
  const baseName = atIdx > 0 ? name.slice(0, atIdx) : name;
65
16
  const versionStr = atIdx > 0 ? name.slice(atIdx + 1) : undefined;
66
17
  const version = versionStr ? parseInt(versionStr, 10) : undefined;
67
- const custom = customTemplates[baseName];
68
- if (custom) {
69
- if (version !== undefined && custom.version !== version)
70
- return undefined;
71
- return custom;
72
- }
73
- const builtin = BUILTIN_TEMPLATES.find(b => b.name === baseName);
74
- if (builtin) {
75
- if (version !== undefined && builtin.version !== version)
76
- return undefined;
77
- return builtin;
78
- }
79
- return undefined;
18
+ const template = customTemplates[baseName];
19
+ if (!template || (version !== undefined && template.version !== version))
20
+ return undefined;
21
+ return template;
80
22
  }
81
23
  export function listTemplates(customTemplates) {
82
- const result = [];
83
- const overridden = new Set();
84
- for (const [name, t] of Object.entries(customTemplates)) {
85
- result.push({ ...t, name });
86
- if (BUILTIN_TEMPLATES.some(b => b.name === name))
87
- overridden.add(name);
88
- }
89
- for (const b of BUILTIN_TEMPLATES) {
90
- if (!overridden.has(b.name))
91
- result.push(b);
92
- }
93
- return result.sort((a, b) => a.name.localeCompare(b.name));
24
+ return Object.entries(customTemplates)
25
+ .map(([name, template]) => ({ ...template, name }))
26
+ .sort((a, b) => a.name.localeCompare(b.name));
94
27
  }
@@ -205,7 +205,8 @@ export interface TemplateDefinition {
205
205
  name: string;
206
206
  version: number;
207
207
  description: string;
208
- builtin?: boolean;
208
+ /** Authoring provenance only; excluded from snapshots and semantic hashes. */
209
+ sourceFile?: string;
209
210
  room?: TemplateRoomConfig;
210
211
  contract?: string;
211
212
  members: TemplateMemberSlot[];
package/dist/runner.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ResolvedRole } from './config.js';
2
2
  import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
3
+ import { type DaemonGenerationProbe } from './daemon-recovery.js';
3
4
  import { type Exec } from './exec.js';
4
5
  import { RoleControlServer } from './session/control.js';
5
6
  import type { AgentSession, ExitRecord, TurnResult } from './session/types.js';
@@ -15,6 +16,7 @@ export interface RunnerDeps {
15
16
  log(line: string): void;
16
17
  /** HTTP transport for the monitor's daemon long-poll (injectable for tests). */
17
18
  fetch: FetchLike;
19
+ probeGeneration(env: NodeJS.ProcessEnv): Promise<DaemonGenerationProbe>;
18
20
  /** Construct the supervisor mail monitor (injectable so tests stub it out). */
19
21
  createMonitor(opts: MonitorOpts): MonitorHandle;
20
22
  /** Construct trusted owner ingress (injectable for lifecycle tests). */
@@ -31,6 +33,11 @@ export interface RunnerDeps {
31
33
  /** Lets a test (or a shutdown path) end the supervised restart loop. */
32
34
  shouldStop?(): boolean;
33
35
  }
36
+ export declare const SUPERVISOR_RECYCLE_REQUIRED = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
37
+ export declare class SupervisorRecycleRequiredError extends Error {
38
+ readonly code = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
39
+ constructor();
40
+ }
34
41
  /** Environment injected only into the managed harness process. */
35
42
  export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record<string, string>;
36
43
  /**
@@ -176,4 +183,4 @@ export declare function runSupervised(name: string, opts?: {
176
183
  allowResumeRotation?: boolean;
177
184
  }, d: Partial<RunnerDeps>) => Promise<AttemptResult>): Promise<RestartLedger>;
178
185
  /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
179
- export declare function runTemp(name: string, deps?: Partial<RunnerDeps>): Promise<void>;
186
+ export declare function runTemp(name: string, deps?: Partial<RunnerDeps>, attempt?: typeof runOnce): Promise<void>;
package/dist/runner.js CHANGED
@@ -6,6 +6,8 @@ import { agentDir, stateRoot } from './paths.js';
6
6
  import { loadConfig, findRole, isolationContextFor, resolveMonitorConfig, resolvePermissions, } from './config.js';
7
7
  import { getAdapter } from './harness/registry.js';
8
8
  import { createMonitor, probeIdentityPresence, } from './monitor.js';
9
+ import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
10
+ import { recoverAgentIdentity } from './agent-recovery-gate.js';
9
11
  import { realExec } from './exec.js';
10
12
  import { resolveIsolation } from './isolation/policy.js';
11
13
  import { selectIsolationBackend } from './isolation/registry.js';
@@ -24,6 +26,14 @@ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-prox
24
26
  import { effectivePermissionMode } from './permissions.js';
25
27
  import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
26
28
  import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
29
+ export const SUPERVISOR_RECYCLE_REQUIRED = 'OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED';
30
+ export class SupervisorRecycleRequiredError extends Error {
31
+ code = SUPERVISOR_RECYCLE_REQUIRED;
32
+ constructor() {
33
+ super('owner channel requires a fresh supervisor process after unproven client quiescence');
34
+ this.name = 'SupervisorRecycleRequiredError';
35
+ }
36
+ }
27
37
  const defaultDeps = () => ({
28
38
  exec: realExec,
29
39
  cpuDelegated: () => cpuControllerDelegated(),
@@ -38,6 +48,7 @@ const defaultDeps = () => ({
38
48
  now: () => Date.now(),
39
49
  log: line => process.stderr.write(line + '\n'),
40
50
  fetch: (url, init) => globalThis.fetch(url, init),
51
+ probeGeneration: env => probeDaemonGeneration((url, init) => globalThis.fetch(url, init), env),
41
52
  createMonitor: opts => createMonitor(opts),
42
53
  createOwnerChannel: opts => new OwnerChannel(opts),
43
54
  startAgentSession: (adapter, options) => adapter.start(options),
@@ -532,6 +543,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
532
543
  name, identity: role.identity, agentDir: dir, cfg: role.monitor,
533
544
  deps: resolvedMonitorDeps,
534
545
  }) : null;
546
+ const daemonObserver = new DaemonGenerationObserver();
547
+ const initialDaemon = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
548
+ let sessionStartedWithoutDaemonBaseline = initialDaemon.kind !== 'baseline';
535
549
  if (monitor)
536
550
  await monitor.prime({ resetCursor: resetMonitorCursor });
537
551
  rmSync(exitFile, { force: true });
@@ -546,6 +560,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
546
560
  let ownerBinder;
547
561
  let loopManager;
548
562
  let arbiter;
563
+ let recoveryController;
549
564
  let reloadLoopConfig;
550
565
  let loopGeneration = JSON.stringify((role.loops ?? []).map(loop => [
551
566
  loop.name, loop.definitionHash, loop.promptHash,
@@ -782,16 +797,49 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
782
797
  // The monitor loop lives exactly as long as the session: it starts once the
783
798
  // pane pid is known and is stopped when that pid dies (task dies with runner).
784
799
  monitorLoop ??= monitor?.run(pid);
800
+ recoveryController = new RoleRecoveryController({
801
+ role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
802
+ log: deps.log,
803
+ recoverAgent: async () => {
804
+ const evidence = await recoverAgentIdentity(arbiter, role.identity);
805
+ return evidence.ok ? { ok: true } : { ok: false, reason: evidence.reason };
806
+ },
807
+ recoverOwner: async (epoch) => {
808
+ if (!ownerChannel?.recover)
809
+ return { ok: true };
810
+ try {
811
+ await ownerChannel.recover(epoch);
812
+ return { ok: true };
813
+ }
814
+ catch (error) {
815
+ return { ok: false, reason: error instanceof Error
816
+ ? `OWNER_${error.name.toUpperCase()}` : 'OWNER_UNKNOWN_ERROR' };
817
+ }
818
+ },
819
+ });
785
820
  const start = deps.now();
786
821
  let nextLoopReloadAt = deps.now() + 30_000;
787
822
  let nextIdentityPollAt = deps.now();
823
+ let nextDaemonProbeAt = deps.now();
788
824
  let lastReloadError = '';
789
825
  let identityObserved = false;
790
826
  let identityAbsentSince;
791
827
  let retirementReason;
828
+ let supervisorRecycleRequired = false;
792
829
  while (sessionHandle.isAlive()) {
793
830
  await deps.sleep(temp ? 500 : 2000);
794
831
  const now = deps.now();
832
+ if (now >= nextDaemonProbeAt) {
833
+ nextDaemonProbeAt = now + 2_000;
834
+ const observation = daemonObserver.observe(await deps.probeGeneration(resolvedMonitorDeps.env));
835
+ if (observation.kind === 'lost')
836
+ recoveryController.noteLoss(observation.reason);
837
+ if (observation.kind === 'changed' || observation.kind === 'available'
838
+ || (observation.kind === 'baseline' && sessionStartedWithoutDaemonBaseline)) {
839
+ sessionStartedWithoutDaemonBaseline = false;
840
+ void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
841
+ }
842
+ }
795
843
  if (temp && deps.shouldStop?.()) {
796
844
  retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
797
845
  deps.log(`[${name}] temporary supervisor retirement requested (${retirementReason})`);
@@ -842,6 +890,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
842
890
  control?.setLoopManager(undefined);
843
891
  await loopManager.stop();
844
892
  }
893
+ // Let already-settled path operations publish their aggregate result before
894
+ // the shutdown fence invalidates the epoch. This never waits on I/O.
895
+ await Promise.resolve();
896
+ await Promise.resolve();
897
+ recoveryController?.cancel();
845
898
  control?.setConfigReloader(undefined);
846
899
  // Close the authenticated control route before releasing the binder lease;
847
900
  // otherwise the predecessor can unlink the replacement's new socket.
@@ -852,7 +905,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
852
905
  }
853
906
  if (ownerChannel)
854
907
  await ownerChannel.close();
855
- ownerBinder?.release();
908
+ if (ownerChannel?.binderReleaseSafe?.() === false) {
909
+ deps.log(`[${name}] owner binder retained because client quiescence was not proven at shutdown`);
910
+ supervisorRecycleRequired = true;
911
+ }
912
+ else
913
+ ownerBinder?.release();
856
914
  if (monitor) {
857
915
  monitor.stop();
858
916
  await monitorLoop;
@@ -901,6 +959,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
901
959
  }
902
960
  else
903
961
  deps.log(`[${name}] ${exitRecord.detail} (${elapsed.toFixed(0)}s) -> next start RESUMES context`);
962
+ if (supervisorRecycleRequired)
963
+ throw new SupervisorRecycleRequiredError();
904
964
  return {
905
965
  elapsedSecs: elapsed, exit: exitRecord, rotated, mode, modelRecovery,
906
966
  ...(retirementReason ? { retirementReason } : {}),
@@ -977,6 +1037,16 @@ export async function runSupervised(name, opts = {}, partialDeps = {}, attempt =
977
1037
  result = await attempt(name, { configPath: opts.configPath, allowResumeRotation: !ledger.resumeDiscarded }, deps);
978
1038
  }
979
1039
  catch (e) {
1040
+ if (e instanceof SupervisorRecycleRequiredError) {
1041
+ writeRestartLedger(dir, {
1042
+ ...ledger,
1043
+ lastReason: SUPERVISOR_RECYCLE_REQUIRED,
1044
+ nextDelayMs: 0,
1045
+ updatedAt: stamp(),
1046
+ });
1047
+ deps.log(`[${name}] supervisor recycle required: ${SUPERVISOR_RECYCLE_REQUIRED}`);
1048
+ throw e;
1049
+ }
980
1050
  // A session that could not even start is an immediate failure like any
981
1051
  // other; it must count, or an unstartable role loops forever.
982
1052
  result = {
@@ -1070,7 +1140,7 @@ function fastFailSecsFor(name, configPath) {
1070
1140
  }
1071
1141
  }
1072
1142
  /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */
1073
- export async function runTemp(name, deps = {}) {
1143
+ export async function runTemp(name, deps = {}, attempt = runOnce) {
1074
1144
  const dir = agentDir(name, true);
1075
1145
  await markTempSupervisorActive(dir);
1076
1146
  let signal;
@@ -1081,7 +1151,7 @@ export async function runTemp(name, deps = {}) {
1081
1151
  let result;
1082
1152
  let failure;
1083
1153
  try {
1084
- result = await runOnce(name, { temp: true }, {
1154
+ result = await attempt(name, { temp: true }, {
1085
1155
  ...deps,
1086
1156
  shouldStop: () => Boolean(signal) || (deps.shouldStop?.() ?? false),
1087
1157
  });
@@ -1095,6 +1165,7 @@ export async function runTemp(name, deps = {}) {
1095
1165
  const requested = requestedTempStopReason(dir);
1096
1166
  const reason = requested
1097
1167
  ?? result?.retirementReason
1168
+ ?? (failure instanceof SupervisorRecycleRequiredError ? 'supervisor-recycle' : undefined)
1098
1169
  ?? (signal ? 'supervisor-signal' : failure ? 'startup-failure' : 'session-ended');
1099
1170
  // A service-manager stop can make the child connection close before the
1100
1171
  // runner reaches its normal loop. That is still a successful requested
@@ -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;
@@ -12,7 +12,7 @@ import { isSensitiveConfigKey } from '../sensitive-config.js';
12
12
  export const REDACTED_ENV_VALUE = '__OURS_FLEET_SECRET_REDACTED__';
13
13
  const MAX_CONFIG_BYTES = 256 * 1024;
14
14
  const DIFF_CONTEXT_LINES = 3;
15
- const KINDS = ['agents', 'roles', 'brains'];
15
+ const KINDS = ['agents', 'roles', 'brains', 'room_templates'];
16
16
  const emptyReport = () => ({ ok: true, checks: [] });
17
17
  export class FleetConfigService {
18
18
  path;
@@ -176,7 +176,8 @@ function readSources(manifest) {
176
176
  }
177
177
  trustedDirectory(dir);
178
178
  for (const name of readdirSync(dir).sort()) {
179
- if (kind === 'agents' && !['.yaml', '.yml'].includes(extname(name).toLowerCase()))
179
+ if ((kind === 'agents' || kind === 'room_templates')
180
+ && !['.yaml', '.yml'].includes(extname(name).toLowerCase()))
180
181
  continue;
181
182
  documents.set(join(kind, name), trustedFile(join(dir, name)));
182
183
  }
@@ -222,7 +223,7 @@ function renderProposal(current, model) {
222
223
  ? render(current.documents.get(oldRel), doc) : stringify(doc));
223
224
  }
224
225
  for (const [rel, source] of current.documents)
225
- if (rel.startsWith('roles/') || rel.startsWith('brains/'))
226
+ if (rel.startsWith('roles/') || rel.startsWith('brains/') || rel.startsWith('room_templates/'))
226
227
  next.set(rel, source);
227
228
  return next;
228
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.1.0-nightly.6",
3
+ "version": "1.1.0-nightly.8",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -16,6 +16,7 @@
16
16
  "dist",
17
17
  "contrib",
18
18
  "examples",
19
+ "presets",
19
20
  "README.md",
20
21
  "LICENSE"
21
22
  ],
@@ -0,0 +1,3 @@
1
+ role: { ref: Agent }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Architect }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Critic }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Developer }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Secretary }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,3 @@
1
+ role: { ref: Tester }
2
+ brain: { ref: claude-default }
3
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
@@ -0,0 +1,5 @@
1
+ harness: claude-code
2
+ session: acp
3
+ model: null
4
+ effort: high
5
+ max_tokens: 500000
@@ -0,0 +1,6 @@
1
+ mission: Complete the assigned task safely and report a verifiable result.
2
+ persona: |
3
+ Work autonomously within the stated scope. Inspect before changing, preserve unrelated
4
+ state, test material work, and ask the owner when authority or intent is ambiguous.
5
+ Do not expand scope, publish, deploy, or claim completion without evidence.
6
+ bio: General-purpose task agent; engage for one bounded task with owner oversight.
@@ -0,0 +1,6 @@
1
+ mission: Produce an implementable specification for the assigned goal.
2
+ persona: |
3
+ Map constraints, interfaces, risks, acceptance criteria, and migration behavior before
4
+ implementation. Resolve ambiguity with the owner and hand off a bounded design. Do not
5
+ implement or approve deployment unless the task explicitly expands your role.
6
+ bio: Solution architect; engage to turn a goal into a bounded, reviewable specification.
@@ -0,0 +1,6 @@
1
+ mission: Independently challenge the proposed solution and verify its evidence.
2
+ persona: |
3
+ Audit assumptions, safety boundaries, regressions, tests, and delivery claims. Explain
4
+ concrete objections and acceptance criteria; do not edit the implementation or block
5
+ on taste alone. Sign off only when material risks have evidence-backed resolution.
6
+ bio: Independent reviewer; engage to challenge design, implementation, and verification.
@@ -0,0 +1,6 @@
1
+ mission: Implement the approved specification with maintainable code and tests.
2
+ persona: |
3
+ Make the smallest coherent change that satisfies the specification. Preserve unrelated
4
+ work, validate behavior in proportion to risk, and report deviations immediately. Do
5
+ not redesign approved scope, waive failing tests, publish, or deploy without authority.
6
+ bio: Implementation specialist; engage after a specification is approved.
@@ -0,0 +1,6 @@
1
+ mission: Implement the agreed solution and maintain the task's shared record.
2
+ persona: |
3
+ Turn agreed decisions into focused code, documentation, and tests. Keep collaborators
4
+ informed with compact evidence and preserve unrelated state. Pause material work for
5
+ required review; do not merge, publish, or widen scope without owner authorization.
6
+ bio: Implementing partner and record keeper; engage to deliver reviewed changes.
@@ -0,0 +1,6 @@
1
+ mission: Verify that the implementation satisfies the approved specification.
2
+ persona: |
3
+ Test observable behavior, failure paths, compatibility, and regression risk independently.
4
+ Report reproducible evidence and distinguish defects from preferences. Do not repair the
5
+ implementation or sign off with unresolved material failures.
6
+ bio: Independent verifier; engage to assess conformance, safety, and regression coverage.
@@ -0,0 +1,11 @@
1
+ version: 1
2
+ description: "Deliberation pair: Secretary writes code, Critic reviews — every decision deliberated together"
3
+ room: { quiet_membership: false, anonymous: false }
4
+ contract: |
5
+ Secretary and Critic deliberate every decision together before acting.
6
+ Secretary writes code; Critic reviews and challenges.
7
+ No material change lands without both agreeing.
8
+ Completion requires joint sign-off.
9
+ members:
10
+ - { slot: secretary, role: Secretary, count: 1, agent: { ref: Secretary } }
11
+ - { slot: critic, role: Critic, count: 1, agent: { ref: Critic } }
@@ -0,0 +1,8 @@
1
+ version: 1
2
+ description: "Solo agent: one general-purpose agent works the task with owner oversight"
3
+ room: { quiet_membership: false, anonymous: false }
4
+ contract: |
5
+ Agent works the task autonomously, consulting the owner when blocked.
6
+ Owner reviews and approves completion.
7
+ members:
8
+ - { slot: agent, role: Agent, count: 1, agent: { ref: Agent } }
@@ -0,0 +1,12 @@
1
+ version: 1
2
+ description: "Phased task pipeline: Architect specifies, Developer implements, Tester verifies"
3
+ room: { quiet_membership: false, anonymous: false }
4
+ contract: |
5
+ Architect produces a spec and posts it to the room. Work pauses for owner approval.
6
+ Developer implements per approved spec.
7
+ Tester verifies spec conformance and test coverage.
8
+ Completion requires tester sign-off.
9
+ members:
10
+ - { slot: architect, role: Architect, count: 1, agent: { ref: Architect } }
11
+ - { slot: developer, role: Developer, count: 1, agent: { ref: Developer } }
12
+ - { slot: tester, role: Tester, count: 1, agent: { ref: Tester } }
@@ -0,0 +1,7 @@
1
+ api_version: ours.network/fleet/v2
2
+
3
+ defaults:
4
+ permissions: { approval: ask, filesystem: workspace, unattended: deny }
5
+
6
+ # Rooms require an authenticated owner CID/invite before they can be started.
7
+ # See `ours-fleet docs`, then add `rooms:` and `tasks:` here.
@@ -0,0 +1,5 @@
1
+ {
2
+ "schema_version": 1,
3
+ "preset_revision": 1,
4
+ "description": "ours-fleet standard configuration presets"
5
+ }