borgmcp 2.5.0 → 2.6.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 +12 -7
- package/dist/assimilate-cmd.d.ts +1 -0
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +58 -30
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +2 -0
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/claude.js +9 -3
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +7 -4
- package/dist/cli-help.js.map +1 -1
- package/dist/codex-launch.d.ts +2 -2
- package/dist/codex-launch.d.ts.map +1 -1
- package/dist/codex-launch.js +7 -7
- package/dist/codex-launch.js.map +1 -1
- package/dist/first-run-server.d.ts +31 -0
- package/dist/first-run-server.d.ts.map +1 -0
- package/dist/first-run-server.js +117 -0
- package/dist/first-run-server.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -27
- package/dist/index.js.map +1 -1
- package/dist/log-stream.d.ts +4 -8
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +22 -15
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +11 -5
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +236 -48
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-wake-copy.d.ts +2 -0
- package/dist/opencode-wake-copy.d.ts.map +1 -0
- package/dist/opencode-wake-copy.js +8 -0
- package/dist/opencode-wake-copy.js.map +1 -0
- package/dist/regen-format.d.ts +4 -6
- package/dist/regen-format.d.ts.map +1 -1
- package/dist/regen-format.js +10 -14
- package/dist/regen-format.js.map +1 -1
- package/dist/repository-cube-init.d.ts +2 -0
- package/dist/repository-cube-init.d.ts.map +1 -1
- package/dist/repository-cube-init.js +14 -3
- package/dist/repository-cube-init.js.map +1 -1
- package/dist/server-facade.d.ts +2 -0
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +25 -3
- package/dist/server-facade.js.map +1 -1
- package/dist/setup.js +10 -0
- package/dist/setup.js.map +1 -1
- package/dist/stream-status.d.ts +9 -8
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +68 -18
- package/dist/stream-status.js.map +1 -1
- package/dist/tool-manifest.js +2 -2
- package/dist/tool-manifest.js.map +1 -1
- package/dist/update-cmd.d.ts +11 -1
- package/dist/update-cmd.d.ts.map +1 -1
- package/dist/update-cmd.js +68 -4
- package/dist/update-cmd.js.map +1 -1
- package/dist/wake-path-health.d.ts +27 -0
- package/dist/wake-path-health.d.ts.map +1 -0
- package/dist/wake-path-health.js +51 -0
- package/dist/wake-path-health.js.map +1 -0
- package/docs/EXTRACTION_PROVENANCE.md +3 -3
- package/docs/LOCAL_SERVER.md +12 -3
- package/docs/RELEASING.md +11 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +66 -27
- package/src/assimilate-deps.ts +9 -0
- package/src/claude.ts +10 -3
- package/src/cli-help.ts +7 -4
- package/src/codex-launch.ts +7 -7
- package/src/first-run-server.ts +178 -0
- package/src/index.ts +24 -28
- package/src/log-stream.ts +36 -25
- package/src/opencode-drone.ts +283 -47
- package/src/opencode-wake-copy.ts +8 -0
- package/src/regen-format.ts +10 -16
- package/src/repository-cube-init.ts +16 -3
- package/src/server-facade.ts +34 -3
- package/src/setup.ts +11 -0
- package/src/stream-status.ts +86 -22
- package/src/tool-manifest.ts +2 -2
- package/src/update-cmd.ts +88 -5
- package/src/wake-path-health.ts +85 -0
package/src/regen-format.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from './roster-render.js';
|
|
19
19
|
import { shellEscape } from './shell-escape.js';
|
|
20
20
|
import { resolveInboxMonitorPath } from './self-path.js';
|
|
21
|
+
import { OPENCODE_WAKE_PATH_GUIDANCE } from './opencode-wake-copy.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Extract the SessionStart `source` from a Claude Code hook payload (gh#926).
|
|
@@ -26,9 +27,8 @@ import { resolveInboxMonitorPath } from './self-path.js';
|
|
|
26
27
|
* one of `startup` / `resume` / `clear` / `compact`. The `borg-regen`
|
|
27
28
|
* SessionStart hook uses this to detect a `/clear` re-orientation, which is
|
|
28
29
|
* the FIRST time the hook is the SOLE orientation path (the launch kickoff
|
|
29
|
-
* prompt is gone)
|
|
30
|
-
*
|
|
31
|
-
* an operational re-arm.
|
|
30
|
+
* prompt is gone), so the re-injected orientation must instruct an operational
|
|
31
|
+
* Monitor re-arm.
|
|
32
32
|
*
|
|
33
33
|
* Best-effort + total: empty input (manual / TTY run with no stdin),
|
|
34
34
|
* malformed JSON, a missing `source`, or a non-string `source` all return
|
|
@@ -59,9 +59,8 @@ export type AgentKind = 'claude' | 'codex' | 'opencode';
|
|
|
59
59
|
*
|
|
60
60
|
* Agent-branched on the existing env-agnostic signal (BORG_SESSION-style
|
|
61
61
|
* `isCodexRemoteWakeEnabled`), NOT on a mutable server-recorded field:
|
|
62
|
-
* - claude: arm the inbox-file tail Monitor
|
|
63
|
-
*
|
|
64
|
-
* healthy or indeterminate; short only while explicitly broken).
|
|
62
|
+
* - claude: arm the inbox-file tail Monitor, drain unread entries on every
|
|
63
|
+
* wake, and re-arm the Monitor if it exits or is missing.
|
|
65
64
|
* - codex: Borg's activity stream reaches the app-server remote-control inbox
|
|
66
65
|
* channel; each wake is followed by an unread-log drain. Manual full regen
|
|
67
66
|
* + drain is a degraded fallback when remote control is unavailable.
|
|
@@ -85,11 +84,7 @@ export function wakePathArming(
|
|
|
85
84
|
].join(' ');
|
|
86
85
|
}
|
|
87
86
|
if (agentKind === 'opencode') {
|
|
88
|
-
return
|
|
89
|
-
'Wake path: OpenCode wakes via SDK-driven entry injection into the TUI session — ' +
|
|
90
|
-
'there is no tail-Monitor or `/loop` heartbeat to arm. Cube posts are delivered ' +
|
|
91
|
-
'directly into your active session. Use \`borg_regen\` at any time for fresh context.'
|
|
92
|
-
);
|
|
87
|
+
return OPENCODE_WAKE_PATH_GUIDANCE;
|
|
93
88
|
}
|
|
94
89
|
// gh#client#18: use absolute path to THIS installation's borg-inbox-monitor
|
|
95
90
|
// so the orientation command always resolves to the same version as the
|
|
@@ -101,9 +96,8 @@ export function wakePathArming(
|
|
|
101
96
|
return [
|
|
102
97
|
'Arm your wake path before working:',
|
|
103
98
|
`1. **Inbox Monitor** (wake path) — run a persistent Monitor on \`${monitorCommand}\` so cube posts wake you in real time.`,
|
|
104
|
-
'2. **
|
|
105
|
-
'3. **
|
|
106
|
-
'4. **Recovery tick** — drain `borg_read-log unread_only=true`. If empty, check wake status, set deadline, resume prior work. No full-regen or liveness post on empty; safety probes may still wake.',
|
|
99
|
+
'2. **On every wake** — drain `borg_read-log unread_only=true`. If empty, resume prior work without a full regen or liveness post; safety probes may still wake.',
|
|
100
|
+
'3. **Monitor recovery** — if the Monitor exits or is missing, re-arm it when the exit notification wakes you.',
|
|
107
101
|
].join('\n');
|
|
108
102
|
}
|
|
109
103
|
|
|
@@ -168,8 +162,8 @@ export function formatLeanOrientation(args: {
|
|
|
168
162
|
? '\n_(`/clear` cleared your conversation; Codex remote-control wake remains active. Follow the required Codex wake path below.)_\n'
|
|
169
163
|
: agentKind === 'claude'
|
|
170
164
|
? [
|
|
171
|
-
'\n_(`/clear` cleared Claude\'s
|
|
172
|
-
'_Quiet-clear fallback: if a later turn follows silence, inspect `borg_stream-status` + `borg_roster`; call `borg_regen mode="full"`, drain `borg_read-log unread_only=true`, then re-arm
|
|
165
|
+
'\n_(`/clear` cleared Claude\'s conversation — re-arm the inbox Monitor now.)_',
|
|
166
|
+
'_Quiet-clear fallback: if a later turn follows silence, inspect `borg_stream-status` + `borg_roster`; call `borg_regen mode="full"`, drain `borg_read-log unread_only=true`, then re-arm the Monitor._\n',
|
|
173
167
|
].join('\n')
|
|
174
168
|
: ''
|
|
175
169
|
: '';
|
|
@@ -46,6 +46,8 @@ export interface RepositoryCubeInitDeps {
|
|
|
46
46
|
isTTY(): boolean;
|
|
47
47
|
prompt(message: string): Promise<string>;
|
|
48
48
|
write(text: string): void;
|
|
49
|
+
writeResult?(text: string): void;
|
|
50
|
+
useColor?(): boolean;
|
|
49
51
|
getIdentity(context: GitRepositoryContext): Promise<CreateCubeRepository>;
|
|
50
52
|
getAssociation(repository: CreateCubeRepository): Promise<RepositoryAssociation | null>;
|
|
51
53
|
saveAssociation(repository: CreateCubeRepository, association: RepositoryAssociation): Promise<void>;
|
|
@@ -132,8 +134,16 @@ function renderResult(
|
|
|
132
134
|
adopted?: boolean;
|
|
133
135
|
},
|
|
134
136
|
): void {
|
|
137
|
+
const resultWriter = deps.writeResult ?? deps.write;
|
|
138
|
+
const heading = input.adopted
|
|
139
|
+
? 'Existing cube associated with this repository.'
|
|
140
|
+
: input.existing
|
|
141
|
+
? 'Cube already initialized.'
|
|
142
|
+
: 'Cube created.';
|
|
135
143
|
const lines = [
|
|
136
|
-
input.
|
|
144
|
+
input.mode === 'cube-init'
|
|
145
|
+
? `${deps.useColor?.() ? '\u001b[32m✓\u001b[0m' : '✓'} ${heading}`
|
|
146
|
+
: heading,
|
|
137
147
|
` Name: ${input.response.name}`,
|
|
138
148
|
` Template: ${presentation(input.response.template).label}`,
|
|
139
149
|
` Repository: ${input.root}`,
|
|
@@ -150,7 +160,7 @@ function renderResult(
|
|
|
150
160
|
} else {
|
|
151
161
|
lines.push('Continuing with role and seat setup...');
|
|
152
162
|
}
|
|
153
|
-
|
|
163
|
+
resultWriter(`${lines.join('\n')}\n`);
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
export async function initializeRepositoryCube(input: {
|
|
@@ -300,8 +310,11 @@ export async function initializeRepositoryCube(input: {
|
|
|
300
310
|
if (proposedAdoption) return proposedAdoption;
|
|
301
311
|
|
|
302
312
|
if (input.canCreate === false) {
|
|
313
|
+
const retryCommand = input.mode === 'cube-init'
|
|
314
|
+
? `borg server cube init --host ${shellEscape(input.serverOrigin)}`
|
|
315
|
+
: `borg assimilate --host ${shellEscape(input.serverOrigin)}`;
|
|
303
316
|
deps.write(
|
|
304
|
-
`This enrolled client cannot create a cube on ${input.serverOrigin}. Ask the server operator to grant access to a cube, then rerun
|
|
317
|
+
`This enrolled client cannot create a cube on ${input.serverOrigin}. Ask the server operator to grant access to a cube, then rerun ${retryCommand}.\n`,
|
|
305
318
|
);
|
|
306
319
|
return { kind: 'stop', code: 1 };
|
|
307
320
|
}
|
package/src/server-facade.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { spawn as spawnChild, type SpawnOptions } from 'node:child_process';
|
|
2
2
|
import { constants } from 'node:os';
|
|
3
|
+
import chalk from 'chalk';
|
|
3
4
|
import { cubeInitHelpText, isHelpFlag, serverHelpText } from './cli-help.js';
|
|
5
|
+
import { consolePrefix } from './console-prefix.js';
|
|
6
|
+
import { getPackageVersion } from './version.js';
|
|
4
7
|
|
|
5
8
|
export const SERVER_LIFECYCLE_COMMANDS = ['setup', 'start', 'stop', 'status', 'update', 'invite', 'dashboard'] as const;
|
|
6
9
|
export type ServerLifecycleCommand = typeof SERVER_LIFECYCLE_COMMANDS[number];
|
|
@@ -12,6 +15,10 @@ export type ParsedServerFacadeArgs =
|
|
|
12
15
|
| { kind: 'command'; command: ServerLifecycleCommand; args: string[] }
|
|
13
16
|
| { kind: 'error'; reason: 'unknown-command'; command: string };
|
|
14
17
|
|
|
18
|
+
export function isClientOwnedCubeInitArgv(argv: readonly string[]): boolean {
|
|
19
|
+
return argv[2] === 'server' && argv[3] === 'cube' && argv[4] === 'init';
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export function parseServerFacadeArgs(args: readonly string[]): ParsedServerFacadeArgs {
|
|
16
23
|
const [command, ...rest] = args;
|
|
17
24
|
if (command === undefined || command === '--help' || command === '-h') {
|
|
@@ -92,8 +99,9 @@ export function buildDefaultServerFacadeClientDeps(
|
|
|
92
99
|
import('./assimilate-cmd.js'),
|
|
93
100
|
]);
|
|
94
101
|
const parsed = parseAssimilateArgs([...args]);
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
const unsupported = parsed.ok ? unsupportedCubeInitInput(args, parsed) : undefined;
|
|
103
|
+
if (!parsed.ok || unsupported !== undefined) {
|
|
104
|
+
process.stderr.write(cubeInitUsageErrorText(parsed.ok ? unsupported! : parsed.error));
|
|
97
105
|
return 1;
|
|
98
106
|
}
|
|
99
107
|
return runAssimilate(
|
|
@@ -104,6 +112,29 @@ export function buildDefaultServerFacadeClientDeps(
|
|
|
104
112
|
};
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
function unsupportedCubeInitInput(
|
|
116
|
+
args: readonly string[],
|
|
117
|
+
parsed: { role: string | undefined },
|
|
118
|
+
): string | undefined {
|
|
119
|
+
if (parsed.role !== undefined) {
|
|
120
|
+
return 'A role is not accepted because this command does not create a drone.';
|
|
121
|
+
}
|
|
122
|
+
const unsupported = ['--worktree', '--here', '--force', '--cli', '--model', '--backend', '--no-template'];
|
|
123
|
+
const flag = args.find((arg) => unsupported.some((candidate) =>
|
|
124
|
+
arg === candidate || arg.startsWith(`${candidate}=`)));
|
|
125
|
+
return flag === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: `${flag.split('=', 1)[0]} is not accepted because this command does not create a drone.`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function cubeInitUsageErrorText(reason: string): string {
|
|
131
|
+
const sentence = /[.!?]$/.test(reason) ? reason : `${reason}.`;
|
|
132
|
+
return (
|
|
133
|
+
chalk.red(`${consolePrefix()}◼ borg server cube init: ${sentence}\n`) +
|
|
134
|
+
'Run `borg server cube init --help` for usage.\n'
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
const defaultClientDeps = buildDefaultServerFacadeClientDeps();
|
|
108
139
|
|
|
109
140
|
const MAX_RENDERED_COMMAND_CODE_POINTS = 80;
|
|
@@ -222,7 +253,7 @@ export async function runEarlyServerFacade(
|
|
|
222
253
|
return 0;
|
|
223
254
|
}
|
|
224
255
|
if (parsed.kind === 'cube-init-help') {
|
|
225
|
-
output.writeStdout(cubeInitHelpText());
|
|
256
|
+
output.writeStdout(cubeInitHelpText(getPackageVersion()));
|
|
226
257
|
return 0;
|
|
227
258
|
}
|
|
228
259
|
if (parsed.kind === 'error') {
|
package/src/setup.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { ensureCliMcpConfigured } from './ensure-mcp-config.js';
|
|
|
37
37
|
import { handleVersionFlag } from './version.js';
|
|
38
38
|
import { initDebugFromArgv } from './debug.js';
|
|
39
39
|
import { defaultApprovalIo, setupApprovalWarnings } from './cli-tool-approval.js';
|
|
40
|
+
import { offerFirstRunServerInstall } from './first-run-server.js';
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Main setup wizard
|
|
@@ -67,6 +68,16 @@ async function main() {
|
|
|
67
68
|
process.exit(1);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// Resolve the separately published local server before setup writes agent
|
|
72
|
+
// configuration. Decline/non-interactive/failure paths therefore leave no
|
|
73
|
+
// partial setup state behind.
|
|
74
|
+
console.log(chalk.blue('◼ Local Server'));
|
|
75
|
+
const serverInstall = await offerFirstRunServerInstall();
|
|
76
|
+
if (serverInstall.kind !== 'present' && serverInstall.kind !== 'installed') {
|
|
77
|
+
process.exit(serverInstall.kind === 'declined' ? 0 : 1);
|
|
78
|
+
}
|
|
79
|
+
console.log('');
|
|
80
|
+
|
|
70
81
|
// Step 1: Configure every detected agent CLI
|
|
71
82
|
console.log(chalk.blue('◼ Agent CLI Integration'));
|
|
72
83
|
|
package/src/stream-status.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
HEARTBEAT_STALE_MS,
|
|
29
29
|
} from './inbox-monitor.js';
|
|
30
30
|
import { shellEscape } from './shell-escape.js';
|
|
31
|
+
import type { WakePathSnapshot } from './wake-path-health.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Best-effort check: is a process tailing this inbox file?
|
|
@@ -111,11 +112,10 @@ export function isHeartbeatStale(inboxPath: string, monitorStateRoot?: string |
|
|
|
111
112
|
|
|
112
113
|
export interface RenderInputs {
|
|
113
114
|
status: StreamStatus;
|
|
114
|
-
/**
|
|
115
|
-
* Tri-state Monitor liveness: true = healthy, false = wake-path
|
|
116
|
-
* broken, null = cannot determine.
|
|
117
|
-
*/
|
|
115
|
+
/** Legacy Claude Monitor health; runtime-aware callers also pass `wakePath`. */
|
|
118
116
|
inboxMonitorHealthy: boolean | null;
|
|
117
|
+
/** Runtime-specific wake-path evidence. Omitted by legacy pure-render callers. */
|
|
118
|
+
wakePath?: WakePathSnapshot;
|
|
119
119
|
/**
|
|
120
120
|
* Inbox path for the State-5 self-arm instruction. Pass null when
|
|
121
121
|
* unknown (no active cube); State 5 will then surface the failure
|
|
@@ -140,6 +140,12 @@ export interface RenderInputs {
|
|
|
140
140
|
export function renderStreamStatus(inputs: RenderInputs): string {
|
|
141
141
|
const { status, inboxMonitorHealthy, inboxPath, monitorStateRoot, droneLabel, cubeName, humanAgo } =
|
|
142
142
|
inputs;
|
|
143
|
+
const wakePath = inputs.wakePath ?? {
|
|
144
|
+
agentKind: 'claude' as const,
|
|
145
|
+
healthy: inboxMonitorHealthy,
|
|
146
|
+
openCode: null,
|
|
147
|
+
};
|
|
148
|
+
const wakePathHealthy = wakePath.healthy;
|
|
143
149
|
|
|
144
150
|
const isNotStarted =
|
|
145
151
|
status.reconnectAttempts === 0 &&
|
|
@@ -149,9 +155,8 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
149
155
|
const orphanedInitialization = status.ownership?.state === 'orphaned-initialization';
|
|
150
156
|
const ownershipInitializing = status.ownership?.state === 'initializing';
|
|
151
157
|
|
|
152
|
-
// Top-line verdict — 5 states +
|
|
153
|
-
// Precedence: disconnected >
|
|
154
|
-
// cause; State 5 only applies when wire is healthy).
|
|
158
|
+
// Top-line verdict — 5 states + runtime wake-path override.
|
|
159
|
+
// Precedence: disconnected > wake-path failure.
|
|
155
160
|
let summary: string;
|
|
156
161
|
if (orphanedInitialization) {
|
|
157
162
|
summary = '**Stream blocked by an orphaned initialization lock.**';
|
|
@@ -161,8 +166,12 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
161
166
|
summary = '**Stream not started.**';
|
|
162
167
|
} else if (!status.connected) {
|
|
163
168
|
summary = `**Stream disconnected (reconnect attempt ${status.reconnectAttempts}).**`;
|
|
164
|
-
} else if (
|
|
165
|
-
summary =
|
|
169
|
+
} else if (wakePathHealthy === false) {
|
|
170
|
+
summary = wakePath.agentKind === 'opencode'
|
|
171
|
+
? '**Stream connected (OpenCode delivery degraded).**'
|
|
172
|
+
: wakePath.agentKind === 'codex'
|
|
173
|
+
? '**Stream connected (Codex wake path unavailable).**'
|
|
174
|
+
: '**Stream connected (no inbox-Monitor — wake path broken).**';
|
|
166
175
|
} else if (status.lastContentEventAt === null) {
|
|
167
176
|
// State 2: wire works, no content yet. Collapses two underlying
|
|
168
177
|
// conditions per drone-4 contract — fresh connect pre-first-content
|
|
@@ -247,13 +256,42 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
247
256
|
);
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
if (wakePath.agentKind === 'opencode' && wakePath.openCode) {
|
|
260
|
+
const delivery = wakePath.openCode.deliveryStates;
|
|
261
|
+
lines.push(`- **OpenCode delivery connected**: ${wakePath.openCode.connected}`);
|
|
262
|
+
lines.push(`- **OpenCode queued**: ${delivery.queued}`);
|
|
263
|
+
lines.push(
|
|
264
|
+
`- **OpenCode delivered-unconfirmed**: ${delivery['delivered-unconfirmed']}`
|
|
265
|
+
);
|
|
266
|
+
lines.push(`- **OpenCode retried**: ${delivery.retried}`);
|
|
267
|
+
lines.push(`- **OpenCode failed**: ${delivery.failed}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Runtime-specific wake-path warning. The wire-down case takes
|
|
271
|
+
// precedence above; an indeterminate signal remains honest and silent.
|
|
272
|
+
if (status.connected && wakePathHealthy === false) {
|
|
273
|
+
if (wakePath.agentKind === 'opencode') {
|
|
274
|
+
lines.push(
|
|
275
|
+
'- **OpenCode delivery**: _(degraded — inspect the durable unread log)_'
|
|
276
|
+
);
|
|
277
|
+
lines.push('');
|
|
278
|
+
lines.push('## OpenCode wake delivery is degraded');
|
|
279
|
+
lines.push(
|
|
280
|
+
'One or more durable inbox entries were rejected or could not be confirmed. Run `borg_read-log unread_only=true` and drain the unread log now. Use `borg_stream-status` to check whether failed or delivered-unconfirmed entries remain.'
|
|
281
|
+
);
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
}
|
|
284
|
+
if (wakePath.agentKind === 'codex') {
|
|
285
|
+
lines.push(
|
|
286
|
+
'- **Codex wake path**: _(remote-control bridge unavailable)_'
|
|
287
|
+
);
|
|
288
|
+
lines.push('');
|
|
289
|
+
lines.push('## Codex wake delivery is unavailable');
|
|
290
|
+
lines.push(
|
|
291
|
+
'The Codex remote-control bridge cannot currently deliver a wake. Run `borg_read-log unread_only=true` and drain the unread log now, then reconnect the Codex wake path.'
|
|
292
|
+
);
|
|
293
|
+
return lines.join('\n');
|
|
294
|
+
}
|
|
257
295
|
lines.push(
|
|
258
296
|
`- **inbox-monitor**: _(no watcher detected — wake path broken)_`
|
|
259
297
|
);
|
|
@@ -278,13 +316,13 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
278
316
|
* from the inline ternary in `src/index.ts` for direct unit-test
|
|
279
317
|
* coverage of the (connected × healthy) cross-product).
|
|
280
318
|
*
|
|
281
|
-
* Returns true ONLY when the wire is up AND
|
|
282
|
-
*
|
|
319
|
+
* Returns true ONLY when the wire is up AND the runtime-specific wake
|
|
320
|
+
* mechanism is positively unhealthy (`=== false` strict). The `null` branch
|
|
283
321
|
* (couldn't determine) stays silent — surfacing an uncertain failure
|
|
284
322
|
* mode is worse UX than omitting it (mirrors the State-5 precedence
|
|
285
323
|
* rule in `renderStreamStatus`). When disconnected, the wire-down case
|
|
286
324
|
* is the upstream cause and takes precedence; no point warning about
|
|
287
|
-
* the wake path when
|
|
325
|
+
* the wake path when its input has no events to deliver.
|
|
288
326
|
*/
|
|
289
327
|
export function shouldShowWakePathWarning(
|
|
290
328
|
streamStatus: StreamStatus,
|
|
@@ -294,7 +332,7 @@ export function shouldShowWakePathWarning(
|
|
|
294
332
|
}
|
|
295
333
|
|
|
296
334
|
/**
|
|
297
|
-
*
|
|
335
|
+
* Runtime-specific wake-path-broken prefix for `borg_regen` output.
|
|
298
336
|
*
|
|
299
337
|
* Pure function — caller decides whether to call (gates on
|
|
300
338
|
* `shouldShowWakePathWarning`). Returns an empty string when called
|
|
@@ -315,13 +353,39 @@ export function formatWakePathPrefix(inputs: {
|
|
|
315
353
|
monitorStateRoot?: string | null;
|
|
316
354
|
droneLabel: string | null;
|
|
317
355
|
cubeName: string | null;
|
|
356
|
+
wakePath?: WakePathSnapshot;
|
|
318
357
|
}): string {
|
|
319
|
-
const { inboxPath, monitorStateRoot, droneLabel, cubeName } = inputs;
|
|
358
|
+
const { inboxPath, monitorStateRoot, droneLabel, cubeName, wakePath } = inputs;
|
|
359
|
+
if (wakePath?.agentKind === 'opencode') {
|
|
360
|
+
const delivery = wakePath.openCode?.deliveryStates;
|
|
361
|
+
return [
|
|
362
|
+
'## ⚠ OpenCode wake delivery degraded',
|
|
363
|
+
'',
|
|
364
|
+
`Durable delivery has ${
|
|
365
|
+
delivery
|
|
366
|
+
? `${delivery['delivered-unconfirmed']} delivered-unconfirmed and ${delivery.failed} failed entries`
|
|
367
|
+
: 'an unhealthy runtime signal'
|
|
368
|
+
}. Run \`borg_stream-status\`, then run \`borg_read-log unread_only=true\` and drain the unread log.`,
|
|
369
|
+
'',
|
|
370
|
+
'---',
|
|
371
|
+
'',
|
|
372
|
+
].join('\n');
|
|
373
|
+
}
|
|
374
|
+
if (wakePath?.agentKind === 'codex') {
|
|
375
|
+
return [
|
|
376
|
+
'## ⚠ Codex wake path unavailable',
|
|
377
|
+
'',
|
|
378
|
+
'The remote-control bridge cannot currently deliver a wake. Run `borg_read-log unread_only=true` and drain the unread log, then reconnect the Codex wake path.',
|
|
379
|
+
'',
|
|
380
|
+
'---',
|
|
381
|
+
'',
|
|
382
|
+
].join('\n');
|
|
383
|
+
}
|
|
320
384
|
if (!inboxPath || !droneLabel || !cubeName) return '';
|
|
321
385
|
return [
|
|
322
386
|
`## ⚠ Wake path broken — arm Monitor NOW`,
|
|
323
387
|
``,
|
|
324
|
-
`No process is tailing this drone's inbox file. SSE delivery is healthy (entries reach disk), but Claude Code has no event source to wake on. Until you arm a Monitor, this session
|
|
388
|
+
`No process is tailing this drone's inbox file. SSE delivery is healthy (entries reach disk), but Claude Code has no event source to wake on. Until you arm a Monitor, this session has no wake path and will miss live coordination from other drones:`,
|
|
325
389
|
``,
|
|
326
390
|
`> Monitor command: \`${monitorCommand(inboxPath, monitorStateRoot)}\` — persistent, 1h timeout, description "borg inbox for ${droneLabel} on cube ${cubeName}".`,
|
|
327
391
|
``,
|
package/src/tool-manifest.ts
CHANGED
|
@@ -172,7 +172,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
|
|
|
172
172
|
{
|
|
173
173
|
name: 'borg_roster',
|
|
174
174
|
description:
|
|
175
|
-
"List all currently connected drones in your cube, with each drone's label, role, and last-seen time. Optional `since` argument adds a sender-side liveness column — pass either an activity_log entry id (e.g., from a dispatch you posted) or an ISO-8601 timestamp; each drone is marked `awake` if they've posted a log entry after that point, otherwise `stale-since-X`. Useful for confirming a dispatch reached its named recipients (catches the silent-wake-path-failure class where SSE delivered but the drone's
|
|
175
|
+
"List all currently connected drones in your cube, with each drone's label, role, and last-seen time. Optional `since` argument adds a sender-side liveness column — pass either an activity_log entry id (e.g., from a dispatch you posted) or an ISO-8601 timestamp; each drone is marked `awake` if they've posted a log entry after that point, otherwise `stale-since-X`. Useful for confirming a dispatch reached its named recipients (catches the silent-wake-path-failure class where SSE delivered but the drone's inbox Monitor never woke it).",
|
|
176
176
|
inputSchema: {
|
|
177
177
|
type: 'object',
|
|
178
178
|
properties: {
|
|
@@ -188,7 +188,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
|
|
|
188
188
|
{
|
|
189
189
|
name: 'borg_stream-status',
|
|
190
190
|
description:
|
|
191
|
-
"Diagnostic probe of the local SSE log-stream consumer: returns `connected`, `lastContentEventAt`, `lastWireActivityAt`, `lastHeartbeatAt`, `lastPersistedEventId`, `reconnectAttempts`, plus a wake-path check that flags if SSE is attached but no inbox
|
|
191
|
+
"Diagnostic probe of the local SSE log-stream consumer: returns `connected`, `lastContentEventAt`, `lastWireActivityAt`, `lastHeartbeatAt`, `lastPersistedEventId`, `reconnectAttempts`, plus a wake-path check that flags if SSE is attached but no inbox Monitor is watching the file (the silent failure where incoming entries reach disk but do not wake the drone). Read-only in-process state; does NOT re-open the stream. Use when troubleshooting wake-ups or verifying the stream is alive.",
|
|
192
192
|
inputSchema: {
|
|
193
193
|
type: 'object',
|
|
194
194
|
properties: {},
|
package/src/update-cmd.ts
CHANGED
|
@@ -54,7 +54,12 @@ export interface UpdateDeps {
|
|
|
54
54
|
name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE,
|
|
55
55
|
version: string,
|
|
56
56
|
): Promise<PublishedPackage>;
|
|
57
|
-
|
|
57
|
+
publishedVersions(name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE): Promise<string[]>;
|
|
58
|
+
installGlobal(
|
|
59
|
+
name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE,
|
|
60
|
+
version: string,
|
|
61
|
+
options?: { ignoreScripts?: boolean },
|
|
62
|
+
): Promise<void>;
|
|
58
63
|
reenter(binPath: string, args: readonly string[]): Promise<number>;
|
|
59
64
|
serverJson(binPath: string, command: 'update' | 'status'): Promise<unknown>;
|
|
60
65
|
verifyRunningProtocol(origin: string): Promise<void>;
|
|
@@ -150,7 +155,7 @@ function renderServerState(
|
|
|
150
155
|
);
|
|
151
156
|
}
|
|
152
157
|
|
|
153
|
-
function isExactSemver(value: unknown):
|
|
158
|
+
export function isExactSemver(value: unknown): value is string {
|
|
154
159
|
return typeof value === 'string' && EXACT_SEMVER.test(value);
|
|
155
160
|
}
|
|
156
161
|
|
|
@@ -275,6 +280,44 @@ async function publishedPair(
|
|
|
275
280
|
return { client, server };
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Resolve the exact published server that can run with an already-installed
|
|
285
|
+
* client. First-run onboarding uses this instead of inventing a second
|
|
286
|
+
* registry path or installing an unpinned `latest` spec.
|
|
287
|
+
*/
|
|
288
|
+
export async function resolveCompatibleServerTarget(
|
|
289
|
+
clientSharedVersion: string,
|
|
290
|
+
deps: Pick<UpdateDeps, 'publishedPackage' | 'publishedVersions'>,
|
|
291
|
+
): Promise<PublishedPackage> {
|
|
292
|
+
if (!isExactSemver(clientSharedVersion)) {
|
|
293
|
+
throw new Error(`installed client has an invalid ${SHARED_PACKAGE} pin`);
|
|
294
|
+
}
|
|
295
|
+
const stableVersions = (await deps.publishedVersions(SERVER_PACKAGE))
|
|
296
|
+
.filter((version) => isExactSemver(version) && !version.includes('-'))
|
|
297
|
+
.sort(compareStableSemverDescending);
|
|
298
|
+
for (const version of stableVersions) {
|
|
299
|
+
const server = await deps.publishedPackage(SERVER_PACKAGE, version);
|
|
300
|
+
validatePublishedPackage(server, SERVER_PACKAGE);
|
|
301
|
+
if (server.version !== version) {
|
|
302
|
+
throw new Error(`registry returned the wrong ${SERVER_PACKAGE} manifest version`);
|
|
303
|
+
}
|
|
304
|
+
if (server.sharedVersion === clientSharedVersion) return server;
|
|
305
|
+
}
|
|
306
|
+
throw new Error(
|
|
307
|
+
`no published ${SERVER_PACKAGE} release uses ${SHARED_PACKAGE}@${clientSharedVersion}`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function compareStableSemverDescending(left: string, right: string): number {
|
|
312
|
+
const leftParts = left.split('+', 1)[0].split('.').map((part) => BigInt(part));
|
|
313
|
+
const rightParts = right.split('+', 1)[0].split('.').map((part) => BigInt(part));
|
|
314
|
+
for (let index = 0; index < 3; index += 1) {
|
|
315
|
+
if (leftParts[index] > rightParts[index]) return -1;
|
|
316
|
+
if (leftParts[index] < rightParts[index]) return 1;
|
|
317
|
+
}
|
|
318
|
+
return right.localeCompare(left);
|
|
319
|
+
}
|
|
320
|
+
|
|
278
321
|
function assertInstalled(
|
|
279
322
|
installed: InstalledPackage,
|
|
280
323
|
published: PublishedPackage,
|
|
@@ -533,7 +576,11 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
|
|
|
533
576
|
if (client.version !== pair.client.version || client.sharedVersion !== pair.client.sharedVersion) {
|
|
534
577
|
let installedClient: InstalledPackage | null = null;
|
|
535
578
|
try {
|
|
536
|
-
await deps.installGlobal(
|
|
579
|
+
await deps.installGlobal(
|
|
580
|
+
CLIENT_PACKAGE,
|
|
581
|
+
pair.client.version,
|
|
582
|
+
{ ignoreScripts: true },
|
|
583
|
+
);
|
|
537
584
|
installedClient = await deps.currentClient();
|
|
538
585
|
assertInstalled(installedClient, pair.client);
|
|
539
586
|
const args = [
|
|
@@ -597,7 +644,11 @@ export async function runUpdate(options: UpdateOptions, deps: UpdateDeps): Promi
|
|
|
597
644
|
discoveredServer.version !== pair.server.version ||
|
|
598
645
|
discoveredServer.sharedVersion !== pair.server.sharedVersion
|
|
599
646
|
) {
|
|
600
|
-
await deps.installGlobal(
|
|
647
|
+
await deps.installGlobal(
|
|
648
|
+
SERVER_PACKAGE,
|
|
649
|
+
pair.server.version,
|
|
650
|
+
{ ignoreScripts: true },
|
|
651
|
+
);
|
|
601
652
|
}
|
|
602
653
|
const verified = await deps.currentServer();
|
|
603
654
|
if (!verified) throw new Error('server controller disappeared after installation');
|
|
@@ -920,6 +971,36 @@ async function defaultPublishedPackage(
|
|
|
920
971
|
return published;
|
|
921
972
|
}
|
|
922
973
|
|
|
974
|
+
async function defaultPublishedVersions(
|
|
975
|
+
name: typeof CLIENT_PACKAGE | typeof SERVER_PACKAGE,
|
|
976
|
+
context: NpmContext,
|
|
977
|
+
): Promise<string[]> {
|
|
978
|
+
void context;
|
|
979
|
+
const endpoint = new URL(encodeURIComponent(name), CANONICAL_NPM_REGISTRY);
|
|
980
|
+
try {
|
|
981
|
+
const response = await fetch(endpoint, {
|
|
982
|
+
headers: { Accept: 'application/json' },
|
|
983
|
+
redirect: 'error',
|
|
984
|
+
});
|
|
985
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
986
|
+
const parsed: unknown = await response.json();
|
|
987
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
988
|
+
throw new Error('response was not a package object');
|
|
989
|
+
}
|
|
990
|
+
const versions = (parsed as Record<string, unknown>).versions;
|
|
991
|
+
if (!versions || typeof versions !== 'object' || Array.isArray(versions)) {
|
|
992
|
+
throw new Error('package versions were not an object');
|
|
993
|
+
}
|
|
994
|
+
const values = Object.keys(versions);
|
|
995
|
+
if (values.length === 0 || values.some((version) => !isExactSemver(version))) {
|
|
996
|
+
throw new Error('package versions were missing or invalid');
|
|
997
|
+
}
|
|
998
|
+
return values;
|
|
999
|
+
} catch {
|
|
1000
|
+
throw new Error(`registry version lookup failed for ${name}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
923
1004
|
async function defaultConfirm(message: string): Promise<'yes' | 'no' | 'eof' | 'interrupted'> {
|
|
924
1005
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
925
1006
|
let interrupted = false;
|
|
@@ -958,11 +1039,13 @@ export function buildDefaultUpdateDeps(): UpdateDeps {
|
|
|
958
1039
|
await context(),
|
|
959
1040
|
),
|
|
960
1041
|
publishedPackage: async (name, version) => defaultPublishedPackage(name, version, await context()),
|
|
961
|
-
|
|
1042
|
+
publishedVersions: async (name) => defaultPublishedVersions(name, await context()),
|
|
1043
|
+
installGlobal: async (name, version, options) => {
|
|
962
1044
|
const npm = await context();
|
|
963
1045
|
const result = await runCommand(npm.commandPath, [
|
|
964
1046
|
'install',
|
|
965
1047
|
'--global',
|
|
1048
|
+
...(options?.ignoreScripts ? ['--ignore-scripts'] : []),
|
|
966
1049
|
`--prefix=${npm.prefix}`,
|
|
967
1050
|
`--registry=${CANONICAL_NPM_REGISTRY}`,
|
|
968
1051
|
`${name}@${version}`,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { probeCodexBridgeArmed } from './codex-app-wake.js';
|
|
2
|
+
import { checkInboxMonitorHealthy } from './stream-status.js';
|
|
3
|
+
import {
|
|
4
|
+
getOpenCodeConnectionState,
|
|
5
|
+
type OpenCodeConnectionState,
|
|
6
|
+
} from './opencode-drone.js';
|
|
7
|
+
import type { AgentKind } from './agent-runtime.js';
|
|
8
|
+
|
|
9
|
+
export interface WakePathSnapshot {
|
|
10
|
+
agentKind: AgentKind;
|
|
11
|
+
healthy: boolean | null;
|
|
12
|
+
openCode: OpenCodeConnectionState | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface InspectWakePathInputs {
|
|
16
|
+
agentKind: AgentKind;
|
|
17
|
+
active: { cubeId: string; droneId: string } | null;
|
|
18
|
+
inboxPath: string | null;
|
|
19
|
+
monitorStateRoot: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface InspectWakePathDeps {
|
|
23
|
+
checkClaudeMonitor?: typeof checkInboxMonitorHealthy;
|
|
24
|
+
probeCodex?: typeof probeCodexBridgeArmed;
|
|
25
|
+
getOpenCodeState?: typeof getOpenCodeConnectionState;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function openCodeWakePathHealthy(
|
|
29
|
+
state: OpenCodeConnectionState,
|
|
30
|
+
): boolean | null {
|
|
31
|
+
if (!state.connected) return false;
|
|
32
|
+
if (
|
|
33
|
+
state.deliveryStates.failed > 0 ||
|
|
34
|
+
state.deliveryStates['delivered-unconfirmed'] > 0
|
|
35
|
+
) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
state.deliveryStates.queued > 0 ||
|
|
40
|
+
state.deliveryStates.retried > 0
|
|
41
|
+
) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (state.sessionId === null) return null;
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function inspectWakePath(
|
|
49
|
+
inputs: InspectWakePathInputs,
|
|
50
|
+
deps: InspectWakePathDeps = {},
|
|
51
|
+
): Promise<WakePathSnapshot> {
|
|
52
|
+
if (!inputs.active) {
|
|
53
|
+
return {
|
|
54
|
+
agentKind: inputs.agentKind,
|
|
55
|
+
healthy: null,
|
|
56
|
+
openCode: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (inputs.agentKind === 'claude') {
|
|
61
|
+
const check = deps.checkClaudeMonitor ?? checkInboxMonitorHealthy;
|
|
62
|
+
return {
|
|
63
|
+
agentKind: inputs.agentKind,
|
|
64
|
+
healthy: check(inputs.inboxPath, inputs.monitorStateRoot),
|
|
65
|
+
openCode: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (inputs.agentKind === 'codex') {
|
|
70
|
+
const probe = deps.probeCodex ?? probeCodexBridgeArmed;
|
|
71
|
+
return {
|
|
72
|
+
agentKind: inputs.agentKind,
|
|
73
|
+
healthy: await probe(inputs.active),
|
|
74
|
+
openCode: null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getState = deps.getOpenCodeState ?? getOpenCodeConnectionState;
|
|
79
|
+
const openCode = getState();
|
|
80
|
+
return {
|
|
81
|
+
agentKind: inputs.agentKind,
|
|
82
|
+
healthy: openCodeWakePathHealthy(openCode),
|
|
83
|
+
openCode,
|
|
84
|
+
};
|
|
85
|
+
}
|