@ours.network/fleet 0.7.0 → 0.7.1

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 CHANGED
@@ -62,7 +62,7 @@ The state dir contract:
62
62
  | `briefing.md` | generated | rewritten on every `up`/`restart`; never hand-edit |
63
63
  | `WORKLOG.md` | the agent | seeded empty, agent-appended; survives restarts |
64
64
  | `ROUTINES.md` | operator / agent | **optional** recurring-work instructions; re-read at the start of every wake, hot-editable **without a restart**; absence means "no routines" |
65
- | `.identity`, `.cwd`, `.session-id`, `.booted`, `.exit-status` | supervisor | dot-marker state — session resume and boot bookkeeping |
65
+ | `.identity`, `.cwd`, `.session-id`, `.booted`, `.exit-status`, `.config-path` | supervisor | dot-marker state — session resume and boot bookkeeping |
66
66
 
67
67
  ## Prerequisites
68
68
 
@@ -169,6 +169,11 @@ ours-fleet doctor [--harness H]
169
169
  ours-fleet init
170
170
  ```
171
171
 
172
+ A permanent role brought up via `-c custom.yaml` remembers that file (`.config-path`
173
+ in its state dir) across supervisor-triggered restarts — systemd/launchd re-invoke the
174
+ agent process with no arguments, so without this the role would silently fall back to
175
+ the default `~/fleet.yaml` on its very first crash-restart and fail to resolve.
176
+
172
177
  ## fleet.yaml reference
173
178
 
174
179
  ```yaml
