@ours.network/fleet 0.18.0 → 0.19.0-nightly.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.
Files changed (69) hide show
  1. package/README.md +57 -121
  2. package/dist/application/fleet-query-service.js +0 -12
  3. package/dist/application/role-creation-service.js +10 -7
  4. package/dist/application/types.d.ts +0 -11
  5. package/dist/briefing.js +6 -15
  6. package/dist/build-info.json +6 -7
  7. package/dist/capabilities.d.ts +1 -3
  8. package/dist/capabilities.js +0 -3
  9. package/dist/cli.js +7 -83
  10. package/dist/config.d.ts +3 -11
  11. package/dist/config.js +15 -40
  12. package/dist/creation.d.ts +38 -22
  13. package/dist/creation.js +111 -24
  14. package/dist/docs.d.ts +1 -1
  15. package/dist/docs.js +46 -120
  16. package/dist/doctor.d.ts +5 -1
  17. package/dist/doctor.js +18 -11
  18. package/dist/fleet-proxy.d.ts +0 -5
  19. package/dist/harness/acp-agent.js +6 -11
  20. package/dist/harness/claude-code.js +11 -204
  21. package/dist/harness/codex.d.ts +1 -4
  22. package/dist/harness/codex.js +12 -74
  23. package/dist/harness/types.d.ts +4 -54
  24. package/dist/index.d.ts +0 -2
  25. package/dist/index.js +0 -1
  26. package/dist/loops/manager.d.ts +1 -30
  27. package/dist/loops/manager.js +6 -69
  28. package/dist/loops/state.d.ts +0 -18
  29. package/dist/loops/state.js +0 -4
  30. package/dist/monitor.js +1 -1
  31. package/dist/ops.d.ts +3 -0
  32. package/dist/ops.js +8 -3
  33. package/dist/owner-channel/attachments.d.ts +25 -2
  34. package/dist/owner-channel/attachments.js +61 -5
  35. package/dist/owner-channel/channel.d.ts +29 -30
  36. package/dist/owner-channel/channel.js +291 -291
  37. package/dist/owner-channel/message-recovery.d.ts +25 -0
  38. package/dist/owner-channel/message-recovery.js +114 -0
  39. package/dist/owner-channel/notices.d.ts +0 -7
  40. package/dist/owner-channel/notices.js +0 -9
  41. package/dist/owner-channel/ours-client.d.ts +148 -0
  42. package/dist/owner-channel/ours-client.js +231 -0
  43. package/dist/resolved-plan.js +0 -1
  44. package/dist/runner.d.ts +0 -48
  45. package/dist/runner.js +94 -236
  46. package/dist/session/acp.d.ts +0 -104
  47. package/dist/session/acp.js +10 -213
  48. package/dist/session/conversation-normalizer.d.ts +0 -6
  49. package/dist/session/conversation-normalizer.js +10 -153
  50. package/dist/session/conversation-types.d.ts +4 -23
  51. package/dist/session/types.d.ts +0 -35
  52. package/dist/spawn.js +26 -33
  53. package/dist/supervisor/systemd.js +29 -2
  54. package/dist/watchdog/briefing.js +0 -7
  55. package/dist/watchdog/run.js +3 -3
  56. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  57. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  58. package/dist/web-app/index.html +1 -1
  59. package/dist/worklog.d.ts +1 -7
  60. package/dist/worklog.js +39 -191
  61. package/package.json +3 -1
  62. package/dist/harness-plugins.d.ts +0 -48
  63. package/dist/harness-plugins.js +0 -309
  64. package/dist/model-env.d.ts +0 -71
  65. package/dist/model-env.js +0 -106
  66. package/dist/owner-channel/mcp.d.ts +0 -24
  67. package/dist/owner-channel/mcp.js +0 -145
  68. package/dist/session/activity.d.ts +0 -31
  69. package/dist/session/activity.js +0 -48
@@ -8,7 +8,7 @@
8
8
  <link rel="manifest" href="/manifest.webmanifest" />
9
9
  <link rel="icon" href="/icons/ours-fleet.svg" type="image/svg+xml" />
10
10
  <title>ours fleet console</title>
11
- <script type="module" crossorigin src="/assets/index-BCBK78hw.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-C3S-xFRU.js"></script>
12
12
  <link rel="stylesheet" crossorigin href="/assets/index-DuC-xnX4.css">
13
13
  </head>
14
14
  <body>
