@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.
- package/README.md +23 -4
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/fleet-query-service.js +13 -1
- package/dist/application/task-room-service.d.ts +1 -1
- package/dist/build-info.json +4 -4
- package/dist/cli.js +6 -2
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +49 -2
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +26 -3
- package/dist/doctor.js +14 -0
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/owner-channel/channel.d.ts +40 -0
- package/dist/owner-channel/channel.js +210 -16
- package/dist/owner-channel/commands.js +3 -9
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/rooms-tasks/cli.js +4 -4
- package/dist/rooms-tasks/config.js +0 -4
- package/dist/rooms-tasks/templates.d.ts +0 -1
- package/dist/rooms-tasks/templates.js +9 -76
- package/dist/rooms-tasks/types.d.ts +2 -1
- package/dist/runner.d.ts +8 -1
- package/dist/runner.js +74 -3
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/web/fleet-config-service.js +4 -3
- package/package.json +2 -1
- package/presets/fleet/agents/Agent.yaml +3 -0
- package/presets/fleet/agents/Architect.yaml +3 -0
- package/presets/fleet/agents/Critic.yaml +3 -0
- package/presets/fleet/agents/Developer.yaml +3 -0
- package/presets/fleet/agents/Secretary.yaml +3 -0
- package/presets/fleet/agents/Tester.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/roles/Agent.yaml +6 -0
- package/presets/fleet/roles/Architect.yaml +6 -0
- package/presets/fleet/roles/Critic.yaml +6 -0
- package/presets/fleet/roles/Developer.yaml +6 -0
- package/presets/fleet/roles/Secretary.yaml +6 -0
- package/presets/fleet/roles/Tester.yaml +6 -0
- package/presets/fleet/room_templates/pair.yaml +11 -0
- package/presets/fleet/room_templates/single.yaml +8 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- 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
|
-
|
|
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
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/temp-lifecycle.d.ts
CHANGED
|
@@ -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'
|
|
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.
|
|
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,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.
|