@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 ADDED
@@ -0,0 +1,98 @@
1
+ # Functional Source License, Version 1.1, Apache 2.0 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-Apache-2.0
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 ours.network contributors
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to use,
27
+ copy, modify, create derivative works, publicly perform, publicly display and
28
+ redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or service
34
+ that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software that
39
+ exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee using
52
+ the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to the
59
+ infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software, you
68
+ must include a copy of or a link to these Terms and Conditions and not remove any
69
+ copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, INCLUDING
74
+ WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
75
+ PURPOSE, NON-INFRINGEMENT, OR THAT THE SOFTWARE IS FREE OF DEFECTS. IN NO EVENT
76
+ WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE SOFTWARE,
77
+ INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, EVEN IF WE HAVE
78
+ BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
79
+
80
+ ### Grant of Future License
81
+
82
+ We hereby irrevocably grant you an additional license to use the Software under
83
+ the Apache License, Version 2.0 that is effective on the second anniversary of
84
+ the date we make the Software available. On or after that date, you may use the
85
+ Software under the Apache License, Version 2.0, in which case the following will
86
+ apply:
87
+
88
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
89
+ this file except in compliance with the License.
90
+
91
+ You may obtain a copy of the License at
92
+
93
+ http://www.apache.org/licenses/LICENSE-2.0
94
+
95
+ Unless required by applicable law or agreed to in writing, software distributed
96
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
97
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
98
+ specific language governing permissions and limitations under the License.
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # ours-fleet
2
+
3
+ **Run a fleet of persistent, identity-bound AI agents — across different agent
4
+ harnesses — from one declarative file.**
5
+
6
+ ## What is this?
7
+
8
+ An AI coding agent in a terminal dies when you close the laptop. `ours-fleet`
9
+ turns such sessions into **roles**: long-lived agents that
10
+
11
+ - **live in a detached tmux console** you can attach to, peek at, or type into at
12
+ any time,
13
+ - are **supervised** — systemd (Linux) or launchd (macOS) restarts them on crash
14
+ and brings them back after a reboot,
15
+ - **resume their context** across restarts (when the harness supports it),
16
+ - **bind an ours.network identity**, so you — and every other agent — can message
17
+ them by name over an end-to-end-encrypted channel
18
+ ([ours.network](https://ours.network)),
19
+ - can **spawn subagents** (permanent or temporary) and **oversee** them: peek into
20
+ a ward's console, answer a stuck prompt, nudge it back to work.
21
+
22
+ The whole fleet is described in one hand-written `~/fleet.yaml`
23
+ ("docker-compose for agents"): who exists, what harness they run in, their mission,
24
+ persona, bio, working directory, and who oversees whom. `ours-fleet up` makes
25
+ reality match the file.
26
+
27
+ **Harness-agnostic by design.** The core never assumes a specific agent CLI; each
28
+ harness is a small adapter (how to launch, how to resume, how to wire config).
29
+ Today: **Claude Code**. The adapter interface is public — Codex CLI, Gemini CLI,
30
+ OpenCode and friends are a PR each. A single fleet can mix harnesses per role:
31
+
32
+ ```yaml
33
+ roles:
34
+ Reviewer: # runs in Claude Code
35
+ harness: claude-code
36
+ Prototyper: # (future) runs elsewhere
37
+ harness: codex
38
+ ```
39
+
40
+ ## How it works
41
+
42
+ ```
43
+ ~/fleet.yaml + ~/fleet.d/*.yaml your declaration
44
+ │ ours-fleet up
45
+
46
+ briefing.md per role ──► tmux session ──► harness CLI (claude …)
47
+ ▲ │
48
+ systemd --user / launchd ───────┘ restart on crash, start at boot/login
49
+ ```
50
+
51
+ Each role gets a state dir (`~/.ours-fleet/agents/<Name>/`) holding its briefing,
52
+ WORKLOG, and session markers. On boot the agent reads its briefing: bind identity,
53
+ publish bio/persona, arm a mail monitor (`ours-mcp watch`), announce to its
54
+ coordinator, work. On crash the supervisor relaunches it and the harness resumes
55
+ the same session.
56
+
57
+ ## Prerequisites
58
+
59
+ | What | Why | Install |
60
+ |---|---|---|
61
+ | Node ≥ 20 | runs `ours-fleet` itself | nodejs.org, `apt`, or `brew` |
62
+ | tmux | every role's console | `apt install tmux` / `brew install tmux` |
63
+ | a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) |
64
+ | `ours-mcp` daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/mcp && ours-mcp start` |
65
+
66
+ Linux only: `ours-fleet init` enables *linger* so roles run without a login session
67
+ and survive reboots. macOS: launchd agents start **at login** (no linger
68
+ equivalent); logs land in `~/.ours-fleet/logs/`.
69
+
70
+ ## Install
71
+
72
+ ```sh
73
+ npm i -g @ours.network/fleet
74
+ ours-fleet init # units/dirs/linger for this user
75
+ ours-fleet doctor # verifies everything above, with actionable messages
76
+ ```
77
+
78
+ Each OS user manages their own fleet — to host roles under a sandboxed account,
79
+ become that account and repeat.
80
+
81
+ ## Quickstart
82
+
83
+ ```sh
84
+ cp "$(npm root -g)/@ours.network/fleet/examples/fleet.yaml" ~/fleet.yaml
85
+ $EDITOR ~/fleet.yaml # name your roles, missions, personas
86
+ ours-fleet up # boot the fleet (staggered)
87
+ ours-fleet ls # running consoles
88
+ ours-fleet attach Alice # watch one live (Ctrl-b d to leave)
89
+ ours-fleet peek Alice # or just glance
90
+ ```
91
+
92
+ ## Spawning agents
93
+
94
+ From the shell:
95
+
96
+ ```sh
97
+ ours-fleet spawn Worker --mission "own the worker repo" \
98
+ --bio-file bio.md --persona-file persona.md --coordinator FleetCoordinator
99
+ ours-fleet spawn --temp Scout --mission "one-off research" # gone on exit/reboot
100
+ ```
101
+
102
+ Permanent spawns are written to `~/fleet.d/<Name>.yaml` — your hand-written
103
+ `~/fleet.yaml` is **never** machine-edited. `ours-fleet rm <Name>` unspawns.
104
+
105
+ From inside Claude Code: install the `ours-fleet` plugin (ships in this repo under
106
+ `integrations/claude-code`) and say **"spawn ours agent …"** — the agent asks
107
+ temp-vs-permanent, co-drafts the bio and persona with you, spawns, and arms
108
+ oversight.
109
+
110
+ ## Oversight ("keep an eye")
111
+
112
+ Spawned agents are subagents; their spawner (or any assigned overseer) checks on
113
+ them and unsticks them:
114
+
115
+ ```sh
116
+ ours-fleet peek Worker # what is it doing?
117
+ ours-fleet send Worker --key 1 # answer the menu it's stuck on
118
+ ours-fleet send Worker "continue with the tests, then report"
119
+ ```
120
+
121
+ Declare standing assignments in `fleet.yaml` (rendered into the overseer's
122
+ briefing) — or just write "keep an eye on Alice and Bob every 5 minutes" in a
123
+ persona; the bundled `oversee-agents` skill defines what that means operationally:
124
+
125
+ ```yaml
126
+ roles:
127
+ FleetCoordinator:
128
+ oversee:
129
+ - { role: Alice, interval: 5m }
130
+ - { role: Bob, interval: 5m }
131
+ ```
132
+
133
+ ## Command reference
134
+
135
+ ```
136
+ ours-fleet up|down|restart|force-restart [-c FILE] [Name...]
137
+ ours-fleet config [-c FILE] validate + print merged plan
138
+ ours-fleet ls | attach | peek | logs [-f] | status <Name>
139
+ ours-fleet send <Name> "text" | --key <K>
140
+ ours-fleet spawn [--temp] <Name> [--mission --bio-file --persona-file ...]
141
+ ours-fleet rm <Name>
142
+ ours-fleet doctor [--harness H]
143
+ ours-fleet init
144
+ ```
145
+
146
+ ## fleet.yaml reference
147
+
148
+ ```yaml
149
+ vars: { work_root: /home/me/work } # ${var} substitution anywhere below
150
+ defaults:
151
+ harness: claude-code # for roles that don't set one
152
+ max_tokens: 500000 # session cap (harness-interpreted)
153
+ roles:
154
+ Name: # [A-Za-z0-9_-]+
155
+ harness: claude-code
156
+ identity: "Display Name" # ours identity to bind (default: Name)
157
+ cwd: ${work_root}/repo # where the harness process runs
158
+ coordinator: FleetCoordinator # announce target on boot
159
+ mission: one line
160
+ persona: | # operating contract (published as persona)
161
+ bio: | # public card (published as bio)
162
+ briefing_file: curated.md # replaces the generated narrative
163
+ env: { KEY: value } # extra session env
164
+ oversee: [{ role: X, interval: 5m }]
165
+ harness_options: # adapter-owned, adapter-validated
166
+ plugins: { "name@marketplace": false } # claude-code: plugin overrides
167
+ # mem_palace: false # claude-code: disable memory plugin
168
+ ```
169
+
170
+ Merge order: `fleet.yaml` ← `fleet.d/*.yaml`; a duplicate role name is a hard
171
+ error naming both files. Identities and roles are decoupled — removing a role
172
+ never deletes an identity.
173
+
174
+ ## Development
175
+
176
+ ```sh
177
+ npm install && npm test # vitest; no systemd/tmux needed for the suite
178
+ npm run build
179
+ ```
180
+
181
+ Adding a harness = implementing `HarnessAdapter`
182
+ (`src/harness/types.ts`) and registering it — see `src/harness/claude-code.ts`
183
+ for the reference implementation.
184
+
185
+ ## Support ours.network
186
+
187
+ ours-fleet is part of [ours.network](https://ours.network) — free, source-available
188
+ software built by a small independent team, running the broker and relay services
189
+ at their own cost. If this is useful to you, please consider chipping in:
190
+ **→ https://ours.network/donate**
191
+
192
+ ## License
193
+
194
+ [FSL-1.1-Apache-2.0](LICENSE) — Functional Source License, converting to
195
+ Apache-2.0 two years after each release. Free for any use except offering a
196
+ competing product or service. Copyright 2026 ours.network contributors.
@@ -0,0 +1,10 @@
1
+ import type { ResolvedRole } from './config.js';
2
+ import type { BriefingVocab } from './harness/types.js';
3
+ export interface BriefingOpts {
4
+ stateDir: string;
5
+ worklogPath: string;
6
+ /** Curated body (from briefing_file) replacing the narrative sections. */
7
+ briefingBody?: string;
8
+ }
9
+ /** Render a role's briefing.md: narrative (or curated body) + mechanical boot steps. */
10
+ export declare function generateBriefing(role: ResolvedRole, v: BriefingVocab, opts: BriefingOpts): string;
@@ -0,0 +1,73 @@
1
+ import { userInfo } from 'node:os';
2
+ /** Render a role's briefing.md: narrative (or curated body) + mechanical boot steps. */
3
+ export function generateBriefing(role, v, opts) {
4
+ const L = [];
5
+ const id = role.identity;
6
+ const hostUser = userInfo().username;
7
+ L.push(`# ${role.name} — Role Briefing`, '');
8
+ L.push(`You are **${role.name}** (ours identity: **${id}**), a persistent agent on this`);
9
+ L.push(`host, running as the \`${hostUser}\` user.`);
10
+ if (opts.briefingBody) {
11
+ L.push('', opts.briefingBody.trim());
12
+ }
13
+ else {
14
+ if (role.cwd)
15
+ L.push('', `Your working directory is \`${role.cwd}\`. Operate on the code there.`);
16
+ if (role.persona)
17
+ L.push('', '## Charter (persona — your local operating contract)', role.persona.trim());
18
+ if (role.bio)
19
+ L.push('', '## Bio (public card — what peers and a coordinator see)', role.bio.trim());
20
+ if (role.mission)
21
+ L.push('', '## Mission', role.mission.trim());
22
+ }
23
+ L.push('', '## Do these NOW, in order');
24
+ L.push(`1. ${v.launchNote(role.name)}`);
25
+ L.push(`2. BIND your predefined ours identity: call the **${v.bindTool}** tool with`);
26
+ L.push(` name "${id}" force=true (search the deferred tool registry first if needed).`);
27
+ L.push(` - If no such identity exists yet, call **${v.createTool}** name "${id}" once`);
28
+ L.push(' to mint it, then you are bound. Re-binding your OWN identity is always allowed.');
29
+ L.push(`3. RECONCILE your profile (idempotent): call **${v.currentIdentityTool}** and read your`);
30
+ L.push(' current bio and persona, so you only write below when they actually differ.');
31
+ L.push(`4. PUBLISH your public **bio** via **${v.setBioTool}**`);
32
+ L.push(role.bio
33
+ ? ' with the **Bio** section above, verbatim. Skip the call if it already matches.'
34
+ : ' with a 1–2 sentence summary of your Charter above. Skip if it already matches.');
35
+ L.push(`5. SET your **persona** (local operating contract, never shared in invites) via`);
36
+ L.push(` **${v.setPersonaTool}** with the **Charter** section above, verbatim. Skip if it matches.`);
37
+ L.push(`6. ${v.monitorInstruction(id)}`);
38
+ if (role.coordinator) {
39
+ L.push(`7. ANNOUNCE yourself: call **${v.sendTool}** to contact "${role.coordinator}" with text:`);
40
+ L.push(` "${role.name} online — identity '${id}' bound, monitor armed, ready."`);
41
+ L.push(`8. Await messages. When the monitor wakes you, call **${v.getMessagesTool}**, act, reply.`);
42
+ }
43
+ else {
44
+ L.push(`7. Await messages. When the monitor wakes you, call **${v.getMessagesTool}**, act on them,`);
45
+ L.push(` and reply with ${v.sendTool}. No coordinator is configured — the owner drives you`);
46
+ L.push(` via \`tmux attach -t ${role.name}\` or by messaging "${id}".`);
47
+ }
48
+ if (role.oversee?.length) {
49
+ L.push('', '## Oversight assignments');
50
+ L.push('These agents are your wards — you keep them unstuck:');
51
+ for (const o of role.oversee)
52
+ L.push(`- **${o.role}** — check every ${o.interval}`);
53
+ L.push('');
54
+ L.push('Procedure (see also the oversee-agents skill if available): on each tick, run');
55
+ for (const o of role.oversee)
56
+ L.push(`\`ours-fleet peek ${o.role}\``);
57
+ L.push('and judge the console: stuck on a prompt/menu/trust dialog → answer it directly with');
58
+ L.push('`ours-fleet send <Name> "<text>"` (or `--key <K>` for raw keys); crashed to a shell →');
59
+ L.push('investigate and restart; idle with work assigned → nudge; healthy → do nothing.');
60
+ L.push('Escalate over ours messaging only when you cannot resolve it yourself.');
61
+ }
62
+ L.push('', '## Durable log');
63
+ L.push(`Append important commands / decisions / results to \`${opts.worklogPath}\` as you go —`);
64
+ L.push('it survives restarts.');
65
+ L.push('', '## On restart (you run under a supervised launcher)');
66
+ L.push(`On restart, WITHOUT asking: re-bind (**${v.bindTool}** name "${id}" force=true),`);
67
+ L.push(`re-arm the \`${v.watchCommand(id)}\` monitor, then continue from your WORKLOG.`);
68
+ L.push('Do not blindly re-run whatever may have crashed you.');
69
+ L.push('', '## House rules');
70
+ L.push('- Never broad `rm -rf` on home/critical paths; quote globs; use explicit paths.');
71
+ L.push('- When you stop, be in a declared state (DONE / BLOCKED / resting ≤2h).');
72
+ return L.join('\n') + '\n';
73
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './harness/claude-code.js';
package/dist/cli.js ADDED
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ import { spawn as spawnChild } from 'node:child_process';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { realpathSync } from 'node:fs';
5
+ import { Command } from 'commander';
6
+ import { VERSION } from './version.js';
7
+ import { agentsRoot, tmpRoot, logsRoot } from './paths.js';
8
+ import { loadConfig } from './config.js';
9
+ import { Tmux } from './tmux.js';
10
+ import { pickBackend } from './supervisor/index.js';
11
+ import { up, down, restartRoles, rmRole } from './ops.js';
12
+ import { runOnce, runTemp } from './runner.js';
13
+ import { spawnPermanent, spawnTemp } from './spawn.js';
14
+ import { doctor } from './doctor.js';
15
+ import './harness/claude-code.js'; // registers the claude-code adapter
16
+ const binPath = (() => { try {
17
+ return realpathSync(process.argv[1]);
18
+ }
19
+ catch {
20
+ return process.argv[1];
21
+ } })();
22
+ const deps = () => ({
23
+ backend: pickBackend(),
24
+ binPath,
25
+ sleep: ms => new Promise(r => setTimeout(r, ms)),
26
+ log: l => console.log(l),
27
+ });
28
+ const die = (e) => { console.error(String(e instanceof Error ? e.message : e)); process.exit(1); };
29
+ /** Exec a child with our stdio (logs/attach). */
30
+ const passthrough = (cmd, args) => new Promise(resolve => {
31
+ const c = spawnChild(cmd, args, { stdio: 'inherit' });
32
+ c.on('exit', code => resolve(code ?? 1));
33
+ });
34
+ const program = new Command()
35
+ .name('ours-fleet')
36
+ .description('Fleet of persistent, identity-bound AI agents — harness-agnostic, tmux + systemd/launchd.')
37
+ .version(VERSION);
38
+ const cOpt = (cmd) => cmd.option('-c, --configuration <file>', 'config file (default: ~/fleet.yaml + ~/fleet.d/)');
39
+ cOpt(program.command('config').description('validate + print the merged plan (no side effects)'))
40
+ .action(opts => {
41
+ try {
42
+ const cfg = loadConfig(opts.configuration);
43
+ console.log(`config: ${cfg.files.join(' + ') || '(none)'}`);
44
+ for (const r of cfg.roles) {
45
+ console.log(`\n● ${r.name}`);
46
+ console.log(` harness: ${r.harness}`);
47
+ console.log(` identity: ${r.identity}`);
48
+ console.log(` source: ${r.sourceFile}`);
49
+ if (r.cwd)
50
+ console.log(` cwd: ${r.cwd}`);
51
+ if (r.coordinator)
52
+ console.log(` coordinator: ${r.coordinator}`);
53
+ if (r.mission)
54
+ console.log(` mission: ${r.mission.split('\n')[0]}`);
55
+ if (r.oversee?.length)
56
+ console.log(` oversees: ${r.oversee.map(o => `${o.role}@${o.interval}`).join(', ')}`);
57
+ }
58
+ }
59
+ catch (e) {
60
+ die(e);
61
+ }
62
+ });
63
+ cOpt(program.command('up [names...]').description('create/start every role (or just the named ones)'))
64
+ .action(async (names, opts) => {
65
+ try {
66
+ await up(loadConfig(opts.configuration), names, deps());
67
+ }
68
+ catch (e) {
69
+ die(e);
70
+ }
71
+ });
72
+ cOpt(program.command('down [names...]').description('stop roles'))
73
+ .action(async (names, opts) => {
74
+ try {
75
+ await down(loadConfig(opts.configuration), names, deps());
76
+ }
77
+ catch (e) {
78
+ die(e);
79
+ }
80
+ });
81
+ cOpt(program.command('restart [names...]').description('re-sync config + bounce, RESUMING context'))
82
+ .action(async (names, opts) => {
83
+ try {
84
+ await restartRoles(loadConfig(opts.configuration), names, deps(), 'keep');
85
+ }
86
+ catch (e) {
87
+ die(e);
88
+ }
89
+ });
90
+ cOpt(program.command('force-restart [names...]').description('re-sync + bounce FRESH (context wiped)'))
91
+ .action(async (names, opts) => {
92
+ try {
93
+ await restartRoles(loadConfig(opts.configuration), names, deps(), 'fresh');
94
+ }
95
+ catch (e) {
96
+ die(e);
97
+ }
98
+ });
99
+ program.command('ls').description('list running tmux consoles')
100
+ .action(async () => { console.log(await new Tmux().list() || '(none)'); });
101
+ program.command('attach <name>').description('open the live console (Ctrl-b d to leave)')
102
+ .action(async (name) => process.exit(await passthrough('tmux', ['attach', '-t', name])));
103
+ program.command('peek <name> [lines]').description('pane snapshot without attaching')
104
+ .action(async (name, lines) => {
105
+ try {
106
+ console.log(await new Tmux().capture(name, lines ? Number(lines) : 40));
107
+ }
108
+ catch {
109
+ die(`'${name}' is not running; try: ours-fleet status ${name}`);
110
+ }
111
+ });
112
+ program.command('send <name> [text...]').description("type into the agent's console")
113
+ .option('--key <key>', 'send a raw key instead (Escape, Up, C-c, ...)')
114
+ .action(async (name, text, opts) => {
115
+ try {
116
+ const tmux = new Tmux();
117
+ if (opts.key)
118
+ await tmux.sendKey(name, opts.key);
119
+ else if (text?.length)
120
+ await tmux.sendText(name, text.join(' '));
121
+ else
122
+ die('nothing to send: give text or --key');
123
+ }
124
+ catch {
125
+ die(`'${name}' is not running; try: ours-fleet status ${name}`);
126
+ }
127
+ });
128
+ program.command('logs <name>').description('show the role log').option('-f, --follow', 'follow')
129
+ .action(async (name, opts) => {
130
+ const { cmd, args } = pickBackend().logsArgs(name, opts.follow === true);
131
+ process.exit(await passthrough(cmd, args));
132
+ });
133
+ program.command('status <name>').description('unit/agent state')
134
+ .action(async (name) => { console.log(await pickBackend().status(name)); });
135
+ cOpt(program.command('rm <name>').description('stop + delete state dir (+ its fleet.d file if spawned)'))
136
+ .action(async (name, opts) => {
137
+ try {
138
+ await rmRole(loadConfig(opts.configuration), name, deps());
139
+ }
140
+ catch (e) {
141
+ die(e);
142
+ }
143
+ });
144
+ cOpt(program.command('spawn <name>').description('spawn a new agent (permanent by default)'))
145
+ .option('--temp', 'temporary: plain tmux, auto-cleaned, gone on reboot')
146
+ .option('--harness <id>', 'harness adapter (default: defaults.harness)')
147
+ .option('--mission <text>', 'one-line mission')
148
+ .option('--identity <name>', 'ours identity to bind (default: role name)')
149
+ .option('--cwd <dir>', 'working directory')
150
+ .option('--coordinator <name>', 'announce target')
151
+ .option('--bio-file <file>', 'public bio (file)')
152
+ .option('--persona-file <file>', 'persona / operating contract (file)')
153
+ .action(async (name, opts) => {
154
+ const o = {
155
+ name, temp: opts.temp, harness: opts.harness, mission: opts.mission,
156
+ identity: opts.identity, cwd: opts.cwd, coordinator: opts.coordinator,
157
+ bioFile: opts.bioFile, personaFile: opts.personaFile, configPath: opts.configuration,
158
+ };
159
+ try {
160
+ if (o.temp) {
161
+ const dir = await spawnTemp(o, new Tmux(), binPath);
162
+ console.log(`spawned temp agent '${name}' (state: ${dir}; gone on exit/reboot)`);
163
+ }
164
+ else {
165
+ const file = await spawnPermanent(o, deps());
166
+ console.log(`spawned '${name}' (config: ${file})`);
167
+ }
168
+ console.log(`→ watch it: ours-fleet peek ${name} | attach: ours-fleet attach ${name}`);
169
+ }
170
+ catch (e) {
171
+ die(e);
172
+ }
173
+ });
174
+ cOpt(program.command('doctor').description('prerequisite report'))
175
+ .option('--harness <id>', 'check one harness explicitly')
176
+ .action(async (opts) => {
177
+ const rep = await doctor({ harness: opts.harness, configPath: opts.configuration });
178
+ for (const c of rep.checks)
179
+ console.log(`${c.ok ? 'ok ' : 'MISS'} ${c.name.padEnd(22)} ${c.detail}`);
180
+ process.exit(rep.ok ? 0 : 1);
181
+ });
182
+ program.command('init').description('one-time host setup (units, dirs, linger)')
183
+ .action(async () => {
184
+ for (const d of [agentsRoot(), tmpRoot(), logsRoot()])
185
+ mkdirSync(d, { recursive: true });
186
+ for (const m of await pickBackend().init(binPath))
187
+ console.log(m);
188
+ console.log('\nNext: copy examples/fleet.yaml to ~/fleet.yaml, edit, then: ours-fleet up');
189
+ });
190
+ program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
191
+ .option('-c, --configuration <file>')
192
+ .action(async (name, opts) => {
193
+ try {
194
+ await runOnce(name, { configPath: opts.configuration });
195
+ }
196
+ catch (e) {
197
+ die(e);
198
+ }
199
+ });
200
+ program.command('_run-temp <name>', { hidden: true }).description('internal: temp-agent entrypoint')
201
+ .action(async (name) => {
202
+ try {
203
+ await runTemp(name);
204
+ }
205
+ catch (e) {
206
+ die(e);
207
+ }
208
+ });
209
+ program.parseAsync(process.argv);
@@ -0,0 +1,36 @@
1
+ export interface OverseeEntry {
2
+ role: string;
3
+ interval: string;
4
+ }
5
+ export interface RoleConfig {
6
+ harness?: string;
7
+ identity?: string;
8
+ cwd?: string;
9
+ coordinator?: string;
10
+ mission?: string;
11
+ persona?: string;
12
+ bio?: string;
13
+ briefing_file?: string;
14
+ max_tokens?: number;
15
+ autocompact_pct?: number;
16
+ env?: Record<string, string>;
17
+ oversee?: OverseeEntry[];
18
+ harness_options?: Record<string, unknown>;
19
+ }
20
+ export interface ResolvedRole extends RoleConfig {
21
+ name: string;
22
+ harness: string;
23
+ identity: string;
24
+ sourceFile: string;
25
+ }
26
+ export interface FleetConfig {
27
+ roles: ResolvedRole[];
28
+ vars: Record<string, string>;
29
+ defaults: Record<string, unknown>;
30
+ files: string[];
31
+ }
32
+ export declare class ConfigError extends Error {
33
+ }
34
+ /** Load ~/fleet.yaml (or an explicit path) merged with ~/fleet.d/*.yaml drop-ins. */
35
+ export declare function loadConfig(configPath?: string): FleetConfig;
36
+ export declare function findRole(cfg: FleetConfig, name: string): ResolvedRole;