package/dist/worklog.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { WorklogPolicy } from './config.js';
2
- export declare const WORKLOG_ARCHIVE_DIR = "WORKLOG.archives";
3
2
  export interface WorklogInspection {
4
3
  enabled: boolean;
5
4
  bytes: number;
@@ -13,11 +12,6 @@ export interface WorklogRotation {
13
12
  archivePath?: string;
14
13
  }
15
14
  export declare function inspectWorklog(path: string, policy?: WorklogPolicy): WorklogInspection;
16
- /**
17
- * Keep a bounded set of recent archives beside WORKLOG.md without deleting
18
- * history. Older archives move atomically-by-link into WORKLOG.archives/.
19
- * The legacy name remains exported for source compatibility.
20
- */
21
15
  export declare function pruneWorklogArchives(path: string, maxArchives: number): void;
22
16
  /**
23
17
  * Conservatively rotate a stable snapshot. A changed size/mtime/inode aborts
@@ -26,6 +20,6 @@ export declare function pruneWorklogArchives(path: string, maxArchives: number):
26
20
  export declare function rotateWorklog(path: string, policy?: WorklogPolicy, deps?: {
27
21
  now?: () => Date;
28
22
  beforeCommit?: () => void;
29
- /** Deterministic test hook after the full archive is published. */
23
+ /** Deterministic test hook for the rename→link commit window. */
30
24
  afterArchiveRename?: () => void;
31
25
  }): WorklogRotation;
