@ours.network/fleet 0.18.1 → 0.19.0-nightly.2
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 +57 -91
- package/dist/application/fleet-query-service.js +0 -12
- package/dist/application/role-creation-service.js +10 -7
- package/dist/application/types.d.ts +0 -11
- package/dist/briefing.js +6 -15
- package/dist/build-info.json +5 -5
- package/dist/cli.js +12 -37
- package/dist/config.d.ts +8 -6
- package/dist/config.js +46 -30
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +44 -95
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +18 -11
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -200
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -70
- package/dist/harness/types.d.ts +4 -54
- package/dist/loops/manager.d.ts +1 -30
- package/dist/loops/manager.js +6 -69
- package/dist/loops/state.d.ts +0 -18
- package/dist/loops/state.js +0 -4
- package/dist/monitor.js +1 -1
- package/dist/ops.d.ts +3 -0
- package/dist/ops.js +8 -3
- package/dist/owner-channel/attachments.d.ts +25 -2
- package/dist/owner-channel/attachments.js +61 -5
- package/dist/owner-channel/channel.d.ts +29 -30
- package/dist/owner-channel/channel.js +291 -291
- package/dist/owner-channel/commands.js +89 -0
- package/dist/owner-channel/message-recovery.d.ts +25 -0
- package/dist/owner-channel/message-recovery.js +114 -0
- package/dist/owner-channel/notices.d.ts +0 -7
- package/dist/owner-channel/notices.js +0 -9
- package/dist/owner-channel/ours-client.d.ts +148 -0
- package/dist/owner-channel/ours-client.js +231 -0
- package/dist/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +565 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +171 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +80 -0
- package/dist/rooms-tasks/cowork-adapter.js +48 -0
- package/dist/rooms-tasks/index.d.ts +7 -0
- package/dist/rooms-tasks/index.js +7 -0
- package/dist/rooms-tasks/room-state.d.ts +22 -0
- package/dist/rooms-tasks/room-state.js +117 -0
- package/dist/rooms-tasks/task-state.d.ts +31 -0
- package/dist/rooms-tasks/task-state.js +173 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +80 -0
- package/dist/rooms-tasks/types.d.ts +153 -0
- package/dist/rooms-tasks/types.js +20 -0
- package/dist/runner.d.ts +0 -48
- package/dist/runner.js +94 -236
- package/dist/session/acp.d.ts +0 -104
- package/dist/session/acp.js +10 -213
- package/dist/session/conversation-normalizer.d.ts +0 -6
- package/dist/session/conversation-normalizer.js +10 -153
- package/dist/session/conversation-types.d.ts +4 -23
- package/dist/session/types.d.ts +0 -35
- package/dist/spawn.js +26 -33
- package/dist/supervisor/systemd.js +29 -2
- package/dist/watchdog/briefing.js +0 -7
- package/dist/watchdog/run.js +3 -3
- package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
- package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +1 -7
- package/dist/worklog.js +39 -191
- package/package.json +3 -1
- package/dist/model-env.d.ts +0 -71
- package/dist/model-env.js +0 -106
- package/dist/owner-channel/mcp.d.ts +0 -24
- package/dist/owner-channel/mcp.js +0 -145
- package/dist/session/activity.d.ts +0 -31
- package/dist/session/activity.js +0 -48
package/dist/web-app/index.html
CHANGED
|
@@ -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-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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((
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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 =
|
|
177
|
-
if (
|
|
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
|
-
|
|
184
|
-
let archivePath =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 (
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
throw error;
|
|
74
|
+
catch (e) {
|
|
75
|
+
if (e.code !== 'EEXIST')
|
|
76
|
+
throw e;
|
|
206
77
|
}
|
|
207
78
|
}
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.19.0-nightly.2",
|
|
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",
|
package/dist/model-env.d.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { ResolvedRole } from './config.js';
|
|
2
|
-
/**
|
|
3
|
-
* Which environment variable a harness reads to pin the model it RUNS.
|
|
4
|
-
*
|
|
5
|
-
* This is not a convenience: for `claude-code` it is the only channel that
|
|
6
|
-
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
7
|
-
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
8
|
-
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
9
|
-
* `settings.model`, then a resumed session's live model, then its first
|
|
10
|
-
* catalogue entry. A role's declared model was therefore invisible to every
|
|
11
|
-
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
12
|
-
* an explicitly requested one.
|
|
13
|
-
*/
|
|
14
|
-
export declare const MODEL_ENV_BY_HARNESS: Readonly<Record<string, string>>;
|
|
15
|
-
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
16
|
-
export declare function modelEnvVar(harness: string | undefined): string | undefined;
|
|
17
|
-
export interface RoleModelEnvInput {
|
|
18
|
-
harness: string;
|
|
19
|
-
/** Already resolved by `resolveRoleModel` — may come from the fleet default. */
|
|
20
|
-
model: string | undefined;
|
|
21
|
-
/** True when the role (or `--model`) named a model, including `model: null`. */
|
|
22
|
-
modelWasExplicit: boolean;
|
|
23
|
-
defaultsEnv?: Record<string, string>;
|
|
24
|
-
roleEnv?: Record<string, string>;
|
|
25
|
-
authProxyBaseUrl?: string;
|
|
26
|
-
}
|
|
27
|
-
export interface RoleModelEnv {
|
|
28
|
-
env: Record<string, string>;
|
|
29
|
-
/**
|
|
30
|
-
* The model the harness will actually run. Equal to `env[pin]` for a harness
|
|
31
|
-
* that pins by env, so anything reporting this value reports the runtime.
|
|
32
|
-
*/
|
|
33
|
-
model: string | undefined;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
37
|
-
* never disagree.
|
|
38
|
-
*
|
|
39
|
-
* Precedence, highest first:
|
|
40
|
-
* 1. an explicit `model:` / `--model` on the role
|
|
41
|
-
* 2. the role's own `env:` pin
|
|
42
|
-
* 3. the fleet `defaults.model`
|
|
43
|
-
* 4. the fleet `defaults.env` pin
|
|
44
|
-
*
|
|
45
|
-
* Inheriting the fleet default remains correct when the role names no model
|
|
46
|
-
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
47
|
-
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
48
|
-
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
49
|
-
*
|
|
50
|
-
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
51
|
-
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
52
|
-
*/
|
|
53
|
-
export declare function resolveRoleModelEnv(input: RoleModelEnvInput, describe?: (message: string) => Error): RoleModelEnv;
|
|
54
|
-
/**
|
|
55
|
-
* The model a role will actually run, read back from the environment it was
|
|
56
|
-
* resolved with. Use this wherever a model is reported to a human.
|
|
57
|
-
*/
|
|
58
|
-
export declare function effectiveRoleModel(role: ResolvedRole): string | undefined;
|
|
59
|
-
/**
|
|
60
|
-
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
61
|
-
* role runs after resolution — model-chain recovery is the live example — must
|
|
62
|
-
* go through this, or it changes only the label.
|
|
63
|
-
*/
|
|
64
|
-
export declare function repinModelEnv(role: ResolvedRole, model: string | undefined): Record<string, string> | undefined;
|
|
65
|
-
/**
|
|
66
|
-
* Last line of defence, at the exact point a child's environment is composed:
|
|
67
|
-
* refuse to launch a role whose child would run a model other than the one the
|
|
68
|
-
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
69
|
-
* agreement must fail loudly, not start and be believed.
|
|
70
|
-
*/
|
|
71
|
-
export declare function assertModelPinReachesChild(role: ResolvedRole, childEnv: Record<string, string | undefined>): void;
|
package/dist/model-env.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which environment variable a harness reads to pin the model it RUNS.
|
|
3
|
-
*
|
|
4
|
-
* This is not a convenience: for `claude-code` it is the only channel that
|
|
5
|
-
* reaches the ACP backend at all. `buildLaunch` (tmux) passes `--model`, but
|
|
6
|
-
* `buildAcpLaunch` launches the ACP adapter with no model argument, and that
|
|
7
|
-
* adapter resolves its model in this order — ANTHROPIC_MODEL, then
|
|
8
|
-
* `settings.model`, then a resumed session's live model, then its first
|
|
9
|
-
* catalogue entry. A role's declared model was therefore invisible to every
|
|
10
|
-
* ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
|
|
11
|
-
* an explicitly requested one.
|
|
12
|
-
*/
|
|
13
|
-
export const MODEL_ENV_BY_HARNESS = {
|
|
14
|
-
'claude-code': 'ANTHROPIC_MODEL',
|
|
15
|
-
};
|
|
16
|
-
/** The model-pin variable for a harness, or undefined if it pins no model by env. */
|
|
17
|
-
export function modelEnvVar(harness) {
|
|
18
|
-
return harness === undefined ? undefined : MODEL_ENV_BY_HARNESS[harness];
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Resolve a role's environment and its runtime model TOGETHER, so the two can
|
|
22
|
-
* never disagree.
|
|
23
|
-
*
|
|
24
|
-
* Precedence, highest first:
|
|
25
|
-
* 1. an explicit `model:` / `--model` on the role
|
|
26
|
-
* 2. the role's own `env:` pin
|
|
27
|
-
* 3. the fleet `defaults.model`
|
|
28
|
-
* 4. the fleet `defaults.env` pin
|
|
29
|
-
*
|
|
30
|
-
* Inheriting the fleet default remains correct when the role names no model
|
|
31
|
-
* (2, 3, 4); an explicitly named one wins (1). Where both are explicit and they
|
|
32
|
-
* disagree, there is no defensible winner, so this refuses rather than picking
|
|
33
|
-
* one silently — the silence is what let a day of "Fable" work run on Opus.
|
|
34
|
-
*
|
|
35
|
-
* `model: null` explicitly asks for no fleet-chosen model, so it also clears an
|
|
36
|
-
* inherited pin instead of leaving one in place to act as a hidden default.
|
|
37
|
-
*/
|
|
38
|
-
export function resolveRoleModelEnv(input, describe = message => new Error(message)) {
|
|
39
|
-
const env = {
|
|
40
|
-
...(input.defaultsEnv ?? {}),
|
|
41
|
-
...(input.roleEnv ?? {}),
|
|
42
|
-
...(input.authProxyBaseUrl ? { ANTHROPIC_BASE_URL: input.authProxyBaseUrl } : {}),
|
|
43
|
-
};
|
|
44
|
-
const pin = modelEnvVar(input.harness);
|
|
45
|
-
if (!pin)
|
|
46
|
-
return { env, model: input.model };
|
|
47
|
-
const rolePin = input.roleEnv?.[pin];
|
|
48
|
-
if (input.modelWasExplicit) {
|
|
49
|
-
if (rolePin !== undefined && rolePin !== input.model)
|
|
50
|
-
throw describe(`model '${input.model ?? '(none)'}' contradicts env.${pin} '${rolePin}'; `
|
|
51
|
-
+ `remove one — ${pin} is what the harness actually runs`);
|
|
52
|
-
if (input.model === undefined)
|
|
53
|
-
delete env[pin];
|
|
54
|
-
else
|
|
55
|
-
env[pin] = input.model;
|
|
56
|
-
return { env, model: input.model };
|
|
57
|
-
}
|
|
58
|
-
// Not explicit: a role-level pin is the most specific thing said about this
|
|
59
|
-
// role, so it decides — and the reported model follows it.
|
|
60
|
-
if (rolePin !== undefined)
|
|
61
|
-
return { env, model: rolePin };
|
|
62
|
-
if (input.model !== undefined)
|
|
63
|
-
env[pin] = input.model;
|
|
64
|
-
return { env, model: input.model ?? env[pin] };
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* The model a role will actually run, read back from the environment it was
|
|
68
|
-
* resolved with. Use this wherever a model is reported to a human.
|
|
69
|
-
*/
|
|
70
|
-
export function effectiveRoleModel(role) {
|
|
71
|
-
const pin = modelEnvVar(role.harness);
|
|
72
|
-
return (pin ? role.env?.[pin] : undefined) ?? role.model;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Move a role's env pin onto a new model. Anything that changes the model a
|
|
76
|
-
* role runs after resolution — model-chain recovery is the live example — must
|
|
77
|
-
* go through this, or it changes only the label.
|
|
78
|
-
*/
|
|
79
|
-
export function repinModelEnv(role, model) {
|
|
80
|
-
const pin = modelEnvVar(role.harness);
|
|
81
|
-
if (!pin)
|
|
82
|
-
return role.env;
|
|
83
|
-
const env = { ...(role.env ?? {}) };
|
|
84
|
-
if (model === undefined)
|
|
85
|
-
delete env[pin];
|
|
86
|
-
else
|
|
87
|
-
env[pin] = model;
|
|
88
|
-
return Object.keys(env).length ? env : undefined;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Last line of defence, at the exact point a child's environment is composed:
|
|
92
|
-
* refuse to launch a role whose child would run a model other than the one the
|
|
93
|
-
* role declares and the banner reports. A spawn that cannot keep those two in
|
|
94
|
-
* agreement must fail loudly, not start and be believed.
|
|
95
|
-
*/
|
|
96
|
-
export function assertModelPinReachesChild(role, childEnv) {
|
|
97
|
-
const pin = modelEnvVar(role.harness);
|
|
98
|
-
if (!pin || role.model === undefined)
|
|
99
|
-
return;
|
|
100
|
-
const actual = childEnv[pin];
|
|
101
|
-
if (actual === role.model)
|
|
102
|
-
return;
|
|
103
|
-
throw new Error(`[${role.name}] refusing to launch: role model is '${role.model}' but the child's `
|
|
104
|
-
+ `${pin} is ${actual === undefined ? 'unset' : `'${actual}'`} — the session would run a `
|
|
105
|
-
+ 'different model than the one reported');
|
|
106
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export declare class OursMcpError extends Error {
|
|
2
|
-
}
|
|
3
|
-
export interface OursToolClient {
|
|
4
|
-
start(): Promise<void>;
|
|
5
|
-
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
6
|
-
close(): Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
/** Minimal MCP stdio client. One instance owns exactly one ours identity binding. */
|
|
9
|
-
export declare class OursMcpClient implements OursToolClient {
|
|
10
|
-
private readonly command;
|
|
11
|
-
private readonly env;
|
|
12
|
-
private readonly log;
|
|
13
|
-
private child?;
|
|
14
|
-
private nextId;
|
|
15
|
-
private tail;
|
|
16
|
-
constructor(command?: string, env?: Record<string, string>, log?: (line: string) => void);
|
|
17
|
-
start(): Promise<void>;
|
|
18
|
-
callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
|
|
19
|
-
close(): Promise<void>;
|
|
20
|
-
private request;
|
|
21
|
-
private requestNow;
|
|
22
|
-
private notify;
|
|
23
|
-
private write;
|
|
24
|
-
}
|