agent-relay-server 0.32.4 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
- package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
- package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
- package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
- package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
- package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
- package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
- package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
- package/public/assets/automation-Dm6rXNxK.js +2 -0
- package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
- package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
- package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
- package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
- package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
- package/public/assets/chat-JZAEDGfX.js +2 -0
- package/public/assets/chat-JZAEDGfX.js.map +1 -0
- package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
- package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
- package/public/assets/display-Bebqs1qu.js +3 -0
- package/public/assets/display-Bebqs1qu.js.map +1 -0
- package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
- package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
- package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
- package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
- package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
- package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
- package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
- package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
- package/public/assets/maintenance-CsoOFBXx.js +2 -0
- package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
- package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
- package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
- package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
- package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
- package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
- package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
- package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
- package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
- package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
- package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
- package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
- package/public/assets/pairs-unqjPlmq.js +2 -0
- package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
- package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
- package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
- package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
- package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
- package/public/assets/store-DiSzYHj9.js +9 -0
- package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
- package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
- package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
- package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
- package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
- package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
- package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
- package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/src/adapter.ts +1 -1
- package/src/agent-lifecycle-events.ts +137 -0
- package/src/artifact-storage.ts +3 -5
- package/src/cli/_shared.ts +80 -0
- package/src/cli/agent-detect.ts +188 -0
- package/src/cli/agent-meta.ts +95 -0
- package/src/cli/context-probe.ts +88 -0
- package/src/cli/daemon.ts +111 -0
- package/src/cli/dev.ts +173 -0
- package/src/cli/index.ts +361 -0
- package/src/cli/introspect.ts +73 -0
- package/src/cli/memory.ts +37 -0
- package/src/cli/message.ts +201 -0
- package/src/cli/orchestrator.ts +227 -0
- package/src/cli/pair.ts +125 -0
- package/src/cli/provider.ts +209 -0
- package/src/cli/recipe.ts +110 -0
- package/src/cli/reply.ts +141 -0
- package/src/cli/setup.ts +57 -0
- package/src/cli/steward.ts +59 -0
- package/src/cli/token.ts +81 -0
- package/src/cli/upgrade.ts +193 -0
- package/src/cli/workspace.ts +215 -0
- package/src/cli.ts +4 -2718
- package/src/config-store.ts +10 -6
- package/src/maintenance.ts +4 -0
- package/src/mcp-errors.ts +7 -0
- package/src/mcp.ts +32 -34
- package/src/routes/agents-spawn.ts +9 -1
- package/src/routes/agents.ts +5 -0
- package/src/routes/commands.ts +15 -0
- package/src/spawn-targets.ts +159 -0
- package/src/utils.ts +16 -1
- package/public/assets/automation-CiaLThdO.js +0 -2
- package/public/assets/chat-5hvHZcAe.js +0 -2
- package/public/assets/chat-5hvHZcAe.js.map +0 -1
- package/public/assets/display-JI19Vc7L.js +0 -3
- package/public/assets/display-JI19Vc7L.js.map +0 -1
- package/public/assets/maintenance-DiFNzNPN.js +0 -2
- package/public/assets/pairs-WpKCPE1n.js +0 -2
- package/public/assets/store-C9VcSo05.js +0 -9
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Upgrade commands — auto-split from cli.ts (#294). Local runtime upgrade plus
|
|
2
|
+
// remote-host fan-out over the relay command bus (#210).
|
|
3
|
+
import {
|
|
4
|
+
createUpgradePlan,
|
|
5
|
+
detectUpgradeSnapshot,
|
|
6
|
+
executeUpgradePlan,
|
|
7
|
+
formatUpgradePlan,
|
|
8
|
+
resolveLocalOrchestratorId,
|
|
9
|
+
type UpgradeProvider,
|
|
10
|
+
} from "../upgrade";
|
|
11
|
+
import { VERSION } from "../config";
|
|
12
|
+
import { errMessage } from "agent-relay-sdk";
|
|
13
|
+
import { apiRequest, confirm } from "./_shared";
|
|
14
|
+
|
|
15
|
+
export async function handleUpgradeCommand(args: string[]): Promise<void> {
|
|
16
|
+
let targetVersion: string | undefined;
|
|
17
|
+
let dryRun = false;
|
|
18
|
+
let noRestart = false;
|
|
19
|
+
let restartDeferred = false;
|
|
20
|
+
let yes = false;
|
|
21
|
+
let json = false;
|
|
22
|
+
let runtimePrefix: string | undefined;
|
|
23
|
+
const pathPrefix: string[] = [];
|
|
24
|
+
const providers: UpgradeProvider[] = [];
|
|
25
|
+
const hosts: string[] = [];
|
|
26
|
+
let allHosts = false;
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
if (arg === "--version" && i + 1 < args.length) targetVersion = args[++i];
|
|
31
|
+
else if (arg === "--runtime-prefix" && i + 1 < args.length) runtimePrefix = args[++i];
|
|
32
|
+
else if (arg === "--providers" && i + 1 < args.length) {
|
|
33
|
+
for (const provider of args[++i]!.split(",")) providers.push(parseUpgradeProvider(provider));
|
|
34
|
+
} else if (arg === "--provider" && i + 1 < args.length) providers.push(parseUpgradeProvider(args[++i]!));
|
|
35
|
+
else if (arg === "--host" && i + 1 < args.length) hosts.push(args[++i]!);
|
|
36
|
+
else if (arg === "--all-hosts") allHosts = true;
|
|
37
|
+
else if (arg === "--codex") providers.push("codex");
|
|
38
|
+
else if (arg === "--claude") providers.push("claude");
|
|
39
|
+
else if (arg === "--orchestrator") providers.push("orchestrator");
|
|
40
|
+
else if (arg === "--all") providers.push("all");
|
|
41
|
+
else if (arg === "--dry-run") dryRun = true;
|
|
42
|
+
else if (arg === "--no-restart") noRestart = true;
|
|
43
|
+
else if (arg === "--restart-deferred") restartDeferred = true;
|
|
44
|
+
else if (arg === "--yes" || arg === "-y") yes = true;
|
|
45
|
+
else if (arg === "--json") json = true;
|
|
46
|
+
else throw new Error(`Unknown upgrade option "${arg}"`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remote-only: drive named hosts' self-upgrade over the relay command bus and
|
|
50
|
+
// skip the local upgrade entirely (#210). `--all-hosts` instead upgrades this
|
|
51
|
+
// host first, then fans out to every behind remote (handled after the local run).
|
|
52
|
+
if (hosts.length && !allHosts) {
|
|
53
|
+
await runRemoteOrchestratorUpgrades({ hosts, targetVersion, providers, json, dryRun, yes });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const snapshot = await detectUpgradeSnapshot({
|
|
58
|
+
...(targetVersion ? { targetVersion } : {}),
|
|
59
|
+
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
60
|
+
providers,
|
|
61
|
+
noRestart,
|
|
62
|
+
});
|
|
63
|
+
const plan = createUpgradePlan(snapshot, {
|
|
64
|
+
...(targetVersion ? { targetVersion } : {}),
|
|
65
|
+
...(runtimePrefix ? { runtimePrefix } : {}),
|
|
66
|
+
providers,
|
|
67
|
+
noRestart,
|
|
68
|
+
restartDeferred,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (json) {
|
|
72
|
+
console.log(JSON.stringify({ plan }, null, 2));
|
|
73
|
+
if (allHosts) await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, json, dryRun: true });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (dryRun) {
|
|
78
|
+
console.log(formatUpgradePlan(plan, { dryRun: true }));
|
|
79
|
+
if (allHosts) await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, dryRun: true });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!yes) {
|
|
84
|
+
console.log(formatUpgradePlan(plan));
|
|
85
|
+
const ok = await confirm("Run this upgrade plan?");
|
|
86
|
+
if (!ok) {
|
|
87
|
+
console.log("Upgrade cancelled.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(await executeUpgradePlan(plan));
|
|
93
|
+
|
|
94
|
+
if (allHosts) {
|
|
95
|
+
await runRemoteOrchestratorUpgrades({ allBehind: true, targetVersion: plan.targetVersion, providers, json, yes: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Trigger orchestrator self-upgrade on remote hosts via the relay command bus
|
|
101
|
+
* (#210). Each host installs its own runtime + self-restarts; the relay settles
|
|
102
|
+
* the version when it re-registers. Replaces the manual ssh + npm install dance.
|
|
103
|
+
*/
|
|
104
|
+
async function runRemoteOrchestratorUpgrades(opts: {
|
|
105
|
+
hosts?: string[];
|
|
106
|
+
allBehind?: boolean;
|
|
107
|
+
targetVersion?: string;
|
|
108
|
+
providers: UpgradeProvider[];
|
|
109
|
+
json?: boolean;
|
|
110
|
+
dryRun?: boolean;
|
|
111
|
+
yes?: boolean;
|
|
112
|
+
}): Promise<void> {
|
|
113
|
+
const targetVersion = opts.targetVersion ?? VERSION;
|
|
114
|
+
const orchestrators = (await apiRequest("GET", "/api/orchestrators")) as Array<{
|
|
115
|
+
id: string;
|
|
116
|
+
version?: string;
|
|
117
|
+
}>;
|
|
118
|
+
const byId = new Map(orchestrators.map((orch) => [orch.id, orch]));
|
|
119
|
+
const localId = resolveLocalOrchestratorId();
|
|
120
|
+
// Default to "all" so a remote host's provider runner is upgraded too, not just
|
|
121
|
+
// the orchestrator package (matters for hosts running claude/codex agents).
|
|
122
|
+
const remoteProviders: UpgradeProvider[] = opts.providers.length ? opts.providers : ["all"];
|
|
123
|
+
|
|
124
|
+
let targets: string[];
|
|
125
|
+
if (opts.allBehind) {
|
|
126
|
+
targets = orchestrators
|
|
127
|
+
.filter((orch) => orch.id !== localId && orch.version && orch.version !== targetVersion)
|
|
128
|
+
.map((orch) => orch.id);
|
|
129
|
+
if (!targets.length) {
|
|
130
|
+
if (opts.json) console.log(JSON.stringify({ remoteUpgrades: [] }, null, 2));
|
|
131
|
+
else console.log(`No remote orchestrators behind ${targetVersion}.`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
targets = opts.hosts ?? [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (opts.dryRun) {
|
|
139
|
+
const lines = targets.map((id) => {
|
|
140
|
+
const orch = byId.get(id);
|
|
141
|
+
const from = orch ? orch.version ?? "unknown" : "(not connected)";
|
|
142
|
+
return ` ${id}: ${from} → ${targetVersion} (providers: ${remoteProviders.join(",")})`;
|
|
143
|
+
});
|
|
144
|
+
if (opts.json) console.log(JSON.stringify({ remoteUpgrades: targets.map((id) => ({ id, targetVersion, providers: remoteProviders, dryRun: true })) }, null, 2));
|
|
145
|
+
else console.log(`Remote orchestrator upgrade plan → ${targetVersion}:\n${lines.join("\n")}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!opts.yes && !opts.json) {
|
|
150
|
+
console.log(`Trigger remote orchestrator upgrade → ${targetVersion} for: ${targets.join(", ")}`);
|
|
151
|
+
const ok = await confirm("Send remote upgrade command(s)?");
|
|
152
|
+
if (!ok) {
|
|
153
|
+
console.log("Remote upgrade cancelled.");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const results: Array<{ id: string; ok: boolean; message: string }> = [];
|
|
159
|
+
for (const id of targets) {
|
|
160
|
+
if (!byId.has(id)) {
|
|
161
|
+
results.push({ id, ok: false, message: "not connected to the relay" });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const res = (await apiRequest("POST", `/api/orchestrators/${encodeURIComponent(id)}/actions`, {
|
|
166
|
+
action: "upgrade",
|
|
167
|
+
targetVersion,
|
|
168
|
+
providers: remoteProviders,
|
|
169
|
+
})) as { command?: { id?: string } };
|
|
170
|
+
const from = byId.get(id)?.version;
|
|
171
|
+
results.push({ id, ok: true, message: `queued ${from ?? "?"} → ${targetVersion} (command ${res?.command?.id ?? "?"})` });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
results.push({ id, ok: false, message: errMessage(err) });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (opts.json) {
|
|
178
|
+
console.log(JSON.stringify({ remoteUpgrades: results }, null, 2));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(`\nRemote orchestrator upgrades → ${targetVersion}:`);
|
|
182
|
+
for (const result of results) console.log(` ${result.ok ? "✓" : "✗"} ${result.id}: ${result.message}`);
|
|
183
|
+
console.log("\nEach host installs and self-restarts; the relay reconciles the version when it re-registers.");
|
|
184
|
+
console.log("Track progress in the dashboard Orchestrators view or via GET /api/orchestrators.");
|
|
185
|
+
if (results.some((result) => !result.ok)) {
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseUpgradeProvider(value: string): UpgradeProvider {
|
|
191
|
+
if (value === "auto" || value === "all" || value === "codex" || value === "claude" || value === "orchestrator") return value;
|
|
192
|
+
throw new Error(`Unknown upgrade provider "${value}". Expected auto, all, codex, claude, or orchestrator.`);
|
|
193
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Workspace commands — auto-split from cli.ts (#294). Self-service lifecycle for
|
|
2
|
+
// agents in isolated worktrees (#205): status/ready/land/claim/release/diagnostics/
|
|
3
|
+
// deps/list/cleanup-stale.
|
|
4
|
+
import { apiRequest } from "./_shared";
|
|
5
|
+
import { detectAgentId } from "./agent-detect";
|
|
6
|
+
import { describeWorkspacePhase, readyContract, type WorkspacePhaseView } from "../workspace-phase";
|
|
7
|
+
import type { WorkspaceDepsRefreshResult } from "agent-relay-sdk";
|
|
8
|
+
|
|
9
|
+
export const WORKSPACE_USAGE = "Usage: agent-relay workspace <status|diagnostics|ready|land|claim|release|list|cleanup-stale|deps> [--id ID] [--strategy ...] [--purpose TEXT] [--repo PATH] [--wait] [--timeout SECONDS] [--check] [--execute] [--json]";
|
|
10
|
+
|
|
11
|
+
// The agent's own isolated-workspace id, published in AGENT_RELAY_WORKSPACE_JSON
|
|
12
|
+
// by the orchestrator at spawn. Undefined for shared-workspace / non-managed agents.
|
|
13
|
+
function currentWorkspaceId(): string | undefined {
|
|
14
|
+
const json = process.env.AGENT_RELAY_WORKSPACE_JSON;
|
|
15
|
+
if (!json) return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(json) as { id?: string };
|
|
18
|
+
return typeof parsed.id === "string" && parsed.id ? parsed.id : undefined;
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatWorkspaceStatus(ws: any, extra?: { guidance?: WorkspacePhaseView; landed?: string | null }): string {
|
|
25
|
+
// Render the directive projection so the agent gets "what does this mean / what
|
|
26
|
+
// do I do next" inline, not a bare enum it has to decode (#235). Computed
|
|
27
|
+
// client-side from the record (the projection is pure) unless the wait response
|
|
28
|
+
// already carried it.
|
|
29
|
+
const guidance = extra?.guidance ?? describeWorkspacePhase(ws);
|
|
30
|
+
const lines = [
|
|
31
|
+
`Workspace ${ws.id}`,
|
|
32
|
+
` status: ${ws.status} (${guidance.phase}${guidance.actionNeeded ? "" : " — no action needed"})`,
|
|
33
|
+
` branch: ${ws.branch ?? "(none)"}`,
|
|
34
|
+
` base: ${ws.baseRef ?? "(none)"}`,
|
|
35
|
+
` worktree: ${ws.worktreePath ?? "(none)"}`,
|
|
36
|
+
"",
|
|
37
|
+
` ${guidance.headline}`,
|
|
38
|
+
` ${guidance.hint}`,
|
|
39
|
+
];
|
|
40
|
+
if (guidance.blockers.length) {
|
|
41
|
+
lines.push("", " Blockers:");
|
|
42
|
+
for (const b of guidance.blockers) lines.push(` - ${b}`);
|
|
43
|
+
}
|
|
44
|
+
if (guidance.nextActions.length) {
|
|
45
|
+
lines.push("", " Next:");
|
|
46
|
+
for (const a of guidance.nextActions) lines.push(` - ${a.cli ?? a.tool} — ${a.when}`);
|
|
47
|
+
}
|
|
48
|
+
if (extra?.landed) lines.push("", ` ${extra.landed}`);
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Poll a command to a terminal state (succeeded/failed). Returns undefined on
|
|
53
|
+
// timeout so the caller can degrade to "dispatched, check later".
|
|
54
|
+
async function pollCommand(id: string, timeoutMs: number): Promise<{ status?: string; result?: unknown; error?: string } | undefined> {
|
|
55
|
+
const deadline = Date.now() + timeoutMs;
|
|
56
|
+
while (Date.now() < deadline) {
|
|
57
|
+
const cmd = await apiRequest("GET", `/api/commands/${encodeURIComponent(id)}`) as { status?: string; result?: unknown; error?: string };
|
|
58
|
+
if (cmd.status === "succeeded" || cmd.status === "failed") return cmd;
|
|
59
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatDepsRefresh(result: WorkspaceDepsRefreshResult, checkOnly: boolean): string {
|
|
65
|
+
if (result.error && (!result.dirs || result.dirs.length === 0)) return `Deps ${checkOnly ? "check" : "refresh"}: ${result.error}`;
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
for (const d of result.dirs) {
|
|
68
|
+
const icon = d.status === "installed" ? "↻" : d.status === "stale" ? "✗" : d.status === "failed" ? "!" : "✓";
|
|
69
|
+
const detail = d.status === "ok" ? "up to date"
|
|
70
|
+
: d.status === "installed" ? `reinstalled${d.wasSymlink ? " (was symlinked)" : ""}`
|
|
71
|
+
: d.status === "stale" ? `stale — missing ${d.missing?.join(", ") ?? "?"}`
|
|
72
|
+
: `failed — ${d.error ?? "unknown"}`;
|
|
73
|
+
lines.push(` ${icon} ${d.dir}: ${detail}`);
|
|
74
|
+
}
|
|
75
|
+
const header = checkOnly
|
|
76
|
+
? (result.stale ? "Deps check: stale dirs found — run `agent-relay workspace deps` to refresh" : "Deps check: all dirs up to date")
|
|
77
|
+
: (result.refreshed ? "Deps refreshed" : result.error ? "Deps refresh hit errors" : "Deps already up to date");
|
|
78
|
+
return [header, ...lines].join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Self-service workspace lifecycle for agents in isolated worktrees (#205) plus
|
|
82
|
+
// steward coordination (#208).
|
|
83
|
+
// status — read your workspace row ready — hand off for review/landing
|
|
84
|
+
// land — request a base merge (operator) list — all workspaces
|
|
85
|
+
// diagnostics — joined briefing + recommended action
|
|
86
|
+
// claim/release — TTL'd steward lease auto-merge yields to
|
|
87
|
+
// cleanup-stale — guarded batch cleanup of stale worktrees (dry-run by default)
|
|
88
|
+
export async function handleWorkspaceCommand(args: string[]): Promise<void> {
|
|
89
|
+
const action = args[0];
|
|
90
|
+
const valid = new Set(["status", "ready", "land", "list", "diagnostics", "diag", "claim", "release", "cleanup-stale", "deps"]);
|
|
91
|
+
if (action === "--help" || action === "-h" || action === "help") {
|
|
92
|
+
console.log(WORKSPACE_USAGE);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!action || !valid.has(action)) {
|
|
96
|
+
throw new Error(WORKSPACE_USAGE);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let id = currentWorkspaceId(), idExplicit = false; // idExplicit: --id was passed, not the ambient default (#307)
|
|
100
|
+
let strategy: string | undefined;
|
|
101
|
+
let purpose: string | undefined;
|
|
102
|
+
let repo: string | undefined;
|
|
103
|
+
let execute = false;
|
|
104
|
+
let check = false;
|
|
105
|
+
let json = false;
|
|
106
|
+
let wait = false;
|
|
107
|
+
let timeoutSeconds: number | undefined;
|
|
108
|
+
for (let i = 1; i < args.length; i++) {
|
|
109
|
+
const arg = args[i];
|
|
110
|
+
if (arg === "--id" && i + 1 < args.length) { id = args[++i]; idExplicit = true; }
|
|
111
|
+
else if (arg === "--strategy" && i + 1 < args.length) strategy = args[++i];
|
|
112
|
+
else if (arg === "--purpose" && i + 1 < args.length) purpose = args[++i];
|
|
113
|
+
else if (arg === "--repo" && i + 1 < args.length) repo = args[++i];
|
|
114
|
+
else if (arg === "--execute") execute = true;
|
|
115
|
+
else if (arg === "--check") check = true;
|
|
116
|
+
else if (arg === "--refresh") check = false; // explicit no-op default for clarity
|
|
117
|
+
else if (arg === "--wait") wait = true;
|
|
118
|
+
else if (arg === "--timeout" && i + 1 < args.length) {
|
|
119
|
+
const parsed = Number.parseInt(args[++i]!, 10);
|
|
120
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error("--timeout must be a positive number of seconds");
|
|
121
|
+
timeoutSeconds = parsed;
|
|
122
|
+
}
|
|
123
|
+
else if (arg === "--json") json = true;
|
|
124
|
+
else throw new Error(`Unknown workspace option "${arg}".`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (action === "list") {
|
|
128
|
+
console.log(JSON.stringify(await apiRequest("GET", "/api/workspaces"), null, 2));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (action === "cleanup-stale") {
|
|
133
|
+
const result = await apiRequest("POST", "/api/workspaces/actions/cleanup-stale", { repoRoot: repo, dryRun: !execute, ...(idExplicit && id ? { workspaceId: id } : {}) });
|
|
134
|
+
console.log(JSON.stringify(result, null, 2));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!id) throw new Error("No current workspace detected (AGENT_RELAY_WORKSPACE_JSON unset). Pass --id WORKSPACE_ID — only isolated-workspace agents have one.");
|
|
139
|
+
|
|
140
|
+
if (action === "status") {
|
|
141
|
+
// --wait long-polls via the action endpoint (server blocks until the status
|
|
142
|
+
// changes — the blessed way to wait for an auto-merge to land, #235), and the
|
|
143
|
+
// response carries the directive projection + land receipt. Plain status is a
|
|
144
|
+
// bare GET; the projection is computed client-side for rendering.
|
|
145
|
+
if (wait) {
|
|
146
|
+
const res = await apiRequest("POST", `/api/workspaces/${encodeURIComponent(id)}/actions`, {
|
|
147
|
+
action: "status",
|
|
148
|
+
wait: true,
|
|
149
|
+
...(timeoutSeconds ? { timeoutSeconds } : {}),
|
|
150
|
+
}) as { workspace?: any; guidance?: WorkspacePhaseView; landed?: string | null };
|
|
151
|
+
if (json) { console.log(JSON.stringify(res, null, 2)); return; }
|
|
152
|
+
console.log(formatWorkspaceStatus(res.workspace ?? res, { guidance: res.guidance, landed: res.landed }));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const ws = await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}`);
|
|
156
|
+
if (json) console.log(JSON.stringify(ws, null, 2));
|
|
157
|
+
else console.log(formatWorkspaceStatus(ws));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (action === "diagnostics" || action === "diag") {
|
|
162
|
+
console.log(JSON.stringify(await apiRequest("GET", `/api/workspaces/${encodeURIComponent(id)}/diagnostics`), null, 2));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Refresh (or --check) deps the shared symlinked node_modules has gone stale on
|
|
167
|
+
// (#51). Emits a host command; poll it to a terminal state so the agent gets a
|
|
168
|
+
// synchronous result and knows when to re-run typecheck.
|
|
169
|
+
if (action === "deps") {
|
|
170
|
+
const from = await detectAgentId();
|
|
171
|
+
const res = await apiRequest("POST", `/api/workspaces/${encodeURIComponent(id)}/actions`, { action: "deps-refresh", agentId: from, checkOnly: check }) as { command?: { id?: string } };
|
|
172
|
+
const commandId = res.command?.id;
|
|
173
|
+
const settled = commandId ? await pollCommand(commandId, 180_000) : undefined;
|
|
174
|
+
const result = (settled?.result ?? null) as WorkspaceDepsRefreshResult | null;
|
|
175
|
+
if (json) {
|
|
176
|
+
console.log(JSON.stringify(settled ?? res, null, 2));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (settled?.status === "failed") {
|
|
180
|
+
console.error(`Deps ${check ? "check" : "refresh"} failed: ${settled.error ?? "unknown error"}`);
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!result) {
|
|
185
|
+
console.log(`Deps ${check ? "check" : "refresh"} dispatched (command ${commandId ?? "?"}) — host did not report back in time. Check \`agent-relay workspace deps --json\`.`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
console.log(formatDepsRefresh(result, check));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const from = await detectAgentId();
|
|
193
|
+
const actionBody: Record<string, unknown> =
|
|
194
|
+
action === "ready" ? { action: "request-review", agentId: from }
|
|
195
|
+
: action === "claim" ? { action: "claim", agentId: from, purpose }
|
|
196
|
+
: action === "release" ? { action: "release-claim", agentId: from }
|
|
197
|
+
: { action: "merge", agentId: from, ...(strategy ? { strategy } : {}) };
|
|
198
|
+
const result = await apiRequest("POST", `/api/workspaces/${encodeURIComponent(id)}/actions`, actionBody);
|
|
199
|
+
if (json) {
|
|
200
|
+
console.log(JSON.stringify(result, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (action === "ready") {
|
|
204
|
+
// Print the whole contract up front so the agent isn't left decoding status
|
|
205
|
+
// enums over the next minutes (#235). `result.workspace` is the post-ready row.
|
|
206
|
+
const ws = (result as { workspace?: any }).workspace ?? { status: "review_requested" };
|
|
207
|
+
console.log(`Workspace ${id} marked ready.\n\n${readyContract(ws)}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
console.log(
|
|
211
|
+
action === "claim" ? `Workspace ${id} claimed${purpose ? ` (${purpose})` : ""} — auto-merge will yield until released or the claim expires.`
|
|
212
|
+
: action === "release" ? `Workspace ${id} claim released.`
|
|
213
|
+
: `Workspace ${id} merge requested (${strategy ?? "auto"}).`,
|
|
214
|
+
);
|
|
215
|
+
}
|