package/dist/worklog.js CHANGED
@@ -1,59 +1,10 @@
1
- import { closeSync, constants, existsSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, unlinkSync, } from 'node:fs';
2
- import { createHash } from 'node:crypto';
1
+ import { existsSync, linkSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from 'node:fs';
3
2
  import { basename, dirname, join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
4
  import { replaceFileAtomically } from './atomic-file.js';
5
- const ARCHIVE_RE = /^WORKLOG\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)(?:\.(\d+))?\.md$/;
6
- export const WORKLOG_ARCHIVE_DIR = 'WORKLOG.archives';
7
- const entryStat = (path) => {
8
- try {
9
- return lstatSync(path);
10
- }
11
- catch (error) {
12
- if (error.code === 'ENOENT')
13
- return undefined;
14
- throw error;
15
- }
16
- };
17
- const requireRegularFile = (path, label) => {
18
- const stat = entryStat(path);
19
- if (!stat)
20
- throw new Error(`${label} disappeared before it could be preserved`);
21
- if (!stat.isFile()) {
22
- const kind = stat.isSymbolicLink() ? 'symbolic link' : 'non-regular file';
23
- throw new Error(`refusing worklog rotation: ${label} is a ${kind}`);
24
- }
25
- return stat;
26
- };
27
- const sameInode = (left, right) => left.dev === right.dev && left.ino === right.ino;
28
- const sameSnapshot = (left, right) => sameInode(left, right) && left.size === right.size
29
- && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
30
- const sha256 = (content) => createHash('sha256').update(content).digest('hex');
31
- /** Remove a newly published duplicate only while the source inode is still available. */
32
- const cleanupDuplicateLink = (source, target, expected) => {
33
- try {
34
- const sourceStat = entryStat(source);
35
- const targetStat = entryStat(target);
36
- if (sourceStat && targetStat && sameInode(expected, sourceStat) && sameInode(expected, targetStat))
37
- unlinkSync(target);
38
- }
39
- catch { /* cleanup is best-effort; preserve the primary failure */ }
40
- };
41
- const validateArchiveBoundary = (path) => {
42
- const archiveDir = join(dirname(path), WORKLOG_ARCHIVE_DIR);
43
- const stat = entryStat(archiveDir);
44
- if (stat && !stat.isDirectory()) {
45
- const kind = stat.isSymbolicLink() ? 'symbolic link' : 'non-directory';
46
- throw new Error(`refusing worklog retention: ${WORKLOG_ARCHIVE_DIR} is a ${kind}`);
47
- }
48
- return stat;
49
- };
5
+ const ARCHIVE_RE = /^WORKLOG\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z(?:\.\d+)?\.md$/;
50
6
  export function inspectWorklog(path, policy) {
51
- const stat = entryStat(path);
52
- if (policy && stat && !stat.isFile()) {
53
- const kind = stat.isSymbolicLink() ? 'symbolic link' : 'non-regular file';
54
- throw new Error(`refusing worklog rotation: WORKLOG.md is a ${kind}`);
55
- }
56
- const bytes = stat?.isFile() ? stat.size : 0;
7
+ const bytes = existsSync(path) ? statSync(path).size : 0;
57
8
  return {
58
9
  enabled: policy !== undefined,
59
10
  bytes,
@@ -71,70 +22,11 @@ const archiveName = (path, now, collision) => {
71
22
  const stamp = now.toISOString().replace(/:/g, '-');
72
23
  return join(dirname(path), `WORKLOG.${stamp}${collision ? `.${collision}` : ''}.md`);
73
24
  };
74
- /**
75
- * Keep a bounded set of recent archives beside WORKLOG.md without deleting
76
- * history. Older archives move atomically-by-link into WORKLOG.archives/.
77
- * The legacy name remains exported for source compatibility.
78
- */
79
25
  export function pruneWorklogArchives(path, maxArchives) {
80
26
  const dir = dirname(path);
81
- const archives = readdirSync(dir).filter(name => ARCHIVE_RE.test(name)).sort((a, b) => {
82
- const left = ARCHIVE_RE.exec(a);
83
- const right = ARCHIVE_RE.exec(b);
84
- return left[1].localeCompare(right[1])
85
- || Number(left[2] ?? 0) - Number(right[2] ?? 0);
86
- }).reverse();
87
- const older = archives.slice(maxArchives);
88
- if (!older.length)
89
- return;
90
- const archiveDir = join(dir, WORKLOG_ARCHIVE_DIR);
91
- let boundary = validateArchiveBoundary(path);
92
- if (!boundary) {
93
- try {
94
- mkdirSync(archiveDir, { mode: 0o700 });
95
- }
96
- catch (error) {
97
- if (error.code !== 'EEXIST')
98
- throw error;
99
- }
100
- boundary = validateArchiveBoundary(path);
101
- }
102
- if (!boundary)
103
- throw new Error(`refusing worklog retention: ${WORKLOG_ARCHIVE_DIR} is unavailable`);
104
- for (const old of older) {
105
- const source = join(dir, old);
106
- const sourceBefore = requireRegularFile(source, `archive ${old}`);
107
- let collision = 0;
108
- for (;;) {
109
- const name = collision === 0 ? old : old.replace(/\.md$/, `.${collision}.md`);
110
- const target = join(archiveDir, name);
111
- try {
112
- const currentBoundary = validateArchiveBoundary(path);
113
- if (!currentBoundary || !sameInode(boundary, currentBoundary))
114
- throw new Error(`refusing worklog retention: ${WORKLOG_ARCHIVE_DIR} changed`);
115
- // Link then unlink: a crash can leave a duplicate, never lose the only
116
- // archive. EEXIST chooses a new name rather than overwriting history.
117
- linkSync(source, target);
118
- const published = requireRegularFile(target, `cold archive ${name}`);
119
- const sourceCurrent = requireRegularFile(source, `archive ${old}`);
120
- const finalBoundary = validateArchiveBoundary(path);
121
- if (!sameInode(sourceBefore, published) || !sameInode(sourceBefore, sourceCurrent)
122
- || !finalBoundary || !sameInode(boundary, finalBoundary)) {
123
- throw new Error(`refusing worklog retention: archive boundary changed during move`);
124
- }
125
- unlinkSync(source);
126
- break;
127
- }
128
- catch (error) {
129
- if (error.code === 'EEXIST') {
130
- collision++;
131
- continue;
132
- }
133
- cleanupDuplicateLink(source, target, sourceBefore);
134
- throw error;
135
- }
136
- }
137
- }
27
+ const archives = readdirSync(dir).filter(name => ARCHIVE_RE.test(name)).sort().reverse();
28
+ for (const old of archives.slice(maxArchives))
29
+ rmSync(join(dir, old), { force: true });
138
30
  }
139
31
  /**
140
32
  * Conservatively rotate a stable snapshot. A changed size/mtime/inode aborts
@@ -144,101 +36,57 @@ export function rotateWorklog(path, policy, deps = {}) {
144
36
  const inspection = inspectWorklog(path, policy);
145
37
  if (!policy || !inspection.overLimit)
146
38
  return { rotated: false, beforeBytes: inspection.bytes, afterBytes: inspection.bytes };
147
- // Refuse a pre-existing symlink/non-directory even before publishing an
148
- // archive or replacing the live path. Cold retention must remain inside the
149
- // role's state boundary.
150
- validateArchiveBoundary(path);
151
- const pathBeforeOpen = requireRegularFile(path, 'WORKLOG.md');
152
- let fd;
153
- let before;
154
- let content;
155
- try {
156
- fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
157
- before = fstatSync(fd);
158
- if (!before.isFile() || !sameInode(pathBeforeOpen, before))
159
- throw new Error('refusing worklog rotation: WORKLOG.md changed while opening');
160
- content = readFileSync(fd);
161
- const afterRead = fstatSync(fd);
162
- if (!sameSnapshot(before, afterRead))
163
- return {
164
- rotated: false, deferred: true,
165
- beforeBytes: before.size, afterBytes: afterRead.size,
166
- };
167
- }
168
- finally {
169
- if (fd !== undefined)
170
- closeSync(fd);
171
- }
39
+ const before = statSync(path);
40
+ const content = readFileSync(path);
172
41
  const start = safeTailStart(content, policy.keep_tail_kb * 1024);
173
42
  const tail = content.subarray(start);
174
- const tailStartsMidLine = start > 0 && content[start - 1] !== 0x0a;
175
43
  deps.beforeCommit?.();
176
- const current = requireRegularFile(path, 'WORKLOG.md');
177
- if (!sameSnapshot(current, before)) {
44
+ const current = statSync(path);
45
+ if (current.ino !== before.ino || current.size !== before.size
46
+ || current.mtimeMs !== before.mtimeMs) {
178
47
  return {
179
48
  rotated: false, deferred: true,
180
49
  beforeBytes: before.size, afterBytes: current.size,
181
50
  };
182
51
  }
183
- const rotatedAt = deps.now?.() ?? new Date();
184
- let archivePath = '';
185
- // Publish the complete original inode under a collision-safe archive name.
186
- // link(2) is create-without-overwrite; concurrent appenders holding the old
187
- // inode continue into the archive after the live path is replaced.
188
- for (let collision = 0;; collision++) {
189
- archivePath = archiveName(path, rotatedAt, collision);
52
+ let collision = 0;
53
+ let archivePath = archiveName(path, deps.now?.() ?? new Date(), collision);
54
+ while (existsSync(archivePath))
55
+ archivePath = archiveName(path, deps.now?.() ?? new Date(), ++collision);
56
+ // Prepare the intended tail before moving the live inode. replaceFileAtomically
57
+ // gives us a fully written/fsynced inode; hard-linking it below publishes that
58
+ // inode without ever overwriting a path a concurrent appender may create.
59
+ const preparedTail = join(dirname(path), `.${basename(path)}.${randomUUID()}.rotate`);
60
+ replaceFileAtomically(preparedTail, tail.toString('utf8'), before.mode & 0o777);
61
+ try {
62
+ // Linearization point: the complete original inode becomes the archive.
63
+ // Writers that opened before this rename keep appending to that inode, so
64
+ // their acknowledged bytes remain in the archive even after the move.
65
+ renameSync(path, archivePath);
66
+ deps.afterArchiveRename?.();
190
67
  try {
191
- linkSync(path, archivePath);
192
- const published = requireRegularFile(archivePath, `archive ${basename(archivePath)}`);
193
- if (!sameInode(before, published)) {
194
- try {
195
- unlinkSync(archivePath);
196
- }
197
- catch { /* keep the primary safety failure */ }
198
- throw new Error('refusing worklog rotation: published archive is not the inspected file');
199
- }
200
- break;
68
+ // Atomic create-without-overwrite. If a writer opened the missing path in
69
+ // the rename→link window, it created the new live file; EEXIST means leave
70
+ // that file untouched. The intended tail remains recoverable in the full
71
+ // archive, and every concurrent suffix remains in the writer-created live.
72
+ linkSync(preparedTail, path);
201
73
  }
