@ours.network/fleet 0.1.0
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/LICENSE +98 -0
- package/README.md +196 -0
- package/dist/briefing.d.ts +10 -0
- package/dist/briefing.js +73 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +209 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +79 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +61 -0
- package/dist/exec.d.ts +12 -0
- package/dist/exec.js +14 -0
- package/dist/harness/claude-code.d.ts +8 -0
- package/dist/harness/claude-code.js +116 -0
- package/dist/harness/registry.d.ts +4 -0
- package/dist/harness/registry.js +13 -0
- package/dist/harness/types.d.ts +58 -0
- package/dist/harness/types.js +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +11 -0
- package/dist/ops.d.ts +20 -0
- package/dist/ops.js +87 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +11 -0
- package/dist/runner.d.ts +21 -0
- package/dist/runner.js +97 -0
- package/dist/spawn.d.ts +19 -0
- package/dist/spawn.js +57 -0
- package/dist/supervisor/index.d.ts +7 -0
- package/dist/supervisor/index.js +16 -0
- package/dist/supervisor/launchd.d.ts +4 -0
- package/dist/supervisor/launchd.js +76 -0
- package/dist/supervisor/none.d.ts +8 -0
- package/dist/supervisor/none.js +24 -0
- package/dist/supervisor/systemd.d.ts +5 -0
- package/dist/supervisor/systemd.js +63 -0
- package/dist/supervisor/types.d.ts +17 -0
- package/dist/supervisor/types.js +1 -0
- package/dist/tmux.d.ts +14 -0
- package/dist/tmux.js +50 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +19 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { defaultConfigPath, fleetDDir } from './paths.js';
|
|
5
|
+
export class ConfigError extends Error {
|
|
6
|
+
}
|
|
7
|
+
const NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
8
|
+
const ROLE_KEYS = [
|
|
9
|
+
'harness', 'identity', 'cwd', 'coordinator', 'mission', 'persona', 'bio',
|
|
10
|
+
'briefing_file', 'max_tokens', 'autocompact_pct', 'env', 'oversee', 'harness_options',
|
|
11
|
+
];
|
|
12
|
+
function deepSub(v, vars) {
|
|
13
|
+
if (typeof v === 'string')
|
|
14
|
+
return v.replace(/\$\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
|
|
15
|
+
if (Array.isArray(v))
|
|
16
|
+
return v.map(x => deepSub(x, vars));
|
|
17
|
+
if (v && typeof v === 'object')
|
|
18
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deepSub(x, vars)]));
|
|
19
|
+
return v;
|
|
20
|
+
}
|
|
21
|
+
/** Load ~/fleet.yaml (or an explicit path) merged with ~/fleet.d/*.yaml drop-ins. */
|
|
22
|
+
export function loadConfig(configPath) {
|
|
23
|
+
const base = configPath ?? defaultConfigPath();
|
|
24
|
+
const files = [];
|
|
25
|
+
const docs = [];
|
|
26
|
+
if (existsSync(base)) {
|
|
27
|
+
docs.push({ file: base, doc: (parse(readFileSync(base, 'utf8')) ?? {}) });
|
|
28
|
+
files.push(base);
|
|
29
|
+
}
|
|
30
|
+
else if (configPath) {
|
|
31
|
+
throw new ConfigError(`config not found: ${base}`);
|
|
32
|
+
}
|
|
33
|
+
const dd = fleetDDir();
|
|
34
|
+
if (existsSync(dd)) {
|
|
35
|
+
for (const f of readdirSync(dd).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')).sort()) {
|
|
36
|
+
const p = join(dd, f);
|
|
37
|
+
const doc = (parse(readFileSync(p, 'utf8')) ?? {});
|
|
38
|
+
const extra = Object.keys(doc).filter(k => k !== 'roles');
|
|
39
|
+
if (extra.length)
|
|
40
|
+
throw new ConfigError(`${p}: fleet.d files may only define roles: (found: ${extra.join(', ')})`);
|
|
41
|
+
docs.push({ file: p, doc });
|
|
42
|
+
files.push(p);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const baseDoc = docs.length && docs[0].file === base ? docs[0].doc : {};
|
|
46
|
+
const vars = (baseDoc.vars ?? {});
|
|
47
|
+
const defaults = (baseDoc.defaults ?? {});
|
|
48
|
+
const seen = new Map();
|
|
49
|
+
const roles = [];
|
|
50
|
+
for (const { file, doc } of docs) {
|
|
51
|
+
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
52
|
+
if (!NAME_RE.test(name))
|
|
53
|
+
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
54
|
+
const prev = seen.get(name);
|
|
55
|
+
if (prev)
|
|
56
|
+
throw new ConfigError(`role '${name}' defined in both ${prev} and ${file}`);
|
|
57
|
+
seen.set(name, file);
|
|
58
|
+
const r = deepSub(raw ?? {}, vars);
|
|
59
|
+
const bad = Object.keys(r).filter(k => !ROLE_KEYS.includes(k));
|
|
60
|
+
if (bad.length)
|
|
61
|
+
throw new ConfigError(`${file}: role '${name}' has unknown key(s) ${bad.join(', ')}; allowed: ${ROLE_KEYS.join(', ')}`);
|
|
62
|
+
roles.push({
|
|
63
|
+
...r,
|
|
64
|
+
name,
|
|
65
|
+
sourceFile: file,
|
|
66
|
+
harness: r.harness ?? defaults.harness ?? 'claude-code',
|
|
67
|
+
identity: r.identity ?? name,
|
|
68
|
+
max_tokens: r.max_tokens ?? defaults.max_tokens,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { roles, vars, defaults, files };
|
|
73
|
+
}
|
|
74
|
+
export function findRole(cfg, name) {
|
|
75
|
+
const r = cfg.roles.find(r => r.name === name);
|
|
76
|
+
if (!r)
|
|
77
|
+
throw new ConfigError(`no such role '${name}' in ${cfg.files.join(', ') || 'config'}`);
|
|
78
|
+
return r;
|
|
79
|
+
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Exec } from './exec.js';
|
|
2
|
+
import type { PrereqReport } from './harness/types.js';
|
|
3
|
+
/** Host-level + per-harness prerequisite report with actionable messages. */
|
|
4
|
+
export declare function doctor(opts?: {
|
|
5
|
+
harness?: string;
|
|
6
|
+
configPath?: string;
|
|
7
|
+
}, exec?: Exec, platform?: NodeJS.Platform): Promise<PrereqReport>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { userInfo } from 'node:os';
|
|
2
|
+
import { realExec } from './exec.js';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { getAdapter } from './harness/registry.js';
|
|
5
|
+
/** Host-level + per-harness prerequisite report with actionable messages. */
|
|
6
|
+
export async function doctor(opts = {}, exec = realExec, platform = process.platform) {
|
|
7
|
+
const checks = [];
|
|
8
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
9
|
+
checks.push({
|
|
10
|
+
name: 'node', ok: major >= 20,
|
|
11
|
+
detail: major >= 20 ? `v${process.versions.node}` : `v${process.versions.node} — need >= 20`,
|
|
12
|
+
});
|
|
13
|
+
const tmux = await exec('tmux', ['-V']);
|
|
14
|
+
checks.push({
|
|
15
|
+
name: 'tmux', ok: tmux.code === 0,
|
|
16
|
+
detail: tmux.code === 0 ? tmux.stdout.trim() : 'not found — apt install tmux / brew install tmux',
|
|
17
|
+
});
|
|
18
|
+
const mcp = await exec('ours-mcp', ['--version']);
|
|
19
|
+
checks.push({
|
|
20
|
+
name: 'ours-mcp', ok: mcp.code === 0,
|
|
21
|
+
detail: mcp.code === 0 ? mcp.stdout.trim() : 'not found — npm i -g @ours.network/mcp',
|
|
22
|
+
});
|
|
23
|
+
if (mcp.code === 0) {
|
|
24
|
+
const st = await exec('ours-mcp', ['status']);
|
|
25
|
+
checks.push({
|
|
26
|
+
name: 'ours-mcp daemon', ok: st.code === 0,
|
|
27
|
+
detail: st.code === 0 ? 'running' : 'not running — start it with: ours-mcp start',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (platform === 'linux') {
|
|
31
|
+
const user = userInfo().username;
|
|
32
|
+
const linger = await exec('loginctl', ['show-user', user, '--property=Linger']);
|
|
33
|
+
const ok = linger.code === 0 && linger.stdout.includes('Linger=yes');
|
|
34
|
+
checks.push({
|
|
35
|
+
name: 'linger', ok,
|
|
36
|
+
detail: ok ? 'enabled (roles survive logout/reboot)'
|
|
37
|
+
: `not enabled — run: ours-fleet init (or: sudo loginctl enable-linger ${user})`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const harnesses = opts.harness
|
|
41
|
+
? [opts.harness]
|
|
42
|
+
: [...new Set(loadConfigSafe(opts.configPath).map(r => r.harness))];
|
|
43
|
+
for (const h of harnesses) {
|
|
44
|
+
try {
|
|
45
|
+
const rep = await getAdapter(h).checkPrereqs();
|
|
46
|
+
checks.push(...rep.checks.map(c => ({ ...c, name: `${h}: ${c.name}` })));
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
checks.push({ name: h, ok: false, detail: e.message });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { ok: checks.every(c => c.ok), checks };
|
|
53
|
+
}
|
|
54
|
+
function loadConfigSafe(configPath) {
|
|
55
|
+
try {
|
|
56
|
+
return loadConfig(configPath).roles;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/exec.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ExecResult {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
code: number;
|
|
5
|
+
}
|
|
6
|
+
export type Exec = (cmd: string, args: string[], opts?: {
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
}) => Promise<ExecResult>;
|
|
9
|
+
/** execFile wrapper that never rejects; missing binary → code 127. */
|
|
10
|
+
export declare const realExec: Exec;
|
|
11
|
+
/** POSIX single-quote shell escaping. */
|
|
12
|
+
export declare const shq: (s: string) => string;
|
package/dist/exec.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
/** execFile wrapper that never rejects; missing binary → code 127. */
|
|
3
|
+
export const realExec = (cmd, args, opts) => new Promise(resolve => {
|
|
4
|
+
execFile(cmd, args, { env: opts?.env ?? process.env, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
5
|
+
let code = 0;
|
|
6
|
+
if (err) {
|
|
7
|
+
const c = err.code;
|
|
8
|
+
code = typeof c === 'number' ? c : c === 'ENOENT' ? 127 : 1;
|
|
9
|
+
}
|
|
10
|
+
resolve({ stdout: String(stdout), stderr: String(stderr), code });
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
/** POSIX single-quote shell escaping. */
|
|
14
|
+
export const shq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Exec } from '../exec.js';
|
|
2
|
+
import type { ResolvedRole } from '../config.js';
|
|
3
|
+
import type { HarnessAdapter } from './types.js';
|
|
4
|
+
export declare function autocompactPct(role: ResolvedRole): number;
|
|
5
|
+
/** Pre-trust a dir in ~/.claude.json so the first launch never blocks on the trust dialog. */
|
|
6
|
+
export declare function pretrust(dir: string): void;
|
|
7
|
+
export declare function makeClaudeCodeAdapter(exec?: Exec): HarnessAdapter;
|
|
8
|
+
export declare const claudeCodeAdapter: HarnessAdapter;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { home } from '../paths.js';
|
|
4
|
+
import { realExec } from '../exec.js';
|
|
5
|
+
import { registerAdapter } from './registry.js';
|
|
6
|
+
const OPTION_KEYS = ['plugins', 'mem_palace', 'mem_palace_midsession_autosave'];
|
|
7
|
+
/** Context window of the fleet model (1M); max_tokens → % of this. */
|
|
8
|
+
const WINDOW = 1_000_000;
|
|
9
|
+
export function autocompactPct(role) {
|
|
10
|
+
let pct;
|
|
11
|
+
if (role.autocompact_pct != null)
|
|
12
|
+
pct = Math.round(role.autocompact_pct);
|
|
13
|
+
else if (role.max_tokens != null)
|
|
14
|
+
pct = Math.round((role.max_tokens / WINDOW) * 100);
|
|
15
|
+
else
|
|
16
|
+
return 50;
|
|
17
|
+
return Math.max(1, Math.min(100, pct));
|
|
18
|
+
}
|
|
19
|
+
/** Pre-trust a dir in ~/.claude.json so the first launch never blocks on the trust dialog. */
|
|
20
|
+
export function pretrust(dir) {
|
|
21
|
+
const p = join(home(), '.claude.json');
|
|
22
|
+
const d = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {};
|
|
23
|
+
const projects = (d.projects ??= {});
|
|
24
|
+
const e = (projects[dir] ??= {});
|
|
25
|
+
e.hasTrustDialogAccepted = true;
|
|
26
|
+
e.hasCompletedProjectOnboarding = true;
|
|
27
|
+
e.projectOnboardingSeenCount = Math.max(e.projectOnboardingSeenCount ?? 0, 1);
|
|
28
|
+
writeFileSync(p, JSON.stringify(d, null, 2));
|
|
29
|
+
}
|
|
30
|
+
export function makeClaudeCodeAdapter(exec = realExec) {
|
|
31
|
+
return {
|
|
32
|
+
id: 'claude-code',
|
|
33
|
+
supportsResume: true,
|
|
34
|
+
async checkPrereqs() {
|
|
35
|
+
const r = await exec('claude', ['--version']);
|
|
36
|
+
const ok = r.code === 0;
|
|
37
|
+
return {
|
|
38
|
+
ok,
|
|
39
|
+
checks: [{
|
|
40
|
+
name: 'claude',
|
|
41
|
+
ok,
|
|
42
|
+
detail: ok ? r.stdout.trim() : 'claude CLI not found on PATH — install Claude Code and log in',
|
|
43
|
+
}],
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
validateOptions(opts) {
|
|
47
|
+
if (opts == null)
|
|
48
|
+
return [];
|
|
49
|
+
if (typeof opts !== 'object' || Array.isArray(opts))
|
|
50
|
+
return [{ path: 'harness_options', message: 'must be a map' }];
|
|
51
|
+
return Object.keys(opts)
|
|
52
|
+
.filter(k => !OPTION_KEYS.includes(k))
|
|
53
|
+
.map(k => ({ path: `harness_options.${k}`, message: `unknown option; allowed: ${OPTION_KEYS.join(', ')}` }));
|
|
54
|
+
},
|
|
55
|
+
async prepareSession(role, dirs) {
|
|
56
|
+
pretrust(dirs.stateDir);
|
|
57
|
+
if (dirs.runCwd && dirs.runCwd !== dirs.stateDir)
|
|
58
|
+
pretrust(dirs.runCwd);
|
|
59
|
+
const o = (role.harness_options ?? {});
|
|
60
|
+
const memPalace = o.mem_palace !== false;
|
|
61
|
+
const enabledPlugins = { ...(o.plugins ?? {}) };
|
|
62
|
+
if (!memPalace)
|
|
63
|
+
enabledPlugins['mempalace@mempalace'] = false;
|
|
64
|
+
const env = {
|
|
65
|
+
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: String(autocompactPct(role)),
|
|
66
|
+
MEMPALACE_HOOKS_AUTO_SAVE: 'false',
|
|
67
|
+
MEMPALACE_MIDSESSION_AUTOSAVE: o.mem_palace_midsession_autosave ? 'true' : 'false',
|
|
68
|
+
};
|
|
69
|
+
if (!memPalace)
|
|
70
|
+
env.MEMPALACE_DISABLED = 'true';
|
|
71
|
+
const argv = [];
|
|
72
|
+
if (Object.keys(enabledPlugins).length) {
|
|
73
|
+
const overlay = join(dirs.stateDir, '.settings-overlay.json');
|
|
74
|
+
writeFileSync(overlay, JSON.stringify({ enabledPlugins }, null, 2));
|
|
75
|
+
argv.push('--settings', overlay);
|
|
76
|
+
}
|
|
77
|
+
return { argv, env };
|
|
78
|
+
},
|
|
79
|
+
buildLaunch(role, mode, s, prep) {
|
|
80
|
+
const stateDir = roleStateDir(role);
|
|
81
|
+
const base = ['claude', ...prep.argv, '--remote-control', role.name];
|
|
82
|
+
const argv = mode === 'fresh'
|
|
83
|
+
? [...base, '--session-id', s.sessionId, `Read and follow ${join(stateDir, 'briefing.md')} now.`]
|
|
84
|
+
: [...base, '--resume', s.sessionId,
|
|
85
|
+
this.vocabulary.restartPrompt(role.identity, join(stateDir, 'WORKLOG.md'))];
|
|
86
|
+
return { argv, env: prep.env };
|
|
87
|
+
},
|
|
88
|
+
vocabulary: {
|
|
89
|
+
bindTool: 'choose_identity',
|
|
90
|
+
createTool: 'create_identity',
|
|
91
|
+
setBioTool: 'set_bio',
|
|
92
|
+
setPersonaTool: 'set_persona',
|
|
93
|
+
currentIdentityTool: 'current_identity',
|
|
94
|
+
sendTool: 'send_message',
|
|
95
|
+
getMessagesTool: 'get_messages',
|
|
96
|
+
watchCommand: id => `ours-mcp watch "${id}"`,
|
|
97
|
+
monitorInstruction: id => `Arm a **persistent Monitor** running the shell command \`ours-mcp watch "${id}"\` so inbound ours mail wakes you.`,
|
|
98
|
+
launchNote: name => `You were launched with \`--remote-control ${name}\`. Confirm you are running.`,
|
|
99
|
+
restartPrompt: (id, worklog) => `Session restarted. Re-bind your ours identity now (choose_identity name "${id}" force=true), ` +
|
|
100
|
+
`re-arm your monitor (ours-mcp watch "${id}"), then continue from ${worklog}. ` +
|
|
101
|
+
'Do not re-run whatever crashed you.',
|
|
102
|
+
},
|
|
103
|
+
exitPolicy: { cleanExitIsFresh: true, fastFailSecs: 20 },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// The adapter needs the state dir for briefing/worklog paths in launch prompts.
|
|
107
|
+
// Roles' state dirs are canonical: agentDir(name) — temp roles carry their dir in cwd handling
|
|
108
|
+
// by the runner, which passes dirs to prepareSession; buildLaunch derives from the same rule.
|
|
109
|
+
import { agentDir } from '../paths.js';
|
|
110
|
+
function roleStateDir(role) {
|
|
111
|
+
// Temp roles are marked by the runner via a private field to keep the interface small.
|
|
112
|
+
const temp = role.__temp === true;
|
|
113
|
+
return agentDir(role.name, temp);
|
|
114
|
+
}
|
|
115
|
+
export const claudeCodeAdapter = makeClaudeCodeAdapter();
|
|
116
|
+
registerAdapter(claudeCodeAdapter);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const adapters = new Map();
|
|
2
|
+
export function registerAdapter(a) {
|
|
3
|
+
adapters.set(a.id, a);
|
|
4
|
+
}
|
|
5
|
+
export function getAdapter(id) {
|
|
6
|
+
const a = adapters.get(id);
|
|
7
|
+
if (!a)
|
|
8
|
+
throw new Error(`unknown harness '${id}'; registered: ${[...adapters.keys()].join(', ') || '(none)'}`);
|
|
9
|
+
return a;
|
|
10
|
+
}
|
|
11
|
+
export function knownAdapters() {
|
|
12
|
+
return [...adapters.keys()];
|
|
13
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ResolvedRole } from '../config.js';
|
|
2
|
+
export interface PrereqCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
ok: boolean;
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PrereqReport {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
checks: PrereqCheck[];
|
|
10
|
+
}
|
|
11
|
+
export interface RoleDirs {
|
|
12
|
+
stateDir: string;
|
|
13
|
+
runCwd: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SessionState {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
}
|
|
18
|
+
/** Extra argv/env contributed by prepareSession (overlays, trust, limits). */
|
|
19
|
+
export interface SessionPrep {
|
|
20
|
+
argv: string[];
|
|
21
|
+
env: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
export interface Launch {
|
|
24
|
+
argv: string[];
|
|
25
|
+
env: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
/** Harness-correct wording/tool names used to generate briefing.md. */
|
|
28
|
+
export interface BriefingVocab {
|
|
29
|
+
bindTool: string;
|
|
30
|
+
createTool: string;
|
|
31
|
+
setBioTool: string;
|
|
32
|
+
setPersonaTool: string;
|
|
33
|
+
currentIdentityTool: string;
|
|
34
|
+
sendTool: string;
|
|
35
|
+
getMessagesTool: string;
|
|
36
|
+
watchCommand(identity: string): string;
|
|
37
|
+
monitorInstruction(identity: string): string;
|
|
38
|
+
launchNote(name: string): string;
|
|
39
|
+
restartPrompt(identity: string, worklogPath: string): string;
|
|
40
|
+
}
|
|
41
|
+
export interface ExitPolicy {
|
|
42
|
+
cleanExitIsFresh: boolean;
|
|
43
|
+
fastFailSecs: number;
|
|
44
|
+
}
|
|
45
|
+
export interface ValidationError {
|
|
46
|
+
path: string;
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
export interface HarnessAdapter {
|
|
50
|
+
id: string;
|
|
51
|
+
supportsResume: boolean;
|
|
52
|
+
checkPrereqs(): Promise<PrereqReport>;
|
|
53
|
+
validateOptions(opts: unknown): ValidationError[];
|
|
54
|
+
prepareSession(role: ResolvedRole, dirs: RoleDirs): Promise<SessionPrep>;
|
|
55
|
+
buildLaunch(role: ResolvedRole, mode: 'fresh' | 'resume', s: SessionState, prep: SessionPrep): Launch;
|
|
56
|
+
vocabulary: BriefingVocab;
|
|
57
|
+
exitPolicy: ExitPolicy;
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { loadConfig, findRole, ConfigError } from './config.js';
|
|
2
|
+
export type { FleetConfig, ResolvedRole, RoleConfig, OverseeEntry } from './config.js';
|
|
3
|
+
export type { HarnessAdapter, BriefingVocab, ExitPolicy, PrereqReport, PrereqCheck, SessionPrep, SessionState, Launch, RoleDirs, ValidationError, } from './harness/types.js';
|
|
4
|
+
export { registerAdapter, getAdapter, knownAdapters } from './harness/registry.js';
|
|
5
|
+
export { claudeCodeAdapter, makeClaudeCodeAdapter } from './harness/claude-code.js';
|
|
6
|
+
export { generateBriefing } from './briefing.js';
|
|
7
|
+
export { pickBackend } from './supervisor/index.js';
|
|
8
|
+
export type { SupervisorBackend } from './supervisor/types.js';
|
|
9
|
+
export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
|
|
10
|
+
export { spawnPermanent, spawnTemp } from './spawn.js';
|
|
11
|
+
export { doctor } from './doctor.js';
|
|
12
|
+
export { runOnce, runTemp } from './runner.js';
|
|
13
|
+
export { Tmux } from './tmux.js';
|
|
14
|
+
export { VERSION } from './version.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { loadConfig, findRole, ConfigError } from './config.js';
|
|
2
|
+
export { registerAdapter, getAdapter, knownAdapters } from './harness/registry.js';
|
|
3
|
+
export { claudeCodeAdapter, makeClaudeCodeAdapter } from './harness/claude-code.js';
|
|
4
|
+
export { generateBriefing } from './briefing.js';
|
|
5
|
+
export { pickBackend } from './supervisor/index.js';
|
|
6
|
+
export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
|
|
7
|
+
export { spawnPermanent, spawnTemp } from './spawn.js';
|
|
8
|
+
export { doctor } from './doctor.js';
|
|
9
|
+
export { runOnce, runTemp } from './runner.js';
|
|
10
|
+
export { Tmux } from './tmux.js';
|
|
11
|
+
export { VERSION } from './version.js';
|
package/dist/ops.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FleetConfig, ResolvedRole } from './config.js';
|
|
2
|
+
import type { SupervisorBackend } from './supervisor/types.js';
|
|
3
|
+
export interface OpsDeps {
|
|
4
|
+
backend: SupervisorBackend;
|
|
5
|
+
binPath: string;
|
|
6
|
+
sleep(ms: number): Promise<void>;
|
|
7
|
+
log(line: string): void;
|
|
8
|
+
}
|
|
9
|
+
/** Materialize a role's state dir from config: briefing + markers. Returns the dir. */
|
|
10
|
+
export declare function applyRole(role: ResolvedRole, opts?: {
|
|
11
|
+
fresh?: boolean;
|
|
12
|
+
temp?: boolean;
|
|
13
|
+
}): string;
|
|
14
|
+
/** Create/start roles declaratively. Idempotent; active roles keep their context. */
|
|
15
|
+
export declare function up(cfg: FleetConfig, names: string[], deps: OpsDeps): Promise<void>;
|
|
16
|
+
export declare function down(cfg: FleetConfig, names: string[], deps: OpsDeps): Promise<void>;
|
|
17
|
+
/** Re-sync from config + bounce. mode 'keep' resumes context; 'fresh' wipes it. */
|
|
18
|
+
export declare function restartRoles(cfg: FleetConfig, names: string[], deps: OpsDeps, mode: 'keep' | 'fresh'): Promise<void>;
|
|
19
|
+
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
20
|
+
export declare function rmRole(cfg: FleetConfig, name: string, deps: OpsDeps): Promise<void>;
|
package/dist/ops.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { agentDir, fleetDDir } from './paths.js';
|
|
5
|
+
import { findRole } from './config.js';
|
|
6
|
+
import { getAdapter } from './harness/registry.js';
|
|
7
|
+
import { generateBriefing } from './briefing.js';
|
|
8
|
+
const STAGGER_MS = () => 1000 * Number(process.env.FLEET_START_STAGGER ?? 5);
|
|
9
|
+
/** Materialize a role's state dir from config: briefing + markers. Returns the dir. */
|
|
10
|
+
export function applyRole(role, opts = {}) {
|
|
11
|
+
const adapter = getAdapter(role.harness);
|
|
12
|
+
const errs = adapter.validateOptions(role.harness_options);
|
|
13
|
+
if (errs.length)
|
|
14
|
+
throw new Error(`role '${role.name}': ` + errs.map(e => `${e.path}: ${e.message}`).join('; '));
|
|
15
|
+
const dir = agentDir(role.name, opts.temp === true);
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
writeFileSync(join(dir, '.identity'), role.identity + '\n');
|
|
18
|
+
if (role.cwd)
|
|
19
|
+
writeFileSync(join(dir, '.cwd'), role.cwd + '\n');
|
|
20
|
+
if (!existsSync(join(dir, '.session-id')))
|
|
21
|
+
writeFileSync(join(dir, '.session-id'), randomUUID() + '\n');
|
|
22
|
+
if (!existsSync(join(dir, 'WORKLOG.md')))
|
|
23
|
+
writeFileSync(join(dir, 'WORKLOG.md'), '');
|
|
24
|
+
const briefingBody = role.briefing_file ? readFileSync(role.briefing_file, 'utf8') : undefined;
|
|
25
|
+
writeFileSync(join(dir, 'briefing.md'), generateBriefing(role, adapter.vocabulary, {
|
|
26
|
+
stateDir: dir, worklogPath: join(dir, 'WORKLOG.md'), briefingBody,
|
|
27
|
+
}));
|
|
28
|
+
if (opts.fresh)
|
|
29
|
+
for (const f of ['.booted', '.session-id', '.exit-status'])
|
|
30
|
+
rmSync(join(dir, f), { force: true });
|
|
31
|
+
return dir;
|
|
32
|
+
}
|
|
33
|
+
function selectRoles(cfg, names) {
|
|
34
|
+
return names.length ? names.map(n => findRole(cfg, n)) : cfg.roles;
|
|
35
|
+
}
|
|
36
|
+
/** Create/start roles declaratively. Idempotent; active roles keep their context. */
|
|
37
|
+
export async function up(cfg, names, deps) {
|
|
38
|
+
let first = true;
|
|
39
|
+
for (const role of selectRoles(cfg, names)) {
|
|
40
|
+
if (!first)
|
|
41
|
+
await deps.sleep(STAGGER_MS());
|
|
42
|
+
first = false;
|
|
43
|
+
const dir = applyRole(role);
|
|
44
|
+
// If the role isn't running, boot fresh so it reads the briefing we just wrote.
|
|
45
|
+
const status = await deps.backend.status(role.name).catch(() => '');
|
|
46
|
+
if (!/running|active \(/.test(status))
|
|
47
|
+
rmSync(join(dir, '.booted'), { force: true });
|
|
48
|
+
await deps.backend.install(role.name, deps.binPath);
|
|
49
|
+
deps.log(`↑ up: ${role.name} (harness: ${role.harness}, identity: ${role.identity}${role.cwd ? `, cwd: ${role.cwd}` : ''})`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export async function down(cfg, names, deps) {
|
|
53
|
+
for (const role of selectRoles(cfg, names)) {
|
|
54
|
+
try {
|
|
55
|
+
await deps.backend.stop(role.name);
|
|
56
|
+
deps.log(`■ stopped ${role.name}`);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
deps.log(` (could not stop ${role.name} — maybe not running)`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Re-sync from config + bounce. mode 'keep' resumes context; 'fresh' wipes it. */
|
|
64
|
+
export async function restartRoles(cfg, names, deps, mode) {
|
|
65
|
+
let first = true;
|
|
66
|
+
for (const role of selectRoles(cfg, names)) {
|
|
67
|
+
if (!first)
|
|
68
|
+
await deps.sleep(STAGGER_MS());
|
|
69
|
+
first = false;
|
|
70
|
+
applyRole(role, { fresh: mode === 'fresh' });
|
|
71
|
+
await deps.backend.restart(role.name);
|
|
72
|
+
deps.log(mode === 'fresh'
|
|
73
|
+
? `↻ ${role.name} — force-restarted (FRESH — context cleared, briefing reloaded)`
|
|
74
|
+
: `↻ ${role.name} — restarted (resumes; briefing re-synced)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
78
|
+
export async function rmRole(cfg, name, deps) {
|
|
79
|
+
const role = findRole(cfg, name);
|
|
80
|
+
await deps.backend.uninstall(name);
|
|
81
|
+
rmSync(agentDir(name), { recursive: true, force: true });
|
|
82
|
+
if (role.sourceFile.startsWith(fleetDDir() + '/')) {
|
|
83
|
+
unlinkSync(role.sourceFile);
|
|
84
|
+
deps.log(`removed ${role.sourceFile}`);
|
|
85
|
+
}
|
|
86
|
+
deps.log(`removed '${name}' (its ours identity is left intact)`);
|
|
87
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Home root for config + state. OURS_FLEET_HOME overrides (tests, exotic setups). */
|
|
2
|
+
export declare const home: () => string;
|
|
3
|
+
export declare const stateRoot: () => string;
|
|
4
|
+
export declare const agentsRoot: () => string;
|
|
5
|
+
export declare const tmpRoot: () => string;
|
|
6
|
+
export declare const logsRoot: () => string;
|
|
7
|
+
export declare const agentDir: (name: string, temp?: boolean) => string;
|
|
8
|
+
export declare const defaultConfigPath: () => string;
|
|
9
|
+
export declare const fleetDDir: () => string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Home root for config + state. OURS_FLEET_HOME overrides (tests, exotic setups). */
|
|
4
|
+
export const home = () => process.env.OURS_FLEET_HOME ?? homedir();
|
|
5
|
+
export const stateRoot = () => join(home(), '.ours-fleet');
|
|
6
|
+
export const agentsRoot = () => join(stateRoot(), 'agents');
|
|
7
|
+
export const tmpRoot = () => join(stateRoot(), 'tmp');
|
|
8
|
+
export const logsRoot = () => join(stateRoot(), 'logs');
|
|
9
|
+
export const agentDir = (name, temp = false) => join(temp ? tmpRoot() : agentsRoot(), name);
|
|
10
|
+
export const defaultConfigPath = () => join(home(), 'fleet.yaml');
|
|
11
|
+
export const fleetDDir = () => join(home(), 'fleet.d');
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ResolvedRole } from './config.js';
|
|
2
|
+
import type { Launch } from './harness/types.js';
|
|
3
|
+
import { Tmux } from './tmux.js';
|
|
4
|
+
export interface RunnerDeps {
|
|
5
|
+
tmux: Tmux;
|
|
6
|
+
isAlive(pid: number): boolean;
|
|
7
|
+
sleep(ms: number): Promise<void>;
|
|
8
|
+
now(): number;
|
|
9
|
+
log(line: string): void;
|
|
10
|
+
}
|
|
11
|
+
/** Compose the tmux pane shell command: env prefix + argv + exit-status capture. */
|
|
12
|
+
export declare function buildPaneCommand(launch: Launch, roleEnv: Record<string, string> | undefined, exitStatusPath: string): string;
|
|
13
|
+
/** Read a temp role's config snapshot written by spawnTemp. */
|
|
14
|
+
export declare function loadTempRole(name: string): ResolvedRole;
|
|
15
|
+
/** One supervised session lifecycle. The supervisor re-invokes us after we return. */
|
|
16
|
+
export declare function runOnce(name: string, opts?: {
|
|
17
|
+
temp?: boolean;
|
|
18
|
+
configPath?: string;
|
|
19
|
+
}, partialDeps?: Partial<RunnerDeps>): Promise<void>;
|
|
20
|
+
/** Temp-agent entrypoint: run one session, then remove the temp dir. */
|
|
21
|
+
export declare function runTemp(name: string, deps?: Partial<RunnerDeps>): Promise<void>;
|