@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/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;
|
package/dist/briefing.js
ADDED
|
@@ -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
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);
|
package/dist/config.d.ts
ADDED
|
@@ -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;
|