202
- catch (error) {
203
- if (error.code === 'EEXIST')
204
- continue;
205
- throw error;
74
+ catch (e) {
75
+ if (e.code !== 'EEXIST')
76
+ throw e;
206
77
  }
207
78
  }
208
- try {
209
- deps.afterArchiveRename?.();
210
- const liveBeforeReplace = requireRegularFile(path, 'WORKLOG.md');
211
- if (!sameInode(before, liveBeforeReplace))
212
- throw new Error('refusing worklog rotation: WORKLOG.md changed after archive publication');
213
- // The helper writes and fsyncs a same-directory temp, atomically renames it
214
- // over the live path, then fsyncs the directory. WORKLOG.md is never absent.
215
- replaceFileAtomically(path, tail.toString('utf8'), before.mode & 0o777);
216
- }
217
- catch (error) {
218
- // If the inspected inode is still the live file, the archive is only a
219
- // duplicate artifact. Remove it best-effort and preserve the real error.
220
- // If the live path changed, retain the archive because it may be the only
221
- // remaining link to the inspected history.
222
- cleanupDuplicateLink(path, archivePath, before);
223
- throw error;
79
+ finally {
80
+ rmSync(preparedTail, { force: true });
224
81
  }
225
- const archiveContent = readFileSync(archivePath);
226
- const liveContent = existsSync(path) ? readFileSync(path) : Buffer.alloc(0);
227
- const liveBytes = liveContent.length;
82
+ const liveBytes = existsSync(path) ? statSync(path).size : 0;
228
83
  const status = {
229
84
  schemaVersion: 1,
230
- rotatedAt: rotatedAt.toISOString(),
85
+ rotatedAt: (deps.now?.() ?? new Date()).toISOString(),
231
86
  beforeBytes: before.size,
232
87
  afterBytes: liveBytes,
233
88
  archive: basename(archivePath),
234
- archiveBytes: archiveContent.length,
235
- archiveSha256: sha256(archiveContent),
236
- liveSha256: sha256(liveContent),
237
89
  archiveContainsFullSnapshot: true,
238
- tailOmittedPrefixBytes: start,
239
- ...(tailStartsMidLine ? { tailStartsMidLine: true } : {}),
240
- olderArchives: WORKLOG_ARCHIVE_DIR,
241
- recentArchiveLimit: policy.max_archives,
242
90
  };
243
91
  replaceFileAtomically(join(dirname(path), '.worklog-rotation.json'), `${JSON.stringify(status, null, 2)}\n`);
244
92
  pruneWorklogArchives(path, policy.max_archives);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "0.18.0",
3
+ "version": "0.19.0-nightly.1",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -38,6 +38,8 @@
38
38
  "@agentclientprotocol/sdk": "^1.3.0",
39
39
  "@fastify/static": "^10.1.2",
40
40
  "@fastify/websocket": "^11.2.0",
41
+ "@ours.network/cli": "1.0.1",
42
+ "@ours.network/sdk": "3.0.1",
41
43
  "@xterm/addon-fit": "0.10.0",
42
44
  "@xterm/addon-serialize": "0.13.0",
43
45
  "@xterm/headless": "5.5.0",
@@ -1,48 +0,0 @@
1
- import { type Exec } from './exec.js';
2
- export declare const HARNESS_PLUGIN_IDS: readonly ["codex", "claude-code"];
3
- export type HarnessPluginId = (typeof HARNESS_PLUGIN_IDS)[number];
4
- export type HarnessPluginChannel = 'stable' | 'nightly';
5
- export interface HarnessPluginConfig {
6
- plugin_channel: HarnessPluginChannel;
7
- }
8
- export type HarnessPluginConfigs = Record<HarnessPluginId, HarnessPluginConfig>;
9
- export interface HarnessPluginLock {
10
- schemaVersion: 1;
11
- harness: HarnessPluginId;
12
- channel: HarnessPluginChannel;
13
- distTag: 'latest' | 'nightly';
14
- package: string;
15
- version: string;
16
- registry: 'https://registry.npmjs.org';
17
- resolvedAt: string;
18
- }
19
- export interface HarnessPluginInstallResult {
20
- lock: HarnessPluginLock;
21
- lockPath: string;
22
- marketplacePath: string;
23
- resolved: boolean;
24
- }
25
- export declare function resolveHarnessPluginConfigs(raw: unknown, file?: string): HarnessPluginConfigs;
26
- export declare const harnessPluginRoot: (harness: HarnessPluginId) => string;
27
- export declare const harnessPluginLockPath: (harness: HarnessPluginId) => string;
28
- export declare const harnessPluginMarketplaceRoot: (harness: HarnessPluginId) => string;
29
- export declare const harnessPluginMarketplacePath: (harness: HarnessPluginId) => string;
30
- export declare function readHarnessPluginLock(harness: HarnessPluginId): HarnessPluginLock | undefined;
31
- /**
32
- * Re-materialize the generated marketplace from the persisted exact lock.
33
- * This is the ONLY ordinary startup/reconciliation path: it performs no exec,
34
- * no network call, and cannot observe a moving npm dist-tag.
35
- */
36
- export declare function restoreLockedHarnessMarketplace(harness: HarnessPluginId, expectedChannel?: HarnessPluginChannel): HarnessPluginLock | undefined;
37
- /**
38
- * Explicit install/update transaction.
39
- *
40
- * install reuses an existing same-channel lock (deterministic repair/reinstall);
41
- * update always resolves the requested channel once and advances the lock.
42
- */
43
- export declare function installHarnessPlugin(harness: HarnessPluginId, channel: HarnessPluginChannel, options?: {
44
- update?: boolean;
45
- exec?: Exec;
46
- now?: () => Date;
47
- }): Promise<HarnessPluginInstallResult>;
48
- export declare function selectedHarnessPluginIds(values: string[]): HarnessPluginId[];