package/dist/cli.js CHANGED
@@ -101,7 +101,7 @@ cOpt(program.command('config').description('validate + print the merged plan (no
101
101
  cOpt(program.command('up [names...]').description('create/start every role (or just the named ones)'))
102
102
  .action(async (names, opts) => {
103
103
  try {
104
- await up(loadConfig(opts.configuration), names, deps());
104
+ await up(loadConfig(opts.configuration), names, deps(), opts.configuration);
105
105
  }
106
106
  catch (e) {
107
107
  die(e);
@@ -119,7 +119,7 @@ cOpt(program.command('down [names...]').description('stop roles'))
119
119
  cOpt(program.command('restart [names...]').description('re-sync config + bounce, RESUMING context'))
120
120
  .action(async (names, opts) => {
121
121
  try {
122
- await restartRoles(loadConfig(opts.configuration), names, deps(), 'keep');
122
+ await restartRoles(loadConfig(opts.configuration), names, deps(), 'keep', opts.configuration);
123
123
  }
124
124
  catch (e) {
125
125
  die(e);
@@ -128,7 +128,7 @@ cOpt(program.command('restart [names...]').description('re-sync config + bounce,
128
128
  cOpt(program.command('force-restart [names...]').description('re-sync + bounce FRESH (context wiped)'))
129
129
  .action(async (names, opts) => {
130
130
  try {
131
- await restartRoles(loadConfig(opts.configuration), names, deps(), 'fresh');
131
+ await restartRoles(loadConfig(opts.configuration), names, deps(), 'fresh', opts.configuration);
132
132
  }
133
133
  catch (e) {
134
134
  die(e);
package/dist/ops.d.ts CHANGED
@@ -10,11 +10,12 @@ export interface OpsDeps {
10
10
  export declare function applyRole(role: ResolvedRole, opts?: {
11
11
  fresh?: boolean;
12
12
  temp?: boolean;
13
+ configPath?: string;
13
14
  }): string;
14
15
  /** 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 up(cfg: FleetConfig, names: string[], deps: OpsDeps, configPath?: string): Promise<void>;
16
17
  export declare function down(cfg: FleetConfig, names: string[], deps: OpsDeps): Promise<void>;
17
18
  /** 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
+ export declare function restartRoles(cfg: FleetConfig, names: string[], deps: OpsDeps, mode: 'keep' | 'fresh', configPath?: string): Promise<void>;
19
20
  /** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
20
21
  export declare function rmRole(cfg: FleetConfig, name: string, deps: OpsDeps): Promise<void>;
package/dist/ops.js CHANGED
@@ -14,6 +14,13 @@ export function applyRole(role, opts = {}) {
14
14
  throw new Error(`role '${role.name}': ` + errs.map(e => `${e.path}: ${e.message}`).join('; '));
15
15
  const dir = agentDir(role.name, opts.temp === true);
16
16
  mkdirSync(dir, { recursive: true });
17
+ // systemd's shared unit template invokes `_run <name>` on every restart with no
18
+ // -c (see supervisor/systemd.ts) — record which config this permanent role was
19
+ // brought up from so the supervised process can reload the SAME file instead of
20
+ // silently falling back to the default ~/fleet.yaml. Temp roles snapshot their
21
+ // whole resolved role into role.yaml instead and don't need this.
22
+ if (!opts.temp)
23
+ writeFileSync(join(dir, '.config-path'), (opts.configPath ?? '') + '\n');
17
24
  writeFileSync(join(dir, '.identity'), role.identity + '\n');
18
25
  if (role.cwd)
19
26
  writeFileSync(join(dir, '.cwd'), role.cwd + '\n');
@@ -35,13 +42,13 @@ function selectRoles(cfg, names) {
35
42
  return names.length ? names.map(n => findRole(cfg, n)) : cfg.roles;
36
43
  }
37
44
  /** Create/start roles declaratively. Idempotent; active roles keep their context. */
38
- export async function up(cfg, names, deps) {
45
+ export async function up(cfg, names, deps, configPath) {
39
46
  let first = true;
40
47
  for (const role of selectRoles(cfg, names)) {
41
48
  if (!first)
42
49
  await deps.sleep(STAGGER_MS());
43
50
  first = false;
44
- const dir = applyRole(role);
51
+ const dir = applyRole(role, { configPath });
45
52
  // If the role isn't running, boot fresh so it reads the briefing we just wrote.
46
53
  const status = await deps.backend.status(role.name).catch(() => '');
47
54
  if (!/running|active \(/.test(status))
@@ -62,13 +69,13 @@ export async function down(cfg, names, deps) {
62
69
  }
63
70
  }
64
71
  /** Re-sync from config + bounce. mode 'keep' resumes context; 'fresh' wipes it. */
65
- export async function restartRoles(cfg, names, deps, mode) {
72
+ export async function restartRoles(cfg, names, deps, mode, configPath) {
66
73
  let first = true;
67
74
  for (const role of selectRoles(cfg, names)) {
68
75
  if (!first)
69
76
  await deps.sleep(STAGGER_MS());
70
77
  first = false;
71
- applyRole(role, { fresh: mode === 'fresh' });
78
+ applyRole(role, { fresh: mode === 'fresh', configPath });
72
79
  await deps.backend.restart(role.name);
73
80
  deps.log(mode === 'fresh'
74
81
  ? `↻ ${role.name} — force-restarted (FRESH — context cleared, briefing reloaded)`
package/dist/runner.js CHANGED
@@ -47,13 +47,31 @@ export function loadTempRole(name) {
47
47
  role.__temp = true;
48
48
  return role;
49
49
  }
50
+ /**
51
+ * Fall back to the config path applyRole() recorded at the last up/restart/spawn
52
+ * for this role. systemd's shared unit template (`ours-fleet-agent@.service`)
53
+ * execs `_run <name>` with no -c on every restart, so a role brought up from a
54
+ * non-default config (`ours-fleet up -c custom.yaml`) would otherwise silently
55
+ * resolve against the default ~/fleet.yaml on its very first restart and fail
56
+ * with "no such role". An empty/missing marker means "use the default", same as
57
+ * no -c was ever given.
58
+ */
59
+ function resolveConfigPath(dir, explicit) {
60
+ if (explicit)
61
+ return explicit;
62
+ const marker = join(dir, '.config-path');
63
+ if (!existsSync(marker))
64
+ return undefined;
65
+ return readFileSync(marker, 'utf8').trim() || undefined;
66
+ }
50
67
  /** One supervised session lifecycle. The supervisor re-invokes us after we return. */
51
68
  export async function runOnce(name, opts = {}, partialDeps = {}) {
52
69
  const deps = { ...defaultDeps(), ...partialDeps };
53
70
  const temp = opts.temp === true;
54
- const role = temp ? loadTempRole(name) : findRole(loadConfig(opts.configPath), name);
55
- const adapter = getAdapter(role.harness);
56
71
  const dir = agentDir(name, temp);
72
+ const configPath = temp ? opts.configPath : resolveConfigPath(dir, opts.configPath);
73
+ const role = temp ? loadTempRole(name) : findRole(loadConfig(configPath), name);
74
+ const adapter = getAdapter(role.harness);
57
75
  mkdirSync(dir, { recursive: true });
58
76
  const sidFile = join(dir, '.session-id');
59
77
  if (!existsSync(sidFile))
package/dist/spawn.js CHANGED
@@ -61,7 +61,7 @@ export async function spawnPermanent(o, deps) {
61
61
  writeFileSync(file, stringify({
62
62
  roles: { [o.name]: roleFromOpts(o, cfg.defaults.harness) },
63
63
  }));
64
- await up(loadConfig(o.configPath), [o.name], deps);
64
+ await up(loadConfig(o.configPath), [o.name], deps, o.configPath);
65
65
  return file;
66
66
  }
67
67
  const detachedSupervisor = (binPath, args, dir) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux consoles, systemd/launchd supervision, ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",