agent-relay-server 0.21.0 → 0.22.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/docs/openapi.json +4 -1
- package/package.json +2 -2
- package/public/index.html +227 -94
- package/src/cli.ts +62 -8
- package/src/maintenance.ts +9 -4
- package/src/mcp.ts +244 -2
- package/src/routes.ts +79 -167
- package/src/workspace-actions.ts +336 -0
- package/src/workspace-phase.ts +181 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Directive status projection (#214 §2 / #235) — the single home for "what does
|
|
2
|
+
// this workspace state mean to the branch agent, and what should it do next."
|
|
3
|
+
//
|
|
4
|
+
// Why its own module (no server deps): a branch agent reads raw `status` enums
|
|
5
|
+
// and can't tell "working as designed" from "stuck" — `review_requested` looks
|
|
6
|
+
// like an escalation, an absent steward looks like nobody's handling it, the
|
|
7
|
+
// recycled `…--2` branch looks alarming. This pure projection is rendered by the
|
|
8
|
+
// CLI (`agent-relay workspace status`, a remote HTTP client) AND returned by the
|
|
9
|
+
// in-process MCP status tool, so the guidance can never drift between surfaces.
|
|
10
|
+
// Keeping it dependency-light (types only) means the CLI can import it without
|
|
11
|
+
// dragging in db/sse/merge — the server graph stays out of the client.
|
|
12
|
+
//
|
|
13
|
+
// The load-bearing decision: `ready` and `review_requested` collapse to the SAME
|
|
14
|
+
// "handed off, healthy, wait" guidance, and `actionNeeded:false` is the explicit
|
|
15
|
+
// anti-panic signal.
|
|
16
|
+
|
|
17
|
+
import type { WorkspaceRecord, WorkspaceStatus } from "./types";
|
|
18
|
+
|
|
19
|
+
// Statuses where the worktree's lifecycle is over — landed or torn down. Single
|
|
20
|
+
// home; imported by maintenance (stale reap), routes (orphan scan), and the MCP
|
|
21
|
+
// initialize primer (don't brief an agent on a dead workspace). Was duplicated.
|
|
22
|
+
export const TERMINAL_WORKSPACE_STATUSES = new Set<WorkspaceStatus>(["cleaned", "merged", "abandoned"]);
|
|
23
|
+
|
|
24
|
+
export type WorkspacePhase =
|
|
25
|
+
| "working" // active — your turn: commit, then mark ready
|
|
26
|
+
| "land-pending" // ready | review_requested — handed off; auto-merge will land it
|
|
27
|
+
| "landing" // merge_planned — merge dispatched, in progress
|
|
28
|
+
| "steward" // conflict — auto-merge couldn't; a steward is reconciling (not your job)
|
|
29
|
+
| "landed" // merged — on the base; a fresh rebased branch is coming
|
|
30
|
+
| "closed"; // abandoned | cleanup_requested | cleaned — torn down
|
|
31
|
+
|
|
32
|
+
export interface WorkspaceNextAction {
|
|
33
|
+
/** MCP tool to call (when the agent is on the MCP surface). */
|
|
34
|
+
tool?: string;
|
|
35
|
+
/** Equivalent CLI invocation (when the agent is on the shell surface). */
|
|
36
|
+
cli?: string;
|
|
37
|
+
/** When/why to take this step. */
|
|
38
|
+
when: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkspacePhaseView {
|
|
42
|
+
phase: WorkspacePhase;
|
|
43
|
+
/** One-line meaning of the current state, in the agent's terms. */
|
|
44
|
+
headline: string;
|
|
45
|
+
/** What to do — or an explicit statement that nothing is required. */
|
|
46
|
+
hint: string;
|
|
47
|
+
/** The crux: false means "do not intervene, wait" — the anti-panic flag. */
|
|
48
|
+
actionNeeded: boolean;
|
|
49
|
+
nextActions: WorkspaceNextAction[];
|
|
50
|
+
/** Real blockers the agent should know about (rare — most "stuck"-looking states aren't). */
|
|
51
|
+
blockers: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const WAIT_ACTION: WorkspaceNextAction = {
|
|
55
|
+
tool: "relay_workspace_status (wait:true)",
|
|
56
|
+
cli: "agent-relay workspace status --wait",
|
|
57
|
+
when: "to block until your branch lands (returns when the status changes)",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const READY_ACTION: WorkspaceNextAction = {
|
|
61
|
+
tool: "relay_workspace_ready",
|
|
62
|
+
cli: "agent-relay workspace ready",
|
|
63
|
+
when: "after you commit your work in this worktree",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Map every WorkspaceStatus to the branch agent's mental model. Statuses that
|
|
67
|
+
// look scary but are healthy (review_requested, conflict) carry actionNeeded:false
|
|
68
|
+
// and an explicit "not your job" hint.
|
|
69
|
+
export function describeWorkspacePhase(workspace: Pick<WorkspaceRecord, "status" | "branch" | "stewardAgentId">): WorkspacePhaseView {
|
|
70
|
+
switch (workspace.status) {
|
|
71
|
+
case "active":
|
|
72
|
+
return {
|
|
73
|
+
phase: "working",
|
|
74
|
+
headline: "Your isolated branch — work in progress.",
|
|
75
|
+
hint: "Commit your work in this worktree, then mark it ready. Relay rebases onto the latest base, lands, and pushes for you — do NOT push, rebase, or merge yourself, and never touch the main checkout.",
|
|
76
|
+
actionNeeded: true,
|
|
77
|
+
nextActions: [READY_ACTION],
|
|
78
|
+
blockers: [],
|
|
79
|
+
};
|
|
80
|
+
case "ready":
|
|
81
|
+
case "review_requested":
|
|
82
|
+
// The #235 crux: these are the SAME healthy "handed off, waiting" state.
|
|
83
|
+
// `review_requested` reads like an escalation but is the normal post-ready
|
|
84
|
+
// node; an absent steward is the healthy case, not a stall.
|
|
85
|
+
return {
|
|
86
|
+
phase: "land-pending",
|
|
87
|
+
headline: "Handed off — waiting for the auto-merge to land your branch. This is the normal, healthy post-ready state (not an escalation).",
|
|
88
|
+
hint: "No action needed. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned only if it can't land deterministically — so no steward visible means it's healthy, not stuck. Wait. Do NOT merge, push, resolve anything, or touch the main checkout.",
|
|
89
|
+
actionNeeded: false,
|
|
90
|
+
nextActions: [WAIT_ACTION],
|
|
91
|
+
blockers: [],
|
|
92
|
+
};
|
|
93
|
+
case "merge_planned":
|
|
94
|
+
return {
|
|
95
|
+
phase: "landing",
|
|
96
|
+
headline: "Merge dispatched — landing your branch on the base now.",
|
|
97
|
+
hint: "No action needed. When it completes you'll be moved onto a fresh rebased branch (the name gains a `--N` suffix — expected) to keep working.",
|
|
98
|
+
actionNeeded: false,
|
|
99
|
+
nextActions: [WAIT_ACTION],
|
|
100
|
+
blockers: [],
|
|
101
|
+
};
|
|
102
|
+
case "conflict":
|
|
103
|
+
return {
|
|
104
|
+
phase: "steward",
|
|
105
|
+
headline: "Auto-merge hit a conflict — a steward agent is reconciling it. Resolving it is NOT your job.",
|
|
106
|
+
hint: "Do not resolve the conflict, rebase, merge, or push yourself. The steward rebases and lands it, escalating to the human only if it truly can't. You can keep working or wait for it to land.",
|
|
107
|
+
actionNeeded: false,
|
|
108
|
+
nextActions: [WAIT_ACTION],
|
|
109
|
+
blockers: ["rebase/merge conflict against the base — a steward is handling it"],
|
|
110
|
+
};
|
|
111
|
+
case "merged":
|
|
112
|
+
return {
|
|
113
|
+
phase: "landed",
|
|
114
|
+
headline: "✅ Landed — your commits are on the base.",
|
|
115
|
+
hint: "A fresh rebased branch is being prepared; you'll continue on it (the branch name gains a `--N` suffix — expected, not an error). Keep working.",
|
|
116
|
+
actionNeeded: false,
|
|
117
|
+
nextActions: [],
|
|
118
|
+
blockers: [],
|
|
119
|
+
};
|
|
120
|
+
case "abandoned":
|
|
121
|
+
case "cleanup_requested":
|
|
122
|
+
case "cleaned":
|
|
123
|
+
return {
|
|
124
|
+
phase: "closed",
|
|
125
|
+
headline: "This workspace is being torn down.",
|
|
126
|
+
hint: "No further branch work happens here. If you still have work to do, the host will spawn you into a fresh workspace.",
|
|
127
|
+
actionNeeded: false,
|
|
128
|
+
nextActions: [],
|
|
129
|
+
blockers: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Plain-language contract printed/returned right when an agent marks a workspace
|
|
135
|
+
// ready, so the whole "what happens next" is stated up front instead of being
|
|
136
|
+
// decoded from status enums over the following minutes (#235).
|
|
137
|
+
export function readyContract(workspace: Pick<WorkspaceRecord, "status">): string {
|
|
138
|
+
return [
|
|
139
|
+
`Marked ready. Status is now \`${workspace.status}\` — this is the normal, healthy hand-off state, not an escalation.`,
|
|
140
|
+
"Relay auto-merges clean rebases roughly every 2 minutes. A steward agent is spawned (after a short delay) ONLY if it can't land deterministically — so no steward appearing means it's working, not stuck.",
|
|
141
|
+
"Wait with `relay_workspace_status wait:true` (or `agent-relay workspace status --wait`) — it returns the moment your branch lands.",
|
|
142
|
+
"On landing you'll be moved onto a fresh rebased branch (its name gains a `--N` suffix — expected). Keep working there.",
|
|
143
|
+
"Do NOT merge, push, rebase, resolve conflicts, or `cd` into the main checkout — Relay (and the steward) own all of that.",
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Mode-tailored MCP `initialize` instructions primer (#214 §3). The relay's MCP
|
|
148
|
+
// endpoint returns this in the `instructions` field ONLY for callers that own an
|
|
149
|
+
// isolated worktree, so worktree agents get a concise one-time map of the
|
|
150
|
+
// merge-back flow even if they connected without the runner's spawn briefing —
|
|
151
|
+
// and shared-workspace agents never see the noise. Kept short and in the same
|
|
152
|
+
// home as the projection so the wording can't drift from `status`/`ready`.
|
|
153
|
+
export function worktreeMcpInstructions(workspace: Pick<WorkspaceRecord, "branch" | "baseRef">): string {
|
|
154
|
+
const branch = workspace.branch ? `\`${workspace.branch}\`` : "an isolated agent branch";
|
|
155
|
+
const base = workspace.baseRef ? `\`${workspace.baseRef}\`` : "the base branch";
|
|
156
|
+
return [
|
|
157
|
+
`You are in an isolated git worktree on branch ${branch}, based on ${base} — NOT the main checkout. ${base} moves under you as other agents land in parallel; that's expected.`,
|
|
158
|
+
"Changes reach the base via: commit your work, then call `relay_workspace_ready`. Relay rebases onto the latest base, lands, and pushes for you.",
|
|
159
|
+
"Do NOT push, rebase, merge, resolve conflicts, or `cd` into the main checkout — Relay (and a steward, spawned only if a clean auto-merge isn't possible) own all of that.",
|
|
160
|
+
"After `ready` the status is `review_requested` — that is the NORMAL, healthy hand-off state, not a stall. Call `relay_workspace_status` with `wait:true` to block until your branch lands; you'll then continue on a fresh rebased branch (name gains a `--N` suffix).",
|
|
161
|
+
"Call `relay_workspace_status` anytime to see where you are and the exact next step.",
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Positive land receipt for the wait-result path: when a long-poll observes the
|
|
166
|
+
// status leave a pending/landing state into landed/recycled, tell the agent its
|
|
167
|
+
// work is on the base and which branch to keep working on. Returns null when the
|
|
168
|
+
// transition isn't a land (so callers can omit the field).
|
|
169
|
+
export function landReceipt(
|
|
170
|
+
fromStatus: WorkspaceStatus | undefined,
|
|
171
|
+
workspace: Pick<WorkspaceRecord, "status" | "branch">,
|
|
172
|
+
): string | null {
|
|
173
|
+
const wasPending = fromStatus === "ready" || fromStatus === "review_requested" || fromStatus === "merge_planned" || fromStatus === "conflict";
|
|
174
|
+
if (!wasPending) return null;
|
|
175
|
+
// merged, or recycled straight back to active on a fresh branch.
|
|
176
|
+
if (workspace.status === "merged" || workspace.status === "active") {
|
|
177
|
+
const branch = workspace.branch ? `\`${workspace.branch}\`` : "a fresh rebased branch";
|
|
178
|
+
return `✅ Landed — your commits are on the base. You're now on ${branch}; keep working there.`;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|