agent-relay-server 0.32.4 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/package.json +2 -2
  2. package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
  3. package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
  4. package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
  5. package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
  6. package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
  7. package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
  8. package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
  9. package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
  10. package/public/assets/automation-Dm6rXNxK.js +2 -0
  11. package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
  12. package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
  13. package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
  14. package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
  15. package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
  16. package/public/assets/chat-JZAEDGfX.js +2 -0
  17. package/public/assets/chat-JZAEDGfX.js.map +1 -0
  18. package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
  19. package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
  20. package/public/assets/display-Bebqs1qu.js +3 -0
  21. package/public/assets/display-Bebqs1qu.js.map +1 -0
  22. package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
  23. package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
  24. package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
  25. package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
  26. package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
  27. package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
  28. package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
  29. package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
  30. package/public/assets/maintenance-CsoOFBXx.js +2 -0
  31. package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
  32. package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
  33. package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
  34. package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
  35. package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
  36. package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
  37. package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
  38. package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
  39. package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
  40. package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
  41. package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
  42. package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
  43. package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
  44. package/public/assets/pairs-unqjPlmq.js +2 -0
  45. package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
  46. package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
  47. package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
  48. package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
  49. package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
  50. package/public/assets/store-DiSzYHj9.js +9 -0
  51. package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
  52. package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
  53. package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
  54. package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
  55. package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
  56. package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
  57. package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
  58. package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
  59. package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
  60. package/public/index.html +3 -3
  61. package/runner/src/adapter.ts +1 -1
  62. package/src/agent-lifecycle-events.ts +137 -0
  63. package/src/artifact-storage.ts +3 -5
  64. package/src/channel-target.ts +24 -0
  65. package/src/cli/_shared.ts +80 -0
  66. package/src/cli/agent-detect.ts +188 -0
  67. package/src/cli/agent-meta.ts +95 -0
  68. package/src/cli/context-probe.ts +88 -0
  69. package/src/cli/daemon.ts +111 -0
  70. package/src/cli/dev.ts +173 -0
  71. package/src/cli/index.ts +361 -0
  72. package/src/cli/introspect.ts +73 -0
  73. package/src/cli/memory.ts +37 -0
  74. package/src/cli/message.ts +201 -0
  75. package/src/cli/orchestrator.ts +227 -0
  76. package/src/cli/pair.ts +125 -0
  77. package/src/cli/provider.ts +209 -0
  78. package/src/cli/recipe.ts +110 -0
  79. package/src/cli/reply.ts +141 -0
  80. package/src/cli/setup.ts +57 -0
  81. package/src/cli/steward.ts +59 -0
  82. package/src/cli/token.ts +81 -0
  83. package/src/cli/upgrade.ts +193 -0
  84. package/src/cli/workspace.ts +215 -0
  85. package/src/cli.ts +4 -2718
  86. package/src/config-store.ts +10 -6
  87. package/src/db/activity.ts +194 -0
  88. package/src/db/agent-search.ts +174 -0
  89. package/src/db/agents.ts +551 -0
  90. package/src/db/artifacts.ts +342 -0
  91. package/src/db/channels.ts +576 -0
  92. package/src/db/connection.ts +71 -0
  93. package/src/db/delivery.ts +395 -0
  94. package/src/db/inbox.ts +249 -0
  95. package/src/db/index.ts +23 -0
  96. package/src/db/integrations.ts +339 -0
  97. package/src/db/mappers.ts +397 -0
  98. package/src/db/merge-lease.ts +160 -0
  99. package/src/db/message-reads.ts +304 -0
  100. package/src/db/messages.ts +434 -0
  101. package/src/db/migrations.ts +431 -0
  102. package/src/db/orchestrators.ts +358 -0
  103. package/src/db/pairs.ts +324 -0
  104. package/src/db/schema.ts +758 -0
  105. package/src/db/stats.ts +337 -0
  106. package/src/db/tasks.ts +407 -0
  107. package/src/db/workspaces.ts +440 -0
  108. package/src/db.ts +4 -5721
  109. package/src/maintenance.ts +4 -0
  110. package/src/mcp-errors.ts +7 -0
  111. package/src/mcp.ts +32 -34
  112. package/src/routes/agents-spawn.ts +9 -1
  113. package/src/routes/agents.ts +5 -0
  114. package/src/routes/commands.ts +15 -0
  115. package/src/routes/integrations.ts +6 -8
  116. package/src/spawn-targets.ts +159 -0
  117. package/src/utils.ts +16 -1
  118. package/public/assets/automation-CiaLThdO.js +0 -2
  119. package/public/assets/chat-5hvHZcAe.js +0 -2
  120. package/public/assets/chat-5hvHZcAe.js.map +0 -1
  121. package/public/assets/display-JI19Vc7L.js +0 -3
  122. package/public/assets/display-JI19Vc7L.js.map +0 -1
  123. package/public/assets/maintenance-DiFNzNPN.js +0 -2
  124. package/public/assets/pairs-WpKCPE1n.js +0 -2
  125. 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
+ }