@ours.network/fleet 1.1.0-nightly.20 → 1.1.0-nightly.22
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 +48 -4
- package/dist/application/task-room-service.js +16 -8
- package/dist/build-info.json +4 -4
- package/dist/cli.js +15 -4
- package/dist/config.d.ts +8 -1
- package/dist/config.js +17 -7
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -2
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +21 -0
- package/dist/fleet-command-audit.js +14 -2
- package/dist/lifecycle-summary.d.ts +16 -0
- package/dist/lifecycle-summary.js +18 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +35 -3
- package/dist/preset-migration.d.ts +1 -1
- package/dist/preset-migration.js +23 -6
- package/dist/resolved-plan.js +9 -1
- package/dist/rooms-tasks/cli.js +14 -3
- package/dist/rooms-tasks/launch-snapshot.js +5 -0
- package/dist/rooms-tasks/member-overrides.d.ts +3 -0
- package/dist/rooms-tasks/member-overrides.js +39 -5
- package/dist/rooms-tasks/provision.js +5 -0
- package/dist/rooms-tasks/types.d.ts +2 -0
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +17 -3
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +65 -8
- package/package.json +1 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +45 -0
- package/presets/fleet/room_templates/team.yaml +1 -0
- package/presets/manifest.json +1 -1
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -8,14 +8,17 @@ import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from './launch-snaps
|
|
|
8
8
|
import { parseGroupedMemberArgs, readMembersFile } from './member-overrides.js';
|
|
9
9
|
const MEMBER_ARG_FLAGS = new Set([
|
|
10
10
|
'--member', '--agent-template', '--brain', '--role', '--approval', '--filesystem', '--unattended',
|
|
11
|
-
'--cwd', '--model', '--effort',
|
|
11
|
+
'--cwd', '--model', '--effort', '--loops-file', '--no-loops',
|
|
12
12
|
]);
|
|
13
13
|
export function cliMemberOverrides(membersFile, argv = process.argv.slice(2)) {
|
|
14
14
|
const grouped = [];
|
|
15
15
|
for (let i = 0; i < argv.length; i++)
|
|
16
16
|
if (MEMBER_ARG_FLAGS.has(argv[i])) {
|
|
17
|
-
grouped.push(argv[i]
|
|
18
|
-
i
|
|
17
|
+
grouped.push(argv[i]);
|
|
18
|
+
if (argv[i] !== '--no-loops') {
|
|
19
|
+
grouped.push(argv[i + 1]);
|
|
20
|
+
i += 1;
|
|
21
|
+
}
|
|
19
22
|
}
|
|
20
23
|
if (membersFile && grouped.length)
|
|
21
24
|
throw new Error('--members-file cannot be combined with grouped --member options');
|
|
@@ -712,6 +715,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
712
715
|
.option('--cwd <path>', 'working directory for the current member block')
|
|
713
716
|
.option('--model <id>', 'model for the current member block')
|
|
714
717
|
.option('--effort <level>', 'reasoning effort for the current member block')
|
|
718
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for the current temporary member')
|
|
719
|
+
.option('--no-loops', 'disable loops for the current temporary member')
|
|
715
720
|
.option('--json', 'JSON output')
|
|
716
721
|
.action(async (opts, command) => {
|
|
717
722
|
try {
|
|
@@ -926,6 +931,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
926
931
|
.option('--cwd <path>', 'working directory for current member')
|
|
927
932
|
.option('--model <id>', 'model for current member')
|
|
928
933
|
.option('--effort <level>', 'reasoning effort for current member')
|
|
934
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for current member')
|
|
935
|
+
.option('--no-loops', 'disable loops for current member')
|
|
929
936
|
.option('--json', 'JSON output')
|
|
930
937
|
.action(async (id, opts, command) => {
|
|
931
938
|
try {
|
|
@@ -1336,6 +1343,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1336
1343
|
.option('--cwd <path>', 'working directory for current member')
|
|
1337
1344
|
.option('--model <id>', 'model for current member')
|
|
1338
1345
|
.option('--effort <level>', 'reasoning effort for current member')
|
|
1346
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for current member')
|
|
1347
|
+
.option('--no-loops', 'disable loops for current member')
|
|
1339
1348
|
.option('--json', 'JSON output')
|
|
1340
1349
|
.action(async (id, opts, command) => {
|
|
1341
1350
|
try {
|
|
@@ -1446,6 +1455,8 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1446
1455
|
.option('--cwd <path>', 'working directory for current member')
|
|
1447
1456
|
.option('--model <id>', 'model for current member')
|
|
1448
1457
|
.option('--effort <level>', 'reasoning effort for current member')
|
|
1458
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for current member')
|
|
1459
|
+
.option('--no-loops', 'disable loops for current member')
|
|
1449
1460
|
.option('--json', 'JSON output')
|
|
1450
1461
|
.action(async (opts, command) => {
|
|
1451
1462
|
try {
|
|
@@ -99,6 +99,11 @@ function sweepUnreferencedSnapshotsLocked() {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
export function redactLaunchDefinition(value, key = '') {
|
|
102
|
+
if (key === 'prompt' && typeof value === 'string')
|
|
103
|
+
return {
|
|
104
|
+
bytes: Buffer.byteLength(value, 'utf8'),
|
|
105
|
+
sha256: createHash('sha256').update(value).digest('hex'),
|
|
106
|
+
};
|
|
102
107
|
if (['env', 'harness_options', 'session_options', 'owner_channel', 'auth_proxy'].includes(key))
|
|
103
108
|
return '<redacted>';
|
|
104
109
|
if (Array.isArray(value))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AgentDefinition, type AgentTemplateDefinition, type FleetConfig } from '../config.js';
|
|
2
2
|
import type { TemplateDefinition, TemplateSnapshot } from './types.js';
|
|
3
|
+
import type { AgentLoopsConfig } from '../loops/config.js';
|
|
3
4
|
export interface MemberOverride {
|
|
4
5
|
agent_template?: string;
|
|
5
6
|
brain?: string;
|
|
@@ -10,6 +11,8 @@ export interface MemberOverride {
|
|
|
10
11
|
cwd?: string;
|
|
11
12
|
model?: string;
|
|
12
13
|
effort?: string;
|
|
14
|
+
/** Whole-block override; false is the explicit no-loops policy. */
|
|
15
|
+
loops?: AgentLoopsConfig | false;
|
|
13
16
|
overrides?: Partial<AgentDefinition>;
|
|
14
17
|
}
|
|
15
18
|
export type MemberOverrides = Record<string, MemberOverride>;
|
|
@@ -6,6 +6,8 @@ import { analyzeRolePermissions } from '../permissions.js';
|
|
|
6
6
|
import { canonicalJson } from '../canonical-json.js';
|
|
7
7
|
import { redactLaunchDefinition } from './launch-snapshot.js';
|
|
8
8
|
import { snapshotTemplate } from './templates.js';
|
|
9
|
+
import { readLoopsFile } from '../spawn.js';
|
|
10
|
+
import { canonicalAgentLoops, resolveAgentLoops } from '../loops/config.js';
|
|
9
11
|
export function hashMemberOverrides(overrides) {
|
|
10
12
|
return createHash('sha256').update(canonicalJson(overrides)).digest('hex');
|
|
11
13
|
}
|
|
@@ -16,20 +18,37 @@ const OPTION_FIELDS = {
|
|
|
16
18
|
};
|
|
17
19
|
/** Parse only the ordered member-option subsequence supplied by the CLI action. */
|
|
18
20
|
export function parseGroupedMemberArgs(argv) {
|
|
19
|
-
if (argv.length % 2)
|
|
20
|
-
throw new Error(`${argv.at(-1)}: value required`);
|
|
21
21
|
const result = {};
|
|
22
22
|
let current;
|
|
23
|
-
for (let index = 0; index < argv.length;
|
|
23
|
+
for (let index = 0; index < argv.length;) {
|
|
24
24
|
const flag = argv[index];
|
|
25
|
+
if (flag === '--no-loops') {
|
|
26
|
+
if (!current)
|
|
27
|
+
throw new Error(`${flag} must follow --member <slot>`);
|
|
28
|
+
if (result[current].loops !== undefined)
|
|
29
|
+
throw new Error(`duplicate loop override for member '${current}'`);
|
|
30
|
+
result[current].loops = false;
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
25
34
|
const value = argv[index + 1];
|
|
26
|
-
if (!value)
|
|
35
|
+
if (!value || value.startsWith('--'))
|
|
27
36
|
throw new Error(`${flag}: value required`);
|
|
28
37
|
if (flag === '--member') {
|
|
29
38
|
if (result[value])
|
|
30
39
|
throw new Error(`duplicate member slot '${value}'`);
|
|
31
40
|
current = value;
|
|
32
41
|
result[current] = {};
|
|
42
|
+
index += 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (flag === '--loops-file') {
|
|
46
|
+
if (!current)
|
|
47
|
+
throw new Error(`${flag} must follow --member <slot>`);
|
|
48
|
+
if (result[current].loops !== undefined)
|
|
49
|
+
throw new Error(`duplicate loop override for member '${current}'`);
|
|
50
|
+
result[current].loops = readLoopsFile(value);
|
|
51
|
+
index += 2;
|
|
33
52
|
continue;
|
|
34
53
|
}
|
|
35
54
|
const field = OPTION_FIELDS[flag];
|
|
@@ -40,6 +59,7 @@ export function parseGroupedMemberArgs(argv) {
|
|
|
40
59
|
if (result[current][field] !== undefined)
|
|
41
60
|
throw new Error(`duplicate ${flag} for member '${current}'`);
|
|
42
61
|
result[current][field] = value;
|
|
62
|
+
index += 2;
|
|
43
63
|
}
|
|
44
64
|
for (const [slot, value] of Object.entries(result))
|
|
45
65
|
validateMemberOverride(value, `member '${slot}'`);
|
|
@@ -60,7 +80,7 @@ export function readMembersFile(path) {
|
|
|
60
80
|
validateMemberOverride(value, `${path}: member '${slot}'`);
|
|
61
81
|
return members;
|
|
62
82
|
}
|
|
63
|
-
const MEMBER_KEYS = new Set(['agent_template', 'brain', 'role', 'approval', 'filesystem', 'unattended', 'cwd', 'model', 'effort', 'overrides']);
|
|
83
|
+
const MEMBER_KEYS = new Set(['agent_template', 'brain', 'role', 'approval', 'filesystem', 'unattended', 'cwd', 'model', 'effort', 'overrides', 'loops']);
|
|
64
84
|
function validateMemberOverride(value, where) {
|
|
65
85
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
66
86
|
throw new Error(`${where}: must be a mapping`);
|
|
@@ -81,6 +101,11 @@ function validateMemberOverride(value, where) {
|
|
|
81
101
|
throw new Error(`${where}: ${key} must be a non-blank string`);
|
|
82
102
|
if (item.overrides !== undefined && (!item.overrides || typeof item.overrides !== 'object' || Array.isArray(item.overrides)))
|
|
83
103
|
throw new Error(`${where}: overrides must be a mapping`);
|
|
104
|
+
if (item.overrides && Object.hasOwn(item.overrides, 'loops'))
|
|
105
|
+
throw new Error(`${where}: overrides.loops is unsupported; use the top-level loops member override`);
|
|
106
|
+
if (item.loops !== undefined && item.loops !== false
|
|
107
|
+
&& (!item.loops || typeof item.loops !== 'object' || Array.isArray(item.loops)))
|
|
108
|
+
throw new Error(`${where}: loops must be a mapping or false`);
|
|
84
109
|
}
|
|
85
110
|
const MAPS = new Set(['permissions', 'env', 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy']);
|
|
86
111
|
function merge(base, overlay) {
|
|
@@ -95,6 +120,8 @@ function merge(base, overlay) {
|
|
|
95
120
|
return out;
|
|
96
121
|
}
|
|
97
122
|
export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
123
|
+
for (const [slot, value] of Object.entries(overrides))
|
|
124
|
+
validateMemberOverride(value, `member '${slot}'`);
|
|
98
125
|
const slots = new Set(template.members.map(member => member.slot));
|
|
99
126
|
for (const slot of Object.keys(overrides))
|
|
100
127
|
if (!slots.has(slot))
|
|
@@ -108,6 +135,10 @@ export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
|
108
135
|
if (!base)
|
|
109
136
|
throw new Error(`Agent Template '${source}' not found`);
|
|
110
137
|
let definition = merge(base, (input.overrides ?? {}));
|
|
138
|
+
if (input.loops === false)
|
|
139
|
+
delete definition.loops;
|
|
140
|
+
else if (input.loops !== undefined)
|
|
141
|
+
definition.loops = structuredClone(input.loops);
|
|
111
142
|
const rolePreset = input.role ? cfg.rolePresets?.[input.role] : undefined;
|
|
112
143
|
if (input.role && !rolePreset)
|
|
113
144
|
throw new Error(`Role '${input.role}' not found`);
|
|
@@ -144,6 +175,8 @@ export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
|
144
175
|
const role = cfg.resolveAgentDefinition(`RoomMember_${member.slot}`, {
|
|
145
176
|
...definition, identity: `RoomMember_${member.slot}`,
|
|
146
177
|
});
|
|
178
|
+
if (definition.loops !== undefined)
|
|
179
|
+
definition.loops = canonicalAgentLoops(resolveAgentLoops(definition.loops, role, `(member '${member.slot}' resolved loops)`));
|
|
147
180
|
const analysis = analyzeRolePermissions(role);
|
|
148
181
|
if (analysis.conflicts?.length)
|
|
149
182
|
throw new Error(`member '${member.slot}' has native permission conflicts: ${analysis.conflicts.map(item => item.warning).join('; ')}`);
|
|
@@ -154,6 +187,7 @@ export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
|
154
187
|
launchDefinitions[id] = definition;
|
|
155
188
|
const hash = createHash('sha256').update(canonicalJson(definition)).digest('hex');
|
|
156
189
|
return { ...member, agent_template: source, launch_definition_id: id,
|
|
190
|
+
loop_source: input.loops !== undefined ? 'cli' : base.loops !== undefined ? 'agent-template' : 'omitted',
|
|
157
191
|
agent_template_hash: hash,
|
|
158
192
|
...(input.role ? { role_preset: { id: input.role, hash: createHash('sha256').update(canonicalJson(rolePreset)).digest('hex') } } : {}),
|
|
159
193
|
...(input.brain ? { brain_preset: { id: input.brain, hash: createHash('sha256').update(canonicalJson(brainPreset)).digest('hex') } } : {}),
|
|
@@ -100,6 +100,7 @@ function expandMembers(template, prefix) {
|
|
|
100
100
|
agentTemplate: slot.agent_template,
|
|
101
101
|
launchDefinitionId: slot.launch_definition_id ?? slot.agent_template,
|
|
102
102
|
agentTemplateHash: slot.agent_template_hash,
|
|
103
|
+
loopSource: slot.loop_source ?? 'omitted',
|
|
103
104
|
});
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -121,6 +122,7 @@ function settingsFor(member, cfg, sealed) {
|
|
|
121
122
|
template: member.agentTemplate,
|
|
122
123
|
templateHash: member.agentTemplateHash
|
|
123
124
|
?? createHash('sha256').update(canonicalJson(definition)).digest('hex'),
|
|
125
|
+
loopSource: member.loopSource,
|
|
124
126
|
};
|
|
125
127
|
}
|
|
126
128
|
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
@@ -286,6 +288,9 @@ async function launchMemberUnlocked(input) {
|
|
|
286
288
|
temp: true,
|
|
287
289
|
identity: member.name,
|
|
288
290
|
agentDefinition: settings.definition,
|
|
291
|
+
...(settings.loopSource === 'cli' && settings.definition.loops === undefined
|
|
292
|
+
? { noLoops: true } : {}),
|
|
293
|
+
loopSource: settings.loopSource,
|
|
289
294
|
surface: 'agent',
|
|
290
295
|
creationActionId: actionId,
|
|
291
296
|
roomMemberStartup: startup,
|
|
@@ -276,6 +276,8 @@ export interface TemplateDefinition {
|
|
|
276
276
|
members: TemplateMemberSlot[];
|
|
277
277
|
}
|
|
278
278
|
export interface TemplateSnapshotMember extends TemplateMemberSlot {
|
|
279
|
+
/** Source of the effective temporary-loop policy, independent of its sealed value. */
|
|
280
|
+
loop_source?: 'agent-template' | 'cli' | 'omitted';
|
|
279
281
|
/** Secret-safe public projection; launch material lives in the sealed snapshot. */
|
|
280
282
|
agent_projection?: Record<string, unknown>;
|
|
281
283
|
agent_template_hash?: string;
|
package/dist/runner.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { AgentSession, ExitRecord, TurnResult } from './session/types.js';
|
|
|
7
7
|
import type { AgentSessionAdapter, AgentSessionStartOptions } from './harness/agent-session.js';
|
|
8
8
|
import { type OwnerChannelHandle, type OwnerChannelOptions } from './owner-channel/channel.js';
|
|
9
9
|
import { type OwnerBinderLease } from './owner-channel/binder.js';
|
|
10
|
+
import { ScheduledLoopManager, type ScheduledLoopManagerHandle } from './loops/manager.js';
|
|
10
11
|
export interface RunnerDeps {
|
|
11
12
|
exec: Exec;
|
|
12
13
|
cpuDelegated(): boolean;
|
|
@@ -26,6 +27,8 @@ export interface RunnerDeps {
|
|
|
26
27
|
startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
|
|
27
28
|
/** Construct the authenticated role control route (injectable where sockets are unavailable). */
|
|
28
29
|
createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setFleetAuditor' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
|
|
30
|
+
/** Construct scheduled-loop execution (injectable for fail-closed startup tests). */
|
|
31
|
+
createLoopManager(...args: ConstructorParameters<typeof ScheduledLoopManager>): ScheduledLoopManagerHandle;
|
|
29
32
|
/** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
|
|
30
33
|
acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise<OwnerBinderLease>;
|
|
31
34
|
/** Ask the still-authenticated predecessor to emit the fixed recovery notice. */
|
package/dist/runner.js
CHANGED
|
@@ -53,6 +53,7 @@ const defaultDeps = () => ({
|
|
|
53
53
|
createOwnerChannel: opts => new OwnerChannel(opts),
|
|
54
54
|
startAgentSession: (adapter, options) => adapter.start(options),
|
|
55
55
|
createControlServer: (stateDir, session, log) => new RoleControlServer(stateDir, session, log),
|
|
56
|
+
createLoopManager: (...args) => new ScheduledLoopManager(...args),
|
|
56
57
|
acquireOwnerBinder: (stateDir, role, identity) => acquireOwnerBinderLease(stateDir, role, identity),
|
|
57
58
|
reportOwnerStartupFailure: async (stateDir) => {
|
|
58
59
|
const response = await controlRequest(stateDir, {
|
|
@@ -774,7 +775,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
774
775
|
if (generation === loopGeneration && (loopManager || !definitions.length))
|
|
775
776
|
return { changed: false, loops: definitions.length };
|
|
776
777
|
if (!loopManager && definitions.length) {
|
|
777
|
-
loopManager =
|
|
778
|
+
loopManager = deps.createLoopManager(name, definitions, dir, arbiter, {
|
|
778
779
|
now: deps.now,
|
|
779
780
|
setTimer: (callback, ms) => setTimeout(callback, ms),
|
|
780
781
|
clearTimer: timer => clearTimeout(timer),
|
|
@@ -790,10 +791,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
790
791
|
deps.log(`[${name}] scheduled loops reloaded (${definitions.length} definitions)`);
|
|
791
792
|
return { changed: true, loops: definitions.length };
|
|
792
793
|
};
|
|
793
|
-
|
|
794
|
+
// Temporary agents are immutable launch snapshots: never re-resolve mutable Fleet YAML.
|
|
795
|
+
if (!temp)
|
|
796
|
+
control.setConfigReloader(reloadLoopConfig);
|
|
794
797
|
if (role.loops?.length) {
|
|
795
798
|
try {
|
|
796
|
-
loopManager =
|
|
799
|
+
loopManager = deps.createLoopManager(name, role.loops, dir, arbiter, {
|
|
797
800
|
now: deps.now,
|
|
798
801
|
setTimer: (callback, ms) => setTimeout(callback, ms),
|
|
799
802
|
clearTimer: timer => clearTimeout(timer),
|
|
@@ -803,6 +806,17 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
803
806
|
loopManager.start();
|
|
804
807
|
}
|
|
805
808
|
catch (error) {
|
|
809
|
+
if (temp) {
|
|
810
|
+
monitor?.stop();
|
|
811
|
+
if (monitorLoop)
|
|
812
|
+
await monitorLoop;
|
|
813
|
+
await control.close();
|
|
814
|
+
ownerBinder?.release();
|
|
815
|
+
await agentSession.close();
|
|
816
|
+
unsubscribeRecovery?.();
|
|
817
|
+
throw new Error(`[${name}] configured temporary loop manager failed to start: `
|
|
818
|
+
+ `${error?.message ?? String(error)}`);
|
|
819
|
+
}
|
|
806
820
|
deps.log(`[${name}] scheduled loop manager unavailable: ${error?.name ?? 'Error'}`);
|
|
807
821
|
}
|
|
808
822
|
}
|
package/dist/spawn.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IsolationConfig } from './isolation/types.js';
|
|
2
|
+
import type { AgentLoopsConfig } from './loops/config.js';
|
|
2
3
|
import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type MonitorConfig, type UnattendedMode, type RoomMemberStartup, type AgentDefinition, type AgentSelection } from './config.js';
|
|
3
4
|
import { type OpsDeps } from './ops.js';
|
|
4
5
|
import { type CreationDeps, type CreationProvenance } from './creation.js';
|
|
@@ -40,6 +41,12 @@ export interface SpawnOpts {
|
|
|
40
41
|
* input in this release.
|
|
41
42
|
*/
|
|
42
43
|
isolationFile?: string;
|
|
44
|
+
/** Trusted YAML containing exactly a top-level `loops:` map. Temporary launches only. */
|
|
45
|
+
loopsFile?: string;
|
|
46
|
+
/** Explicitly override any selected Agent Template loops with none. */
|
|
47
|
+
noLoops?: boolean;
|
|
48
|
+
/** Internal source label retained after room-member plan resolution. */
|
|
49
|
+
loopSource?: 'agent-template' | 'cli' | 'omitted';
|
|
43
50
|
overseeInterval?: string;
|
|
44
51
|
configPath?: string;
|
|
45
52
|
dryRun?: boolean;
|
|
@@ -56,6 +63,8 @@ export declare function agentDefinitionFromSpawn(o: SpawnOpts): AgentDefinition;
|
|
|
56
63
|
* must fail before any artifact exists.
|
|
57
64
|
*/
|
|
58
65
|
export declare function readIsolationFile(path: string): IsolationConfig;
|
|
66
|
+
/** Read a private canonical temporary-loop override before any creation side effect. */
|
|
67
|
+
export declare function readLoopsFile(path: string): AgentLoopsConfig;
|
|
59
68
|
export declare function validateSpawnOpts(o: SpawnOpts): void;
|
|
60
69
|
/** The ours identity a spawn will bind: explicit, else the role name. */
|
|
61
70
|
export declare const effectiveIdentity: (o: SpawnOpts) => string;
|
package/dist/spawn.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn as spawnChild } from 'node:child_process';
|
|
2
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parse, stringify } from 'yaml';
|
|
5
5
|
import { agentDir, defaultConfigPath } from './paths.js';
|
|
@@ -22,9 +22,12 @@ export function agentDefinitionFromSpawn(o) {
|
|
|
22
22
|
if (o.agentDefinition) {
|
|
23
23
|
if (o.brain !== undefined || o.role !== undefined || o.cwd !== undefined
|
|
24
24
|
|| o.coordinator !== undefined || o.approval !== undefined || o.filesystem !== undefined
|
|
25
|
-
|| o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined
|
|
25
|
+
|| o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined
|
|
26
|
+
|| o.loopsFile !== undefined)
|
|
26
27
|
throw new Error('canonical agentDefinition conflicts with separate Agent fields');
|
|
27
28
|
const definition = structuredClone(o.agentDefinition);
|
|
29
|
+
if (o.noLoops === true && definition.loops !== undefined)
|
|
30
|
+
throw new Error('canonical agentDefinition loops conflict with explicit no-loops policy');
|
|
28
31
|
if (o.identity)
|
|
29
32
|
definition.identity = o.identity;
|
|
30
33
|
return definition;
|
|
@@ -46,12 +49,21 @@ export function agentDefinitionFromSpawn(o) {
|
|
|
46
49
|
...(permissions ? { permissions } : {}),
|
|
47
50
|
...(o.isolationFile ? { isolation: readIsolationFile(o.isolationFile) } : {}),
|
|
48
51
|
...(o.monitorConfig ? { monitor: structuredClone(o.monitorConfig) } : {}),
|
|
52
|
+
...(o.loopsFile ? { loops: readLoopsFile(o.loopsFile) } : {}),
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
function resolvedSpawn(o) {
|
|
52
56
|
const definition = agentDefinitionFromSpawn(o);
|
|
53
|
-
const cfg = loadConfig(o.configPath, {
|
|
54
|
-
|
|
57
|
+
const cfg = loadConfig(o.configPath, {
|
|
58
|
+
additionalAgent: { id: o.name, definition, temporary: o.temp === true },
|
|
59
|
+
});
|
|
60
|
+
const role = findRole(cfg, o.name);
|
|
61
|
+
if (o.temp)
|
|
62
|
+
role.temporaryLoopSource = o.loopSource
|
|
63
|
+
?? (role.temporaryLoops?.length ? 'agent-template' : 'omitted');
|
|
64
|
+
if (o.noLoops === true)
|
|
65
|
+
role.temporaryLoops = [];
|
|
66
|
+
return { definition, role };
|
|
55
67
|
}
|
|
56
68
|
/**
|
|
57
69
|
* Read and validate an `--isolation-file`. The file is the existing
|
|
@@ -78,7 +90,37 @@ export function readIsolationFile(path) {
|
|
|
78
90
|
throw new Error(`--isolation-file ${path}: ${problems.join('; ')}`);
|
|
79
91
|
return cfg;
|
|
80
92
|
}
|
|
93
|
+
/** Read a private canonical temporary-loop override before any creation side effect. */
|
|
94
|
+
export function readLoopsFile(path) {
|
|
95
|
+
const stat = lstatSync(path);
|
|
96
|
+
const uid = process.getuid?.();
|
|
97
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_000_000
|
|
98
|
+
|| (uid !== undefined && stat.uid !== uid) || (stat.mode & 0o777) !== 0o600)
|
|
99
|
+
throw new Error(`${path}: loops file must be an owner-only regular file no larger than 1 MB`);
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = parse(readFileSync(path, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error(`--loops-file ${path}: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)
|
|
108
|
+
|| !Object.hasOwn(raw, 'loops')
|
|
109
|
+
|| Object.keys(raw).some(key => key !== 'loops'))
|
|
110
|
+
throw new Error(`${path}: expected exactly a top-level loops: mapping`);
|
|
111
|
+
const loops = raw.loops;
|
|
112
|
+
if (!loops || typeof loops !== 'object' || Array.isArray(loops)
|
|
113
|
+
|| !Object.keys(loops).length)
|
|
114
|
+
throw new Error(`${path}: loops must be a non-empty mapping; use --no-loops to disable loops`);
|
|
115
|
+
return structuredClone(loops);
|
|
116
|
+
}
|
|
81
117
|
export function validateSpawnOpts(o) {
|
|
118
|
+
if (o.loopsFile && o.noLoops === true)
|
|
119
|
+
throw new Error('--loops-file and --no-loops are mutually exclusive');
|
|
120
|
+
if ((o.loopsFile || o.noLoops === true || o.agentDefinition?.loops !== undefined) && !o.temp)
|
|
121
|
+
throw new Error('temporary Agent loops require --temp');
|
|
122
|
+
if (o.loopsFile)
|
|
123
|
+
readLoopsFile(o.loopsFile);
|
|
82
124
|
if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
|
|
83
125
|
throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
|
|
84
126
|
if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
|
|
@@ -149,7 +191,7 @@ export function spawnDryRun(o) {
|
|
|
149
191
|
* `env`, `bio`, `persona` and `harness_options` are deliberately absent: the
|
|
150
192
|
* record exists to be read, and must not become a place credentials collect.
|
|
151
193
|
*/
|
|
152
|
-
function provenanceSettings(o, defaults) {
|
|
194
|
+
function provenanceSettings(o, defaults, resolvedLoops = []) {
|
|
153
195
|
const perms = (defaults.permissions ?? {});
|
|
154
196
|
const callerDefaults = new Set(o.inheritedFromCaller ?? []);
|
|
155
197
|
const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
|
|
@@ -169,6 +211,16 @@ function provenanceSettings(o, defaults) {
|
|
|
169
211
|
? { value: 'declared via --isolation-file', source: 'cli' }
|
|
170
212
|
: { value: defaults.isolation ? 'from fleet defaults' : undefined, source: defaults.isolation ? 'fleet-default' : 'built-in' },
|
|
171
213
|
monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
|
|
214
|
+
loops: {
|
|
215
|
+
value: o.noLoops ? { enabled: false, loops: [] }
|
|
216
|
+
: resolvedLoops.length ? resolvedLoops.map(loop => ({
|
|
217
|
+
name: loop.name, enabled: loop.enabled, intervalMs: loop.intervalMs,
|
|
218
|
+
initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
|
|
219
|
+
prompt: { bytes: loop.promptBytes, sha256: loop.promptHash },
|
|
220
|
+
})) : 'omitted (legacy no temporary loops)',
|
|
221
|
+
source: o.loopSource === 'agent-template' ? 'agent-template'
|
|
222
|
+
: o.loopsFile || o.noLoops || o.loopSource === 'cli' ? 'cli' : 'built-in',
|
|
223
|
+
},
|
|
172
224
|
};
|
|
173
225
|
}
|
|
174
226
|
/** Permanent spawn: persist one bare Agent document under the selected v2 root. */
|
|
@@ -232,7 +284,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
232
284
|
// launch still records how it was asked for.
|
|
233
285
|
const provenance = buildProvenance({
|
|
234
286
|
role: o.name, lifetime: 'permanent', fleetVersion: VERSION,
|
|
235
|
-
settings: provenanceSettings(o, cfg.defaults),
|
|
287
|
+
settings: provenanceSettings(o, cfg.defaults, prepared.role.temporaryLoops),
|
|
236
288
|
surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
|
|
237
289
|
});
|
|
238
290
|
mkdirSync(agentDir(o.name), { recursive: true });
|
|
@@ -293,14 +345,19 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
|
|
|
293
345
|
}
|
|
294
346
|
async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
|
|
295
347
|
const cfg = loadConfig(o.configPath);
|
|
348
|
+
const { temporaryLoops, ...launchRole } = preparedRole;
|
|
296
349
|
const role = {
|
|
297
|
-
...
|
|
350
|
+
...launchRole, sourceFile: '(temp)',
|
|
351
|
+
...(o.noLoops === true ? { loops: [] }
|
|
352
|
+
: temporaryLoops?.length ? { loops: temporaryLoops } : { loops: undefined }),
|
|
353
|
+
temporaryLoopSource: o.loopSource ?? (temporaryLoops?.length ? 'agent-template' : 'omitted'),
|
|
354
|
+
roomMemberStartup: o.roomMemberStartup,
|
|
298
355
|
};
|
|
299
356
|
onStage?.('writing_role');
|
|
300
357
|
const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
|
|
301
358
|
const provenance = buildProvenance({
|
|
302
359
|
role: o.name, lifetime: 'temporary', fleetVersion: VERSION,
|
|
303
|
-
settings: provenanceSettings(o, cfg.defaults),
|
|
360
|
+
settings: provenanceSettings(o, cfg.defaults, preparedRole.temporaryLoops),
|
|
304
361
|
surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
|
|
305
362
|
});
|
|
306
363
|
writeProvenance(dir, provenance);
|
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.22",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -2,3 +2,48 @@ role: { ref: LocalCoordinator }
|
|
|
2
2
|
brain: { ref: claude-default }
|
|
3
3
|
coordinator: FleetCoordinator
|
|
4
4
|
permissions: { approval: ask, filesystem: workspace, unattended: deny }
|
|
5
|
+
loops:
|
|
6
|
+
continuity:
|
|
7
|
+
interval: 15m
|
|
8
|
+
prompt: |
|
|
9
|
+
Perform one bounded, observation-only continuity pass for the assigned task room. Do not modify
|
|
10
|
+
task or project state; the only permitted writes are the room action and secret-free WORKLOG
|
|
11
|
+
checkpoint defined below. Anchor the preceding interval to scheduled_at minus 15 minutes
|
|
12
|
+
through scheduled_at. A late start or missed-loop metadata describes this loop's outage, not
|
|
13
|
+
evidence that development froze.
|
|
14
|
+
|
|
15
|
+
Re-read ROUTINES.md when present and the continuity checkpoint in WORKLOG.md. Use bounded
|
|
16
|
+
`list_history` reads filtered to the assigned room CID and direction as needed. Do not drain
|
|
17
|
+
unread mail or mark unrelated mail read. Ignore unrelated contacts and private messages.
|
|
18
|
+
Use only attributable room/task evidence. Exclude loop-authored `CONTINUITY *` messages from
|
|
19
|
+
development evidence and fingerprint inputs; retain their request/report IDs separately for
|
|
20
|
+
dedupe. Lifecycle state `active`, elapsed time, silence, task difficulty, and filesystem
|
|
21
|
+
dirtiness alone do not prove development is active, meaningful progress, or a stall. Meaningful
|
|
22
|
+
progress is a concrete reported change, test result, decision, handoff, blocker delta, active
|
|
23
|
+
step, or unexpired ETA.
|
|
24
|
+
|
|
25
|
+
After a mandatory final history recheck, choose exactly one mutually exclusive branch:
|
|
26
|
+
- Terminal: if attributable evidence says the task is terminal, do nothing.
|
|
27
|
+
- Recent update: if the room already has a meaningful update in the interval, do nothing.
|
|
28
|
+
- Ongoing: if attributable development evidence is ongoing but the room has no meaningful
|
|
29
|
+
update in the interval, send at most one concise
|
|
30
|
+
`CONTINUITY ACTIVE [start..end]: <observed facts>; next=<known step or ETA>` update.
|
|
31
|
+
- Unobservable/no evidence: never invent activity. If no qualifying outstanding continuity
|
|
32
|
+
request exists, send one direct `CONTINUITY CHECK` request for status, next step, and ETA.
|
|
33
|
+
If the prior request is less than 10 minutes old or its ETA is unexpired, do nothing. If it
|
|
34
|
+
is at least 10 minutes old, no later meaningful evidence exists, and the ETA is absent or
|
|
35
|
+
elapsed, send at most one clear `CONTINUITY STALLED` report.
|
|
36
|
+
- Explicit stop: if attributable evidence says work stopped or is blocked with no active next
|
|
37
|
+
step, send at most one clear `CONTINUITY STALLED` report.
|
|
38
|
+
|
|
39
|
+
Before any send, suppress a semantic duplicate found by the final history recheck or checkpoint;
|
|
40
|
+
room history wins if they disagree. Build the stable fingerprint from classification and
|
|
41
|
+
non-loop evidence IDs/timestamps plus requested/reported state, excluding loop-authored
|
|
42
|
+
continuity messages, prose, and time-of-check so unchanged classification and evidence state
|
|
43
|
+
stay silent. Store continuity request/report IDs separately. Later meaningful progress resets
|
|
44
|
+
a prior request or stall. Append a secret-free WORKLOG checkpoint with interval, classification,
|
|
45
|
+
evidence IDs/times, fingerprint, and any separate request/report ID.
|
|
46
|
+
|
|
47
|
+
Do not edit project files, run implementation or tests, interrupt peers, spawn or manage agents,
|
|
48
|
+
invoke task/room lifecycle actions, infer from unrelated messages, or exceed LocalCoordinator
|
|
49
|
+
authority. Complete silently when no room action is required.
|
|
@@ -3,6 +3,7 @@ description: "Locally coordinated team: LocalCoordinator sequences, Developer ex
|
|
|
3
3
|
room: { quiet_membership: false, anonymous: false }
|
|
4
4
|
contract: |
|
|
5
5
|
LocalCoordinator coordinates sequencing, handoffs, context, and blockers among existing members.
|
|
6
|
+
LocalCoordinator performs a bounded 15-minute continuity check without taking over implementation.
|
|
6
7
|
Developer owns implementation and verification evidence.
|
|
7
8
|
Critic independently reviews and withholds sign-off on material failures.
|
|
8
9
|
LocalCoordinator has no Fleet lifecycle or agent-management authority.
|